diff --git a/one_compliance/one_compliance/doc_events/sales_order.py b/one_compliance/one_compliance/doc_events/sales_order.py
index 8f4d9db3..7db61421 100644
--- a/one_compliance/one_compliance/doc_events/sales_order.py
+++ b/one_compliance/one_compliance/doc_events/sales_order.py
@@ -2,474 +2,377 @@
import frappe
from frappe import _
-from frappe.utils import add_days, add_months, date_diff, getdate, json, today
-from one_compliance.one_compliance.utils import add_custom as add_assign
-from one_compliance.one_compliance.utils import create_todo, get_users_with_role
+from frappe.utils import add_days, date_diff, getdate, json, today
+from one_compliance.one_compliance.utils import (
+ add_custom as add_assign,
+ create_todo,
+ get_users_with_role,
+ create_compliance_project,
+)
+
+
+# JOURNAL ENTRY HANDLING
@frappe.whitelist()
def update_journal_entry(doc):
if doc.custom_reimbursement_details:
for reimbursement_detail in doc.custom_reimbursement_details:
- if frappe.db.exists('Journal Entry', reimbursement_detail.journal_entry):
- entry_doc = frappe.get_doc('Journal Entry', reimbursement_detail.journal_entry)
+ if frappe.db.exists(
+ "Journal Entry",
+ reimbursement_detail.journal_entry,
+ ):
+ entry_doc = frappe.get_doc(
+ "Journal Entry",
+ reimbursement_detail.journal_entry,
+ )
+
if entry_doc and entry_doc.docstatus == 0:
entry_doc.posting_date = reimbursement_detail.date
- entry_doc.user_remark = reimbursement_detail.user_remark
+ entry_doc.user_remark = (
+ reimbursement_detail.user_remark
+ )
+
for account in entry_doc.accounts:
if account.debit_in_account_currency:
- account.debit_in_account_currency = reimbursement_detail.amount
+ account.debit_in_account_currency = (
+ reimbursement_detail.amount
+ )
else:
- account.credit_in_account_currency = reimbursement_detail.amount
+ account.credit_in_account_currency = (
+ reimbursement_detail.amount
+ )
+
entry_doc.save()
- frappe.msgprint("Journal Entry Updated", indicator="blue", alert=1)
+ frappe.msgprint(
+ "Journal Entry Updated",
+ indicator="blue",
+ alert=1,
+ )
+
@frappe.whitelist()
def submit_journal_entry(journal_entry):
- if frappe.db.exists('Journal Entry', journal_entry):
- journal_entry_doc = frappe.get_doc('Journal Entry', journal_entry)
+ if frappe.db.exists("Journal Entry", journal_entry):
+ journal_entry_doc = frappe.get_doc(
+ "Journal Entry",
+ journal_entry,
+ )
+
if journal_entry_doc.docstatus == 0:
journal_entry_doc.submit()
return True
else:
- frappe.throw(_("Journal Entry is already submitted."))
+ frappe.throw(
+ _("Journal Entry is already submitted.")
+ )
+
+
+# SALES ORDER → PROJECT CREATION
@frappe.whitelist()
def create_project_on_submit(doc, method):
- assign_to = []
- if(doc.custom_create_project_automatically):
- for employee_list in doc.custom_assign_to:
- employee_name = frappe.get_doc('Employee', employee_list.employee)
- assign_to.append(employee_name.name)
- assign_to_str = json.dumps(assign_to)
- for item in doc.items:
- create_project_from_sales_order(doc.name, doc.custom_expected_start_date, item.item_code, doc.custom_priority, assign_to_str, doc.custom_expected_end_date, custom_instructions=item.custom_instructions)
+ """
+ Hook on Submit:
+ Create Projects for items using the utility function.
+ """
+ if not doc.custom_create_project_automatically:
+ return
-@frappe.whitelist()
-def get_compliance_subcategory(item_code):
- # Fetch compliance subcategory based on the item code
- compliance_subcategory = frappe.get_doc('Compliance Sub Category', {'item_code': item_code})
- return {
- 'compliance_category': compliance_subcategory.compliance_category,
- 'name': compliance_subcategory.name,
- 'project_template': compliance_subcategory.project_template
- }
+ assign_to_ids = []
+ for employee_row in doc.custom_assign_to:
+ assign_to_ids.append(employee_row.employee)
-@frappe.whitelist()
-def create_project_from_sales_order(sales_order, start_date, item_code, priority, assign_to=None, expected_end_date=None, remark=None, custom_instructions=None):
- """Create project from sales order with tasks based on project template"""
- employees = json.loads(assign_to) if assign_to else []
-
- # Fetch required documents
- sales_order_doc = frappe.get_doc('Sales Order', sales_order)
- compliance_sub_category = frappe.get_doc('Compliance Sub Category', {'item_code': item_code})
-
- if not compliance_sub_category.project_template:
- frappe.throw(
- title=_('ALERT !!'),
- msg=_(f'Project Template does not exist for {compliance_sub_category.name}')
+ for item in doc.items:
+
+ naming_conf = {
+ "auto": doc.custom_project_name_automatically,
+ "custom_name": doc.custom_project_name,
+ }
+
+ project_args = {
+ "compliance_sub_category": item.item_code,
+ "customer": doc.customer,
+ "company": doc.company,
+ "start_date": doc.custom_expected_start_date
+ or today(),
+ "sales_order": doc.name,
+ "assign_to_employees": assign_to_ids,
+ "is_premium": doc.get("is_premium_project"),
+ "priority": doc.custom_priority,
+ "custom_instructions": item.custom_instructions,
+ "naming_override": naming_conf,
+ }
+
+ project = create_compliance_project(
+ project_args
)
-
- project_template_doc = frappe.get_doc('Project Template', compliance_sub_category.project_template)
- head_of_department = frappe.db.get_value(
- 'Employee',
- {'employee': compliance_sub_category.head_of_department},
- 'user_id'
- )
-
- # Validate assignees
- if not assign_to and not _has_template_assignees(project_template_doc):
- frappe.msgprint("Project can't be created since no assignees are specified in tasks")
- return
-
- # Create project
- project = _create_project(
- sales_order_doc,
- compliance_sub_category,
- project_template_doc,
- start_date,
- expected_end_date,
- priority,
- remark,
- custom_instructions
- )
-
- # Assign to head of department
- if compliance_sub_category.head_of_department:
- _assign_to_head_of_department(project.name, head_of_department, 'Project')
-
- # Assign to additional employees
- if assign_to:
- _assign_to_employees(employees, project.name, head_of_department, 'Project')
-
- frappe.msgprint(f'Project Created for {compliance_sub_category.name}.', alert=1)
-
- # Create tasks from template
- _create_tasks_from_template(
- project,
- project_template_doc,
- compliance_sub_category,
- start_date,
- employees,
- head_of_department
- )
-
- # Create premium tasks if applicable
- if sales_order_doc.get("is_premium_project") and hasattr(project_template_doc, "premium_tasks"):
- _create_premium_tasks(
- project,
- project_template_doc,
- compliance_sub_category,
- start_date,
- employees,
- head_of_department
+
+ if doc.custom_expected_end_date and project:
+ project.expected_end_date = (
+ doc.custom_expected_end_date
+ )
+ project.save(ignore_permissions=True)
+
+ frappe.msgprint(
+ f"Project Created for {item.item_name}.",
+ alert=1,
)
-
- frappe.db.commit()
-def _has_template_assignees(project_template_doc):
- """Check if any template task has assignees"""
- return any(
- template_task.type and template_task.employee_or_group
- for template_task in project_template_doc.tasks
+@frappe.whitelist()
+def get_compliance_subcategory(item_code):
+ compliance_subcategory = frappe.get_doc(
+ "Compliance Sub Category",
+ {"item_code": item_code},
)
-
-def _get_naming_info(start_date, compliance_sub_category):
- """Calculate naming year, month, and quarter based on repeat settings"""
- project_based_on_prior_phase = compliance_sub_category.project_based_on_prior_phase
- repeat_on = compliance_sub_category.repeat_on
-
- reference_date = add_months(getdate(start_date), -1) if project_based_on_prior_phase else getdate(start_date)
- naming_year = reference_date.year
- naming_month = reference_date.strftime("%B")
-
- # Determine quarter
- month_to_quarter = {
- **dict.fromkeys(['January', 'February', 'March'], 'Quarter 1'),
- **dict.fromkeys(['April', 'May', 'June'], 'Quarter 2'),
- **dict.fromkeys(['July', 'August', 'September'], 'Quarter 3'),
- **dict.fromkeys(['October', 'November', 'December'], 'Quarter 4')
+ return {
+ "compliance_category": compliance_subcategory.compliance_category,
+ "name": compliance_subcategory.name,
+ "project_template": compliance_subcategory.project_template,
}
- naming_quarter = month_to_quarter[naming_month]
-
- # Build naming string
- if repeat_on == "Yearly":
- return str(naming_year)
- elif repeat_on == "Quarterly":
- return f"{naming_year} {naming_quarter}"
- else:
- return f"{naming_year} {naming_month}"
-
-
-def _create_project(sales_order_doc, compliance_sub_category, project_template_doc,
- start_date, expected_end_date, priority, remark, custom_instructions):
- """Create and save project document"""
- naming = _get_naming_info(start_date, compliance_sub_category)
-
- project = frappe.new_doc('Project')
- project.company = sales_order_doc.company
- project.cost_center = frappe.get_cached_value("Company", sales_order_doc.company, "cost_center")
-
- # Set project name
- add_compliance_category = frappe.db.get_single_value(
- 'Compliance Settings',
- 'add_compliance_category_in_project_name'
- )
-
- if sales_order_doc.custom_project_name_automatically:
- category_name = compliance_sub_category.name if add_compliance_category else compliance_sub_category.sub_category
- project.project_name = f"{sales_order_doc.customer_name or ' '}-{category_name}-{naming}"
- else:
- project.project_name = f"{sales_order_doc.custom_project_name or ' '}-{sales_order_doc.customer_name}-{compliance_sub_category.name}-{naming}"
-
- # Set project fields
- project.customer = sales_order_doc.customer
- project.compliance_sub_category = compliance_sub_category.name
- project.compliance_category = compliance_sub_category.compliance_category
- project.expected_start_date = start_date
- project.priority = priority
- project.custom_project_service = f"{compliance_sub_category.name}-{naming}"
- project.sales_order = sales_order_doc.name
- project.category_type = compliance_sub_category.category_type
- project.department = compliance_sub_category.department
- project.is_premium = 1 if (sales_order_doc.is_premium_project and project_template_doc.has_premium_tasks) else 0
-
- if custom_instructions:
- project.custom_instructions = custom_instructions
- if remark:
- project.notes = remark
-
- # Calculate end date
- if expected_end_date:
- days_diff = date_diff(getdate(expected_end_date), getdate(start_date))
- project.expected_end_date = expected_end_date if days_diff > project_template_doc.custom_project_duration else add_days(start_date, project_template_doc.custom_project_duration)
- elif project_template_doc.custom_project_duration:
- project.expected_end_date = add_days(start_date, project_template_doc.custom_project_duration)
-
- project.save(ignore_permissions=True)
- return project
-
-
-def _assign_to_head_of_department(reference_name, head_of_department, reference_type):
- """Create ToDo for head of department"""
- create_todo(reference_type, reference_name, head_of_department, frappe.session.user, f"{reference_type} assigned to {head_of_department}")
- frappe.msgprint(f"{reference_type} is assigned to {head_of_department}", alert=1)
-
-
-def _assign_to_employees(employees, reference_name, head_of_department, reference_type):
- """Assign reference to multiple employees"""
- for employee in employees:
- user = frappe.db.get_value('Employee', employee, 'user_id')
- if user and user != head_of_department:
- message = f'{reference_type} {reference_name} Assigned Successfully'
- create_todo(reference_type, reference_name, user, frappe.session.user, message)
-
-
-def _create_tasks_from_template(project, project_template_doc, compliance_sub_category,
- start_date, employees, head_of_department):
- """Create tasks from project template"""
- for template_task in reversed(project_template_doc.tasks):
- task_doc = _create_task_doc(
- project,
- template_task,
- compliance_sub_category,
- project_template_doc,
- start_date,
- is_premium=False
- )
-
- _assign_task(task_doc, template_task, employees, head_of_department, compliance_sub_category)
-
-
-def _create_task_doc(project, template_task, compliance_sub_category, project_template_doc,
- start_date, is_premium=False):
- """Create a single task document from template task (regular or premium)"""
- template_task_doc = frappe.get_doc('Task', template_task.task)
-
- task_doc = frappe.new_doc('Task')
- task_doc.compliance_sub_category = compliance_sub_category.name
- task_doc.subject = template_task.subject
- task_doc.project = project.name
- task_doc.company = project.company
- task_doc.project_name = project.project_name
- task_doc.category_type = project.category_type
- task_doc.exp_start_date = start_date
- task_doc.custom_serial_number = template_task.idx
- task_doc.department = compliance_sub_category.department
- task_doc.task_weightage = template_task.task_weightage or 0
- task_doc.is_premium_task = 1 if is_premium else 0
-
- if template_task_doc.expected_time:
- task_doc.expected_time = template_task_doc.expected_time
-
- if template_task.custom_task_duration:
- task_doc.duration = template_task.custom_task_duration
- task_doc.exp_end_date = add_days(start_date, template_task.custom_task_duration)
-
- if template_task.has_external_dependencies:
- task_doc.has_external_dependencies = 1
- task_doc.send_email_notification_for_lag_time = template_task.send_email_notification_for_lag_time
-
- if template_task.has_reimbursement:
- task_doc.has_reimbursement = template_task.has_reimbursement
-
- # Add dependencies
- if template_task_doc.depends_on:
- for depends_task in template_task_doc.depends_on:
- dependent_task = frappe.get_doc('Task', {'project': project.name, 'subject': depends_task.subject}, 'name')
- task_doc.append("depends_on", {"task": dependent_task.name})
-
- # Add documents
- if template_task.custom_has_document:
- _add_task_documents(task_doc, template_task, project_template_doc)
-
- task_doc.save(ignore_permissions=True)
- return task_doc
-
-def _add_task_documents(task_doc, template_task, project_template_doc):
- """Add required documents to task"""
- for documents in project_template_doc.custom_documents_required:
- if documents.task == template_task.task:
- for doc in documents.documents.split(', '):
- task_doc.append("custom_task_document_items", {"document": doc})
-
-def _assign_task(task_doc, template_task, employees, head_of_department, compliance_sub_category):
- """Assign task to appropriate users"""
- # Assign to head of department
- if compliance_sub_category.head_of_department:
- create_todo('Task', task_doc.name, head_of_department, frappe.session.user,
- f"Task Assign to {head_of_department}")
-
- # Assign to specific employees if provided
- if employees:
- _assign_to_employees(employees, task_doc.name, head_of_department, 'Task')
-
- # Assign based on template settings
- elif template_task.type and template_task.employee_or_group:
- frappe.db.set_value('Task', task_doc.name, 'assigned_to', template_task.employee_or_group)
-
- if template_task.type == "Employee":
- employee_user = frappe.db.get_value('Employee', template_task.employee_or_group, 'user_id')
- if employee_user and employee_user != head_of_department:
- create_todo('Task', task_doc.name, employee_user, frappe.session.user,
- f'Task {task_doc.name} Assigned Successfully')
-
- elif template_task.type == "Employee Group":
- employee_group = frappe.get_doc('Employee Group', template_task.employee_or_group)
- if employee_group.employee_list:
- for employee in employee_group.employee_list:
- create_todo('Task', task_doc.name, employee.user_id, frappe.session.user,
- f'Task {task_doc.name} Assigned Successfully')
-
-
-def _create_premium_tasks(project, project_template_doc, compliance_sub_category,
- start_date, employees, head_of_department):
- """Create premium tasks from project template"""
- for premium_task in project_template_doc.premium_tasks:
- # Skip if task already exists
- if frappe.db.exists("Task", {"project": project.name, "subject": premium_task.subject}):
- continue
-
- task_doc = _create_task_doc(
- project,
- premium_task,
- compliance_sub_category,
- project_template_doc,
- start_date,
- is_premium=True
- )
-
- _assign_task(task_doc, premium_task, employees, head_of_department, compliance_sub_category)
+
+
+# SALES ORDER FROM EVENT
@frappe.whitelist()
-def create_sales_order_from_event(event, customer=None, sub_category=None, rate=None, description=None, company=None):
+def create_sales_order_from_event(
+ event,
+ customer=None,
+ sub_category=None,
+ rate=None,
+ description=None,
+ company=None,
+):
missing_fields = []
+
if not customer:
missing_fields.append("Customer")
if not sub_category:
missing_fields.append("Service")
if not description:
missing_fields.append("Service Description")
- if missing_fields:
- if len(missing_fields) > 1:
- missing_fields_str = ', '.join(missing_fields[:-1]) + ' and ' + missing_fields[-1]
- else:
- missing_fields_str = missing_fields[0]
- frappe.throw(f"Required Field: {missing_fields_str}.")
+ if missing_fields:
+ missing_fields_str = (
+ ", ".join(missing_fields[:-1])
+ + " and "
+ + missing_fields[-1]
+ if len(missing_fields) > 1
+ else missing_fields[0]
+ )
+ frappe.throw(
+ f"Required Field: {missing_fields_str}."
+ )
sales_orders = frappe.get_all(
"Sales Order",
- filters={"customer": customer, "docstatus": 1},
- fields=["name"]
+ filters={
+ "customer": customer,
+ "docstatus": 1,
+ },
+ fields=["name"],
)
+
for sales_order in sales_orders:
items = frappe.get_all(
"Sales Order Item",
- filters={"parent": sales_order.name, "description": description},
- fields=["name"]
+ filters={
+ "parent": sales_order.name,
+ "description": description,
+ },
+ fields=["name"],
)
+
if items:
- frappe.throw("Proforma Invoice is already created for this Event.")
+ frappe.throw(
+ "Proforma Invoice is already created for this Event."
+ )
+
event_doc = frappe.get_doc("Event", event)
- custom_service = event_doc.custom_service or sub_category
- custom_customer = event_doc.custom_customer or customer
- sub_category_doc = frappe.get_doc("Compliance Sub Category", sub_category)
+
+ custom_service = (
+ event_doc.custom_service or sub_category
+ )
+ custom_customer = (
+ event_doc.custom_customer or customer
+ )
+
+ sub_category_doc = frappe.get_doc(
+ "Compliance Sub Category",
+ sub_category,
+ )
+
new_sales_order = frappe.new_doc("Sales Order")
new_sales_order.customer = customer
new_sales_order.event = event
new_sales_order.posting_date = frappe.utils.today()
new_sales_order.delivery_date = frappe.utils.today()
- new_sales_order.append('items', {
- 'item_code': sub_category_doc.item_code,
- 'item_name': sub_category_doc.sub_category,
- 'custom_compliance_category': sub_category_doc.compliance_category,
- 'custom_compliance_subcategory': sub_category_doc.name,
- 'rate': rate,
- 'qty': 1,
- 'description': description
- })
+
+ new_sales_order.append(
+ "items",
+ {
+ "item_code": sub_category_doc.item_code,
+ "item_name": sub_category_doc.sub_category,
+ "custom_compliance_category": sub_category_doc.compliance_category,
+ "custom_compliance_subcategory": sub_category_doc.name,
+ "rate": rate,
+ "qty": 1,
+ "description": description,
+ },
+ )
+
new_sales_order.company = company
new_sales_order.insert(ignore_permissions=True)
new_sales_order.submit()
- frappe.db.set_value("Sales Order", new_sales_order.name, "status", "Proforma Invoice")
- frappe.db.set_value("Sales Order", new_sales_order.name, "workflow_state", "Proforma Invoice")
- frappe.db.set_value("Sales Order", sales_order, "invoice_generation_date", today())
- frappe.msgprint(f"Proforma Invoice {new_sales_order.name} Created against {event}", alert=True)
- accounts_users = get_users_with_role("Accounts User")
- add_assign({
- "assign_to": accounts_users,
- "doctype": "Sales Order",
- "name": new_sales_order.name,
- "description": f"{custom_service} for {custom_customer} is Completed, Please Proceed with the Invoice"
- })
+ frappe.db.set_value(
+ "Sales Order",
+ new_sales_order.name,
+ "status",
+ "Proforma Invoice",
+ )
+ frappe.db.set_value(
+ "Sales Order",
+ new_sales_order.name,
+ "workflow_state",
+ "Proforma Invoice",
+ )
+
+ frappe.msgprint(
+ f"Proforma Invoice {new_sales_order.name} Created against {event}",
+ alert=True,
+ )
+
+ accounts_users = get_users_with_role(
+ "Accounts User"
+ )
+
+ add_assign(
+ {
+ "assign_to": accounts_users,
+ "doctype": "Sales Order",
+ "name": new_sales_order.name,
+ "description": (
+ f"{custom_service} for "
+ f"{custom_customer} is Completed, "
+ "Please Proceed with the Invoice"
+ ),
+ }
+ )
+
+# SALES ORDER LIFECYCLE HOOKS
def so_on_cancel_custom(doc, method=None):
- """Set workflow state to Cancelled when cancelling sales order"""
+ """Set workflow state to Cancelled"""
doc.db_set("workflow_state", "Cancelled")
if doc.project:
frappe.db.set_value("Project", doc.project, "status", "Cancelled")
+ if doc.project:
+ frappe.db.set_value(
+ "Project",
+ doc.project,
+ "status",
+ "Cancelled",
+ )
+
+
def so_on_update_after_submit(doc, method):
- '''
- Method trigger on so_on_update_after_submit of Sales Order
- '''
update_journal_entry(doc)
set_total_reimbursement_amount(doc)
doc.reload()
+
def set_total_reimbursement_amount(doc):
- '''
- Method to set total_reimbursement_amount
- '''
total_reimbursement_amount = 0
+
for row in doc.custom_reimbursement_details:
total_reimbursement_amount += row.amount
- doc.custom_total_reimbursement_amount = total_reimbursement_amount
- frappe.db.set_value('Sales Order', doc.name, 'custom_total_reimbursement_amount', total_reimbursement_amount)
+
+ doc.custom_total_reimbursement_amount = (
+ total_reimbursement_amount
+ )
+
+ frappe.db.set_value(
+ "Sales Order",
+ doc.name,
+ "custom_total_reimbursement_amount",
+ total_reimbursement_amount,
+ )
+
+
+# DELETE LINKED RECORDS
@frappe.whitelist()
def delete_linked_records(sales_order):
"""
- Deletes all records linked with the specified sales order.
-
- Args:
- project (str): The name of the project to delete linked records for.
+ Deletes all records linked with the specified Sales Order.
"""
sales_invoice = frappe.db.get_value(
- "Sales Invoice Item", {"sales_order": sales_order}, "parent"
+ "Sales Invoice Item",
+ {"sales_order": sales_order},
+ "parent",
)
+
if frappe.db.exists("Sales Invoice", sales_invoice):
- frappe.throw("Cannot proceed with this operation as it is invoiced")
+ frappe.throw(
+ "Cannot proceed with this operation as it is invoiced"
+ )
- project = frappe.db.get_value("Sales Order", sales_order, "project")
+ project = frappe.db.get_value(
+ "Sales Order",
+ sales_order,
+ "project",
+ )
if frappe.db.exists("Project", project):
- linked_tasks = frappe.get_all("Task", filters={"project": project})
+ linked_tasks = frappe.get_all(
+ "Task",
+ filters={"project": project},
+ )
+
for task in linked_tasks:
- frappe.delete_doc("Task", task["name"], ignore_permissions=True)
+ frappe.delete_doc(
+ "Task",
+ task["name"],
+ ignore_permissions=True,
+ )
- project_doc = frappe.get_doc("Project", project)
+ project_doc = frappe.get_doc(
+ "Project",
+ project,
+ )
project_doc.sales_order = ""
project_doc.save(ignore_permissions=True)
- frappe.delete_doc("Project", project, ignore_permissions=True)
+
+ frappe.delete_doc(
+ "Project",
+ project,
+ ignore_permissions=True,
+ )
doc = frappe.get_doc("Sales Order", sales_order)
doc.flags.ignore_permissions = True
doc.cancel()
- frappe.delete_doc("Sales Order", sales_order, ignore_permissions=True)
+
+ frappe.delete_doc(
+ "Sales Order",
+ sales_order,
+ ignore_permissions=True,
+ )
return "success"
+# OPPORTUNITY CREATION
+
@frappe.whitelist()
def create_opportunity():
"""
- Creates Opportunities for Sales Orders flagged for follow_up_for_next_project
- and assigns a Task to follow_up_person from Compliance Sub Category
+ Creates Opportunities for Sales Orders
+ flagged for follow_up_for_next_project
"""
today_date = getdate(today())
this_year = today_date.year
@@ -477,179 +380,310 @@ def create_opportunity():
sales_orders = frappe.db.get_all(
"Sales Order",
- filters={"follow_up_for_next_project": 1, "follow_up_completed": 0},
- fields=["name", "customer", "status", "workflow_state", "company", "follow_up_completed"]
+ filters={
+ "follow_up_for_next_project": 1,
+ "follow_up_completed": 0,
+ },
+ fields=[
+ "name",
+ "customer",
+ "status",
+ "workflow_state",
+ "company",
+ "follow_up_completed",
+ ],
)
for so in sales_orders:
sales_order_items = frappe.get_all(
"Sales Order Item",
filters={"parent": so.name},
- fields=[
- "item_code", "item_name", "uom", "qty",
- "brand", "item_group", "description",
- "image", "base_rate", "base_amount", "rate", "amount",
- "custom_compliance_subcategory as compliance_sub_category",
- "custom_compliance_category as compliance_category"
- ]
+ fields=["*"],
)
for item in sales_order_items:
- subcat_name = item.compliance_sub_category
+ subcat_name = (
+ item.custom_compliance_subcategory
+ )
if not subcat_name:
continue
- compliance = frappe.get_doc("Compliance Sub Category", subcat_name)
+ compliance = frappe.get_doc(
+ "Compliance Sub Category",
+ subcat_name,
+ )
- if not (compliance.allow_repeat and compliance.renew_notif):
+ if not (
+ compliance.allow_repeat
+ and compliance.renew_notif
+ ):
continue
day = int(compliance.day or 1)
- notif_days = int(float(compliance.renew_notif_days_before or 0))
+ notif_days = int(
+ float(
+ compliance.renew_notif_days_before
+ or 0
+ )
+ )
repeat_on = compliance.repeat_on
scheduled_date = None
try:
- # ---------------- Monthly ----------------
if repeat_on == "Monthly":
- last_day_of_month = (datetime(this_year, this_month + 1, 1) - timedelta(days=1)).day \
- if this_month < 12 else 31
-
- if day == 1 and today_date.day == last_day_of_month:
- next_month = this_month + 1 if this_month < 12 else 1
- next_year = this_year if this_month < 12 else this_year + 1
- scheduled_date = datetime(next_year, next_month, 1).date()
+ last_day_of_month = (
+ (
+ datetime(
+ this_year,
+ this_month + 1,
+ 1,
+ )
+ - timedelta(days=1)
+ ).day
+ if this_month < 12
+ else 31
+ )
+
+ if (
+ day == 1
+ and today_date.day
+ == last_day_of_month
+ ):
+ next_month = (
+ this_month + 1
+ if this_month < 12
+ else 1
+ )
+ next_year = (
+ this_year
+ if this_month < 12
+ else this_year + 1
+ )
+ scheduled_date = datetime(
+ next_year,
+ next_month,
+ 1,
+ ).date()
else:
- scheduled_date = datetime(this_year, this_month, day).date()
+ scheduled_date = datetime(
+ this_year,
+ this_month,
+ day,
+ ).date()
- # ---------------- Quarterly ----------------
elif repeat_on == "Quarterly":
for m in [1, 4, 7, 10]:
- d = datetime(this_year, m, day).date()
- if d >= today_date:
- scheduled_date = d
- break
+ try:
+ d = datetime(
+ this_year,
+ m,
+ day,
+ ).date()
+ if d >= today_date:
+ scheduled_date = d
+ break
+ except ValueError:
+ continue
+
if not scheduled_date:
- scheduled_date = datetime(this_year + 1, 1, day).date()
+ scheduled_date = datetime(
+ this_year + 1,
+ 1,
+ day,
+ ).date()
- # ---------------- Half Yearly ----------------
elif repeat_on == "Half Yearly":
for m in [1, 7]:
- d = datetime(this_year, m, day).date()
- if d >= today_date:
- scheduled_date = d
- break
+ try:
+ d = datetime(
+ this_year,
+ m,
+ day,
+ ).date()
+ if d >= today_date:
+ scheduled_date = d
+ break
+ except ValueError:
+ continue
+
if not scheduled_date:
- scheduled_date = datetime(this_year + 1, 1, day).date()
+ scheduled_date = datetime(
+ this_year + 1,
+ 1,
+ day,
+ ).date()
- # ---------------- Yearly ----------------
elif repeat_on == "Yearly":
if compliance.month:
- m = datetime.strptime(compliance.month, "%B").month
- d = datetime(this_year, m, day).date()
- if d >= today_date:
- scheduled_date = d
- else:
- scheduled_date = datetime(this_year + 1, m, day).date()
+ m = datetime.strptime(
+ compliance.month,
+ "%B",
+ ).month
+ try:
+ d = datetime(
+ this_year,
+ m,
+ day,
+ ).date()
+ if d >= today_date:
+ scheduled_date = d
+ else:
+ scheduled_date = datetime(
+ this_year + 1,
+ m,
+ day,
+ ).date()
+ except ValueError:
+ continue
except Exception as e:
- frappe.log_error(f"Invalid date calculation for {subcat_name}: {e}", "Create Opportunity Error")
+ frappe.log_error(
+ f"Invalid date calculation for {subcat_name}: {e}",
+ "Create Opportunity Error",
+ )
continue
if not scheduled_date:
continue
- # --- Notification trigger date ---
if day == 1 and notif_days > 0:
- prev_month = scheduled_date.month - 1 if scheduled_date.month > 1 else 12
- prev_year = scheduled_date.year if scheduled_date.month > 1 else scheduled_date.year - 1
- last_day_prev_month = (datetime(prev_year, prev_month + 1, 1) - timedelta(days=1)).date()
- notif_trigger_date = last_day_prev_month
+ prev_month = (
+ scheduled_date.month - 1
+ if scheduled_date.month > 1
+ else 12
+ )
+ prev_year = (
+ scheduled_date.year
+ if scheduled_date.month > 1
+ else scheduled_date.year - 1
+ )
+ last_day_prev_month = (
+ datetime(
+ prev_year,
+ prev_month + 1,
+ 1,
+ )
+ - timedelta(days=1)
+ ).date()
+ notif_trigger_date = (
+ last_day_prev_month
+ )
else:
- notif_trigger_date = add_days(scheduled_date, -notif_days)
+ notif_trigger_date = add_days(
+ scheduled_date,
+ -notif_days,
+ )
if notif_trigger_date != today_date:
continue
- # Skip if Opportunity already exists
- existing_opportunity = frappe.db.exists("Opportunity", {"sales_order": so.name})
+ existing_opportunity = frappe.db.exists(
+ "Opportunity",
+ {"sales_order": so.name},
+ )
if existing_opportunity:
continue
try:
- # Only create for active sales orders
- if so.status not in ["Draft", "Closed", "Cancelled"] or so.workflow_state not in ["Pending", "Cancelled"]:
- opportunity = frappe.new_doc("Opportunity")
- opportunity.opportunity_from = "Customer"
- opportunity.party_name = so.customer
+ if (
+ so.status
+ not in ["Draft", "Closed", "Cancelled"]
+ or so.workflow_state
+ not in ["Pending", "Cancelled"]
+ ):
+ opportunity = frappe.new_doc(
+ "Opportunity"
+ )
+ opportunity.opportunity_from = (
+ "Customer"
+ )
+ opportunity.party_name = (
+ so.customer
+ )
opportunity.status = "Open"
opportunity.opportunity_type = "Sales"
opportunity.sales_order = so.name
- opportunity.naming_series = "CRM-OPP-.YYYY.-"
+ opportunity.naming_series = (
+ "CRM-OPP-.YYYY.-"
+ )
opportunity.company = so.company
- opportunity.opportunity_date = today_date
+ opportunity.opportunity_date = (
+ today_date
+ )
for soi in sales_order_items:
- opp_item = opportunity.append("items", {})
- opp_item.item_code = soi.item_code
- opp_item.item_name = soi.item_name
- opp_item.uom = soi.uom
+ opp_item = opportunity.append(
+ "items",
+ {},
+ )
+ opp_item.item_code = (
+ soi.item_code
+ )
+ opp_item.item_name = (
+ soi.item_name
+ )
opp_item.qty = soi.qty
- opp_item.brand = soi.brand
- opp_item.item_group = soi.item_group
- opp_item.description = soi.description
- opp_item.image = soi.image
- opp_item.base_rate = soi.base_rate
- opp_item.base_amount = soi.base_amount
opp_item.rate = soi.rate
opp_item.amount = soi.amount
- opp_item.compliance_category = soi.compliance_category
- opp_item.compliance_sub_category = soi.compliance_sub_category
-
- opportunity.insert(ignore_permissions=True)
- frappe.db.commit()
-
- frappe.db.set_value("Sales Order", so.name, "follow_up_completed", 1)
- frappe.db.commit()
-
- follow_up_user = frappe.db.get_value("Employee", compliance.follow_up_person, "user_id")
-
- try:
- if follow_up_user:
- todo = frappe.new_doc("ToDo")
- todo.owner = follow_up_user
- todo.assigned_by = frappe.session.user
- todo.allocated_to = follow_up_user
- todo.reference_type = "Opportunity"
- todo.reference_name = opportunity.name
- todo.description = f"Follow up for compliance sub category: {subcat_name}"
- todo.status = "Open"
- todo.priority = "Medium"
- todo.date = today()
- todo.insert(ignore_permissions=True)
- frappe.db.commit()
-
- except Exception as e:
- frappe.log_error(f"Failed to create ToDo for {subcat_name}: {e}", "Create Opportunity Error")
+ opp_item.compliance_category = (
+ soi.custom_compliance_category
+ )
+ opp_item.compliance_sub_category = (
+ soi.custom_compliance_subcategory
+ )
+
+ opportunity.insert(
+ ignore_permissions=True
+ )
+
+ frappe.db.set_value(
+ "Sales Order",
+ so.name,
+ "follow_up_completed",
+ 1,
+ )
+
+ follow_up_user = frappe.db.get_value(
+ "Employee",
+ compliance.follow_up_person,
+ "user_id",
+ )
+
+ if follow_up_user:
+ create_todo(
+ "Opportunity",
+ opportunity.name,
+ follow_up_user,
+ frappe.session.user,
+ f"Follow up for compliance sub category: {subcat_name}",
+ )
except Exception as e:
- frappe.log_error(f"Failed to create Opportunity for {subcat_name}: {e}", "Create Opportunity Error")
+ frappe.log_error(
+ f"Failed to create Opportunity for {subcat_name}: {e}",
+ "Create Opportunity Error",
+ )
+# SET COMPLIANCE FIELDS
def set_compliance_fields(doc, method):
- """
- For each item , this function fetches the related compliance category
- and subcategory
- """
for item in doc.items:
if item.item_code:
- subcat = frappe.db.get_value(
+ subcat = frappe.db.get_value(
"Compliance Sub Category",
- {"item_code": item.item_code},
- ["compliance_category", "name"],
- as_dict=True
+ {"item_code": item.item_code},
+ [
+ "compliance_category",
+ "name",
+ ],
+ as_dict=True,
)
+
if subcat:
- item.custom_compliance_category = subcat.compliance_category
- item.custom_compliance_subcategory = subcat.name
\ No newline at end of file
+ item.custom_compliance_category = (
+ subcat.compliance_category
+ )
+ item.custom_compliance_subcategory = (
+ subcat.name
+ )
diff --git a/one_compliance/one_compliance/doctype/compliance_agreement/compliance_agreement.py b/one_compliance/one_compliance/doctype/compliance_agreement/compliance_agreement.py
index 942698ec..28c62833 100644
--- a/one_compliance/one_compliance/doctype/compliance_agreement/compliance_agreement.py
+++ b/one_compliance/one_compliance/doctype/compliance_agreement/compliance_agreement.py
@@ -3,7 +3,18 @@
from frappe.utils import add_days, add_months, get_last_day, getdate, nowdate, today
from frappe.utils.data import cint
-from one_compliance.one_compliance.utils import create_todo
+from one_compliance.one_compliance.utils import create_todo, create_compliance_project
+
+
+MONTH_MAP = {
+ "January": 1, "February": 2, "March": 3, "April": 4,
+ "May": 5, "June": 6, "July": 7, "August": 8,
+ "September": 9, "October": 10, "November": 11, "December": 12
+}
+
+
+def get_month_name(num):
+ return next(name for name, number in MONTH_MAP.items() if number == num)
MONTH_MAP = {
"January": 1, "February": 2, "March": 3, "April": 4,
@@ -18,12 +29,6 @@ class ComplianceAgreement(Document):
def before_insert(self):
self.status = "Open"
- def sign_validation(self):
- if self.workflow_state == 'Approved' and not self.authority_signature:
- frappe.throw('Authority Signature is required for Approval')
- if self.workflow_state == 'Customer Approved' and not self.customer_signature:
- frappe.throw('Customer Signature is required for Customer Approval')
-
def validate(self):
self.validate_agreement_dates()
self.validate_date_range()
@@ -101,17 +106,131 @@ def validate_compliance_dates_on_table(self):
)
def on_trash(self):
- delete_project_along_with_compliance_agreement = frappe.db.get_single_value('Compliance Settings', 'delete_project_along_with_compliance_agreement')
+ delete_project_along_with_compliance_agreement = frappe.db.get_single_value(
+ 'Compliance Settings',
+ 'delete_project_along_with_compliance_agreement'
+ )
if delete_project_along_with_compliance_agreement:
delete_project_and_task(self.name)
else:
- if frappe.db.exists('Project',{'compliance_agreement': self.name}):
+ if frappe.db.exists('Project', {'compliance_agreement': self.name}):
project_list = frappe.db.get_all('Project', filters={'compliance_agreement': self.name})
for project in project_list:
- project_doc = frappe.get_doc('Project',project.name)
+ project_doc = frappe.get_doc('Project', project.name)
project_doc.set('compliance_agreement', None)
project_doc.save()
+ def sign_validation(self):
+ if self.workflow_state == 'Approved' and not self.authority_signature:
+ frappe.throw('Authority Signature is required for Approval')
+ if self.workflow_state == 'Customer Approved' and not self.customer_signature:
+ frappe.throw('Customer Signature is required for Customer Approval')
+
+ def validate_agreement_dates(self):
+ if self.posting_date and getdate(self.posting_date) > getdate(today()):
+ frappe.throw('Posting Date cannot be a future date.')
+ if self.valid_from and self.valid_upto and not self.has_long_term_validity:
+ if getdate(self.valid_from) > getdate(self.valid_upto):
+ frappe.throw('From Date cannot be greater than Valid Upto Date')
+
+ def validate_date_range(self):
+ instance_valid_from = getdate(self.valid_from)
+ instance_valid_upto = getdate(self.valid_upto) if self.valid_upto else None
+ instance_categories = {d.sub_category_name for d in self.compliance_category_details}
+ if not instance_categories:
+ return
+
+ existing_agreements = frappe.get_all(
+ "Compliance Agreement",
+ filters={
+ "customer": self.customer,
+ "workflow_state": "Customer Approved",
+ "name": ["!=", self.name]
+ },
+ fields=["name", "valid_from", "valid_upto", "has_long_term_validity"]
+ )
+
+ for agreement_data in existing_agreements:
+ agreement_categories = frappe.get_all(
+ "Compliance Category Details",
+ filters={"parent": agreement_data.name},
+ fields=["sub_category_name"],
+ pluck="sub_category_name"
+ )
+ overlapping_categories = instance_categories.intersection(set(agreement_categories))
+
+ if not overlapping_categories:
+ continue
+
+ if self._check_date_overlap(
+ instance_valid_from,
+ instance_valid_upto,
+ self.has_long_term_validity,
+ getdate(agreement_data.valid_from),
+ getdate(agreement_data.valid_upto) if agreement_data.valid_upto else None,
+ agreement_data.has_long_term_validity
+ ):
+ categories_list = ", ".join(sorted(overlapping_categories))
+ frappe.throw(
+ f"The compliance subcategories ({categories_list}) already exist in "
+ f"Agreement '{agreement_data.name}' with overlapping date ranges."
+ )
+
+ def _check_date_overlap(self, start1, end1, long_term1, start2, end2, long_term2):
+ if long_term1:
+ end1 = None
+ if long_term2:
+ end2 = None
+ if end1 is None and end2 is None:
+ return True
+ if end1 is None:
+ return start1 <= end2
+ if end2 is None:
+ return start2 <= end1
+ return start1 <= end2 and start2 <= end1
+
+ def validate_compliance_dates_on_table(self):
+ for row in self.compliance_category_details:
+ if row.compliance_date and row.compliance_sub_category:
+ compliance_date = getdate(row.compliance_date)
+ sub = frappe.get_doc("Compliance Sub Category", row.compliance_sub_category)
+ day = cint(sub.day)
+
+ # Day Check
+ if compliance_date.day != day:
+ frappe.throw(
+ title='Invalid Compliance Date',
+ msg=f'Compliance Date must be on day `{day}` on Row #{row.idx}.'
+ )
+
+ # Validate Month logic
+ if sub.repeat_on == "Yearly" and sub.month:
+ if compliance_date.month != MONTH_MAP.get(sub.month):
+ frappe.throw(
+ title='Invalid Compliance Date',
+ msg=f'Compliance Date must be on month `{sub.month}` on Row #{row.idx}.'
+ )
+
+ elif sub.repeat_on == "Quarterly" and sub.month:
+ start_month = MONTH_MAP.get(sub.month)
+ valid_months = [(start_month + i*3 - 1) % 12 + 1 for i in range(4)]
+ if compliance_date.month not in valid_months:
+ allowed = ", ".join([get_month_name(m) for m in valid_months])
+ frappe.throw(
+ title='Invalid Compliance Date',
+ msg=f'Compliance Date must be on month of `{allowed}` on Row #{row.idx}.'
+ )
+
+ elif sub.repeat_on == "Half Yearly" and sub.month:
+ start_month = MONTH_MAP.get(sub.month)
+ valid_months = [start_month, ((start_month + 5) % 12) + 1]
+ if compliance_date.month not in valid_months:
+ allowed = ", ".join([get_month_name(m) for m in valid_months])
+ frappe.throw(
+ title='Invalid Compliance Date',
+ msg=f'Compliance Date must be on month of `{allowed}` on Row #{row.idx}.'
+ )
+
def set_compliance_date(self):
'''
Set compliance_date and next_compliance_date for each compliance sub category detail.
@@ -147,124 +266,104 @@ def set_compliance_date(self):
date = add_months(date, step)
next_date = add_months(date, step)
- # Check if values actually changed
+
if not row.compliance_date:
row.db_set("compliance_date", date)
if not row.next_compliance_date:
row.db_set("next_compliance_date", next_date)
continue
else:
+ # One-Time Logic
if self.status != "Active":
continue
if valid_from > today_date:
continue
- else:
- project_date = valid_from
+
+ project_date = valid_from
+ if sub.project_template and not frappe.db.exists(
+ "Project",
+ {
+ "compliance_agreement": self.name,
+ "compliance_sub_category": sub.name
+ }
+ ):
+ create_compliance_project({
+ "compliance_sub_category": sub.name,
+ "customer": self.customer,
+ "company": self.company,
+ "start_date": project_date,
+ "compliance_agreement": self.name,
+ "compliance_category": sub.compliance_category or "",
+ "priority": "Medium"
+ })
- if sub.project_template and not frappe.db.exists("Project", {
+ project_name = frappe.db.get_value(
+ "Project",
+ {
"compliance_agreement": self.name,
"compliance_sub_category": sub.name
- }):
- create_project_from_template(
- sales_order=None,
- project_template=sub.project_template,
- customer=self.customer,
- company=self.company,
- compliance_sub_category=sub.name,
- compliance_category_details_id=row.name,
- compliance_agreement=self.name,
- compliance_category=sub.compliance_category or "",
- compliance_date=project_date
- )
+ },
+ "name"
+ )
- project_name = frappe.db.get_value(
- "Project",
+ if sub.is_billable:
+ exists = frappe.db.exists(
+ "Sales Order",
{
"compliance_agreement": self.name,
- "compliance_sub_category": sub.name
- },
- "name"
+ "compliance_sub_category": sub.name,
+ "transaction_date": project_date
+ }
)
- if sub.is_billable:
- exists = frappe.db.exists(
- "Sales Order",
- {
- "compliance_agreement": self.name,
- "compliance_sub_category": sub.name,
- "transaction_date": project_date
- }
- )
-
- if not exists:
- so = frappe.new_doc("Sales Order")
- so.customer = self.customer
- so.company = self.company
- so.compliance_agreement = self.name
- so.compliance_sub_category = sub.name
- so.transaction_date = today_date
- so.delivery_date = today_date
-
- if self.default_payment_terms_template:
- so.payment_terms_template = self.default_payment_terms_template
-
- item_code = sub.item_code
- item_name = frappe.db.get_value("Item", item_code, "item_name")
- rate = sub.rate or 0
-
- so.append("items", {
- "item_code": item_code,
- "item_name": item_name,
- "qty": 1,
- "rate": rate
- })
-
- so.insert(ignore_permissions=True)
- so.submit()
-
- if project_name:
- frappe.db.set_value("Project", project_name, "sales_order", so.name)
- frappe.db.set_value("Sales Order", so.name, "project", project_name)
+ if not exists:
+ so = frappe.new_doc("Sales Order")
+ so.customer = self.customer
+ so.company = self.company
+ so.compliance_agreement = self.name
+ so.compliance_sub_category = sub.name
+ so.transaction_date = today_date
+ so.delivery_date = today_date
+ if self.default_payment_terms_template:
+ so.payment_terms_template = self.default_payment_terms_template
+
+ item_code = sub.item_code
+ item_name = frappe.db.get_value("Item", item_code, "item_name")
+
+ so.append("items", {
+ "item_code": item_code,
+ "item_name": item_name,
+ "qty": 1,
+ "rate": sub.rate or 0
+ })
+ so.insert(ignore_permissions=True)
+ so.submit()
- def validate_agreement_dates(self):
- if self.posting_date:
- if getdate(self.posting_date) > getdate(today()):
- frappe.throw('Posting Date cannot be a future date.')
- if self.valid_from and self.valid_upto and not self.has_long_term_validity:
- if getdate(self.valid_from) > getdate(self.valid_upto):
- frappe.throw('From Date cannot be greater than Valid Upto Date')
+ if project_name:
+ frappe.db.set_value("Project", project_name, "sales_order", so.name)
+ frappe.db.set_value("Sales Order", so.name, "project", project_name)
def update_compliance_agreement_status(self):
- """
- Update Compliance Agreement status based on:
- - Customer freeze/disable state (highest priority)
- - Workflow state
- - Validity period (valid_from and valid_upto)
- - Cancellation state (docstatus)
- """
-
today = getdate(frappe.utils.today())
-
if self.customer:
customer_status = frappe.db.get_value(
- "Customer", self.customer, ["is_frozen", "disabled"], as_dict=True
+ "Customer",
+ self.customer,
+ ["is_frozen", "disabled"],
+ as_dict=True
)
if customer_status and (customer_status.is_frozen or customer_status.disabled):
self.db_set("status", "Hold")
return
-
if self.docstatus == 2:
self.db_set("status", "Cancelled")
return
-
if self.valid_from and today < getdate(self.valid_from):
self.db_set("status", "Open")
return
-
if self.valid_upto and today > getdate(self.valid_upto) and not self.has_long_term_validity:
self.db_set("status", "Expired")
return
-
if self.workflow_state in ["Customer Approval Waiting", "Pending", "Draft"]:
self.db_set("status", "Draft")
elif self.workflow_state in ["Customer Approved"]:
@@ -272,125 +371,36 @@ def update_compliance_agreement_status(self):
else:
self.db_set("status", "Open")
- def validate_date_range(self):
- """Validate that compliance categories don't overlap with existing approved agreements."""
-
- # Get current instance data
- instance_valid_from = getdate(self.valid_from)
- instance_valid_upto = getdate(self.valid_upto) if self.valid_upto else None
- instance_categories = {d.sub_category_name for d in self.compliance_category_details}
-
- if not instance_categories:
- return
-
- # Fetch existing data
- existing_agreements = frappe.get_all(
- "Compliance Agreement",
- filters={
- "customer": self.customer,
- "workflow_state": "Customer Approved",
- "name": ["!=", self.name],
- },
- fields=["name", "valid_from", "valid_upto", "has_long_term_validity"],
- )
-
- # Get category details for all existing agreements
- for agreement_data in existing_agreements:
- agreement_valid_from = getdate(agreement_data.valid_from)
- agreement_valid_upto = getdate(agreement_data.valid_upto) if agreement_data.valid_upto else None
-
- # Fetch categories for this agreement
- agreement_categories = frappe.get_all(
- "Compliance Category Details",
- filters={"parent": agreement_data.name},
- fields=["sub_category_name"],
- pluck="sub_category_name"
- )
-
- # Check for overlapping categories
- overlapping_categories = instance_categories.intersection(set(agreement_categories))
-
- if not overlapping_categories:
- continue
-
- # Check for date range overlap
- has_overlap = self._check_date_overlap(
- instance_valid_from,
- instance_valid_upto,
- self.has_long_term_validity,
- agreement_valid_from,
- agreement_valid_upto,
- agreement_data.has_long_term_validity
- )
-
- if has_overlap:
- categories_list = ", ".join(sorted(overlapping_categories))
- frappe.throw(
- f"The compliance subcategories ({categories_list}) already exist in "
- f"Agreement '{agreement_data.name}' with overlapping date ranges.
"
- f"Instance: {instance_valid_from} to {instance_valid_upto or 'Indefinite'}
"
- f"Existing: {agreement_valid_from} to {agreement_valid_upto or 'Indefinite'}"
- )
-
- def _check_date_overlap(self, start1, end1, long_term1, start2, end2, long_term2):
- """
- Check if two date ranges overlap.
-
- Two date ranges overlap if: start1 <= end2 AND start2 <= end1
- For long-term (indefinite) agreements, end date is considered as infinite.
- """
-
- # Handle long-term validity (no end date = indefinite)
- if long_term1:
- end1 = None # Indefinite end
- if long_term2:
- end2 = None # Indefinite end
-
- # If either range has no end date, check if the other starts before the first ends
- if end1 is None and end2 is None:
- # Both are indefinite - they overlap if one starts before or on the other
- return True
-
- if end1 is None:
- # Instance is indefinite, agreement has end date
- # Overlap if instance starts on or before agreement ends
- return start1 <= end2
-
- if end2 is None:
- # Agreement is indefinite, instance has end date
- # Overlap if agreement starts on or before instance ends
- return start2 <= end1
-
- # Both have end dates - standard overlap check
- return start1 <= end2 and start2 <= end1
-
def make_sales_invoice(self):
-
projectlist = frappe.get_all(
"Project",
filters={
"compliance_agreement": self.name,
"status": "Completed",
"expected_start_date": (">=", self.invoice_date),
- "expected_end_date": ("<", self.next_invoice_date),
+ "expected_end_date": ("<", self.next_invoice_date)
},
fields=["name", "customer", "compliance_sub_category", "company"]
)
-
- if(projectlist and len(projectlist) > 0):
+ if projectlist:
sales_invoice = frappe.new_doc("Sales Invoice")
for project in projectlist:
sales_invoice.customer = project.customer
sales_invoice.posting_date = frappe.utils.today()
income_account = frappe.db.get_value('Company', project.company, 'default_income_account')
- payment_terms = frappe.db.get_value('Compliance Agreement', project.compliance_agreement, 'default_payment_terms_template')
- rate = get_rate_from_compliance_agreement(project.compliance_agreement, project.compliance_sub_category)
+ payment_terms = frappe.db.get_value(
+ 'Compliance Agreement',
+ project.compliance_agreement,
+ 'default_payment_terms_template'
+ )
sub_category_doc = frappe.get_doc("Compliance Sub Category", project.compliance_sub_category)
- rate = rate if rate else sub_category_doc.rate
+ rate = get_rate_from_compliance_agreement(
+ project.compliance_agreement,
+ project.compliance_sub_category
+ ) or sub_category_doc.rate
if payment_terms:
sales_invoice.default_payment_terms_template = payment_terms
-
sales_invoice.append('items', {
'item_code': sub_category_doc.item_code,
'item_name': sub_category_doc.sub_category,
@@ -402,43 +412,32 @@ def make_sales_invoice(self):
sales_invoice.insert()
frappe.db.set_value(self.doctype, self.name, "invoice_date", self.next_invoice_date)
- next_invoice_date = calculate_next_invoice_date(self.next_invoice_date, self.invoice_generation, self.valid_upto)
+ next_invoice_date = calculate_next_invoice_date(
+ self.next_invoice_date,
+ self.invoice_generation,
+ self.valid_upto
+ )
frappe.db.set_value(self.doctype, self.name, "next_invoice_date", next_invoice_date)
frappe.db.commit()
+
+# Utility Functions
def calculate_next_invoice_date(current_invoice_date, invoice_generation, valid_upto):
if invoice_generation == 'Monthly':
- next_invoice_date = frappe.utils.add_months(current_invoice_date, 1)
+ next_date = frappe.utils.add_months(current_invoice_date, 1)
elif invoice_generation == 'Quarterly':
- next_invoice_date = frappe.utils.add_months(current_invoice_date, 3)
+ next_date = frappe.utils.add_months(current_invoice_date, 3)
elif invoice_generation == 'Half Yearly':
- next_invoice_date = frappe.utils.add_months(current_invoice_date, 6)
+ next_date = frappe.utils.add_months(current_invoice_date, 6)
elif invoice_generation == 'Yearly':
- next_invoice_date = frappe.utils.add_years(current_invoice_date, 1)
+ next_date = frappe.utils.add_years(current_invoice_date, 1)
else:
- next_invoice_date = current_invoice_date
+ next_date = current_invoice_date
- if valid_upto and next_invoice_date <= valid_upto:
- return next_invoice_date
- elif valid_upto and next_invoice_date > valid_upto:
+ if valid_upto and next_date > valid_upto:
return valid_upto
- else:
- return next_invoice_date
+ return next_date
-@frappe.whitelist()
-def check_project_status(compliance_agreement):
- if frappe.db.exists('Project', {'compliance_agreement':compliance_agreement, 'status':'Completed'}):
- return True
-
-def check_exist(target, compliance_category):
- ''' checking if item already exist in child table '''
- exist = False
- if target.items:
- for item in target.items:
- if compliance_category:
- if item.item_name == compliance_category:
- exist = True
- return exist
def calculate_rate(compliance_category_details, compliance_category):
rate = 0
@@ -447,39 +446,50 @@ def calculate_rate(compliance_category_details, compliance_category):
rate += category.rate
return rate
+
+def update_status_on_customer_change(doc, method):
+ agreements = frappe.get_all(
+ "Compliance Agreement",
+ filters={"customer": doc.name, "docstatus": ["!=", 2]},
+ pluck="name"
+ )
+ for agreement_name in agreements:
+ frappe.get_doc("Compliance Agreement", agreement_name).update_compliance_agreement_status()
+
+
+# Whitelisted API Functions
+@frappe.whitelist()
+def check_project_status(compliance_agreement):
+ if frappe.db.exists('Project', {'compliance_agreement': compliance_agreement, 'status': 'Completed'}):
+ return True
+
+
@frappe.whitelist()
def change_agreement_status_scheduler():
- '''Method used for set value to status field '''
- agreements = frappe.db.get_all('Compliance Agreement', filters = {'status': ['!=', 'Hold'], 'docstatus': ['!=', 2]})
- if agreements:
- for agreement in agreements:
- self = frappe.get_doc('Compliance Agreement', agreement.name)
- if self.valid_from:
- today = getdate(frappe.utils.today())
- if today < getdate(self.valid_from):
- frappe.db.set_value('Compliance Agreement', agreement.name, 'status', 'Open')
- elif ((self.valid_upto and today > getdate(self.valid_upto)) and not self.has_long_term_validity):
- frappe.db.set_value('Compliance Agreement', agreement.name, 'status', 'Expired')
- else:
- frappe.db.set_value('Compliance Agreement', agreement.name, 'status', 'Active')
- frappe.db.commit()
- frappe.db.commit()
+ agreements = frappe.db.get_all(
+ 'Compliance Agreement',
+ filters={'status': ['!=', 'Hold'], 'docstatus': ['!=', 2]}
+ )
+ today = getdate(frappe.utils.today())
+ for agreement in agreements:
+ self = frappe.get_doc('Compliance Agreement', agreement.name)
+ if today < getdate(self.valid_from):
+ frappe.db.set_value('Compliance Agreement', agreement.name, 'status', 'Open')
+ elif ((self.valid_upto and today > getdate(self.valid_upto)) and not self.has_long_term_validity):
+ frappe.db.set_value('Compliance Agreement', agreement.name, 'status', 'Expired')
+ else:
+ frappe.db.set_value('Compliance Agreement', agreement.name, 'status', 'Active')
+ frappe.db.commit()
+
@frappe.whitelist()
def get_compliance_sub_category_list(compliance_category):
- '''method used for list sub category'''
- sub_category_list = frappe.db.get_list('Compliance Sub Category', filters = {'compliance_category':compliance_category, 'enabled':1}, fields = ['rate','name','compliance_category', 'sub_category'])
- return sub_category_list
-
-def check_exist_list(self, compliance_sub_category):
- '''method used for checking sub category in corresponding sub category'''
- exist = False
- if self.compliance_category_details:
- for item in self.compliance_category_details:
- if compliance_sub_category:
- if item.compliance_sub_category == compliance_sub_category.name and item.compliance_category == compliance_sub_category.compliance_category:
- exist = True
- return exist
+ return frappe.db.get_list(
+ 'Compliance Sub Category',
+ filters={'compliance_category': compliance_category, 'enabled': 1},
+ fields=['rate', 'name', 'compliance_category', 'sub_category']
+ )
+
@frappe.whitelist()
def set_agreement_status(agreement_id, status):
@@ -490,58 +500,54 @@ def set_agreement_status(agreement_id, status):
frappe.db.commit()
return True
+
@frappe.whitelist()
def delete_project_and_task(agreement_id):
- if frappe.db.exists('Project',{'compliance_agreement': agreement_id}):
+ if frappe.db.exists('Project', {'compliance_agreement': agreement_id}):
project_list = frappe.db.get_all('Project', filters={'compliance_agreement': agreement_id})
for project in project_list:
task_list = frappe.db.get_all('Task', filters={'project': project.name})
for task in task_list:
frappe.db.delete('Task', task.name)
frappe.db.delete('Project', project.name)
- frappe.msgprint('Agreement Deleted {0}.'.format(agreement_id), alert = 1)
+ frappe.msgprint('Agreement Deleted {0}.'.format(agreement_id), alert=1)
+
@frappe.whitelist()
def check_project_exists_or_not(compliance_sub_category, compliance_agreement):
- '''
- Method used for checking project against Compliance Sub Category
- '''
- if frappe.db.exists('Project', {'status': 'Open', 'compliance_agreement':compliance_agreement, 'compliance_sub_category': compliance_sub_category }):
+ if frappe.db.exists(
+ 'Project',
+ {
+ 'status': 'Open',
+ 'compliance_agreement': compliance_agreement,
+ 'compliance_sub_category': compliance_sub_category
+ }
+ ):
return True
return False
+
@frappe.whitelist()
def get_rate_from_compliance_agreement(compliance_agreement, compliance_sub_category):
rate_result = frappe.db.sql(
- """
- select rate
- from `tabCompliance Category Details`
+ """select rate from `tabCompliance Category Details`
where parent=%s and compliance_sub_category=%s""",
(compliance_agreement, compliance_sub_category),
- as_dict=1,
- )
+ as_dict=1
+ )
if rate_result:
return rate_result[0].rate
+
@frappe.whitelist()
def create_sales_orders_from_compliance_agreements(posting_date=today()):
- """
- Create Sales Orders and/or Projects automatically from active Compliance Agreements.
- Sales Order is created only if the Compliance Sub Category is billable.
- Project is created in both cases.
- """
current_date = getdate(posting_date)
agreements = frappe.db.sql("""
- SELECT
- ca.name, ca.customer, ca.company, ca.valid_from, ca.valid_upto,
- ca.default_payment_terms_template
+ SELECT ca.name, ca.customer, ca.company, ca.valid_from, ca.valid_upto, ca.default_payment_terms_template
FROM `tabCompliance Agreement` ca
INNER JOIN `tabCustomer` c ON ca.customer = c.name
- WHERE
- ca.status = 'Active'
- AND IFNULL(c.is_frozen, 0) = 0
- AND IFNULL(c.disabled, 0) = 0
+ WHERE ca.status = 'Active' AND IFNULL(c.is_frozen, 0) = 0 AND IFNULL(c.disabled, 0) = 0
""", as_dict=True)
if not agreements:
@@ -550,8 +556,7 @@ def create_sales_orders_from_compliance_agreements(posting_date=today()):
for agreement in agreements:
valid_from = getdate(agreement.valid_from)
valid_upto = getdate(agreement.valid_upto) if agreement.valid_upto else None
-
- if current_date < valid_from:
+ if current_date < valid_from or (valid_upto and current_date > valid_upto):
continue
category_details = frappe.get_all(
@@ -561,360 +566,155 @@ def create_sales_orders_from_compliance_agreements(posting_date=today()):
)
for detail in category_details:
- sub_cat = detail.compliance_sub_category
- if not sub_cat:
+ if not detail.compliance_sub_category:
continue
-
+
sub_category = frappe.db.get_value(
"Compliance Sub Category",
- sub_cat,
- [
- "allow_repeat", "repeat_on", "day", "month", "item_code",
- "project_template", "head_of_department", "category_type",
- "department", "compliance_category", "is_billable"
- ],
+ detail.compliance_sub_category,
+ ["allow_repeat", "repeat_on", "day", "month", "item_code", "project_template",
+ "compliance_category", "is_billable"],
as_dict=True
)
if not sub_category:
continue
- allow_repeat = sub_category.allow_repeat
- repeat_on = sub_category.repeat_on
- repeat_day = sub_category.day
- repeat_month = sub_category.month
- item_code = sub_category.item_code
- project_template = sub_category.project_template
- is_billable = sub_category.is_billable
-
- if valid_upto and current_date > valid_upto:
- continue
-
+ # Scheduling Logic
should_create = False
- if not allow_repeat:
+ if not sub_category.allow_repeat:
if current_date == valid_from:
should_create = True
else:
- month_number = MONTH_MAP.get(repeat_month) if repeat_month else None
- if repeat_on == "Monthly" and repeat_day and current_date.day == int(repeat_day):
- should_create = True
- elif repeat_on in ["Quarterly", "Half Yearly", "Yearly"] and month_number and repeat_day:
- if current_date.month == month_number and current_date.day == int(repeat_day):
+ month_number = MONTH_MAP.get(sub_category.month) if sub_category.month else None
+ if sub_category.repeat_on == "Monthly" and sub_category.day:
+ if current_date.day == int(sub_category.day):
should_create = True
+ elif sub_category.repeat_on in ["Quarterly", "Half Yearly", "Yearly"]:
+ if month_number and sub_category.day:
+ if current_date.month == month_number and current_date.day == int(sub_category.day):
+ should_create = True
if not should_create:
continue
+ # Create Project via Utility
project = None
- if project_template:
- project = create_project_from_template(
- None,
- project_template,
- agreement.customer,
- agreement.company,
- sub_cat,
- detail.name,
- agreement.name,
- sub_category.compliance_category,
- posting_date
- )
+ if sub_category.project_template:
+ project = create_compliance_project({
+ "compliance_sub_category": detail.compliance_sub_category,
+ "customer": agreement.customer,
+ "company": agreement.company,
+ "start_date": posting_date,
+ "compliance_agreement": agreement.name,
+ "compliance_category": sub_category.compliance_category,
+ "priority": "Medium"
+ })
- # === Calculate compliance dates (common for both cases) ===
+ # Next Date Calculation
base_date = getdate(nowdate())
- compliance_date = None
+ compliance_date = base_date
next_compliance_date = None
- if allow_repeat:
- if repeat_on == "Monthly":
- compliance_date = add_months(base_date, 1)
- next_compliance_date = add_months(compliance_date, 1)
- elif repeat_on == "Quarterly":
- compliance_date = add_months(base_date, 3)
- next_compliance_date = add_months(compliance_date, 3)
- elif repeat_on == "Half Yearly":
- compliance_date = add_months(base_date, 6)
- next_compliance_date = add_months(compliance_date, 6)
- elif repeat_on == "Yearly":
- compliance_date = add_months(base_date, 12)
- next_compliance_date = add_months(compliance_date, 12)
- else:
- compliance_date = base_date
- next_compliance_date = None
+ if sub_category.allow_repeat:
+ step = {
+ "Monthly": 1,
+ "Quarterly": 3,
+ "Half Yearly": 6,
+ "Yearly": 12
+ }.get(sub_category.repeat_on, 0)
+ compliance_date = add_months(base_date, step)
+ next_compliance_date = add_months(compliance_date, step)
- # === Create Sales Order only if billable ===
- if is_billable:
- if not frappe.db.exists("Sales Order", {
- "compliance_agreement": agreement.name,
- "compliance_sub_category": sub_cat,
- "transaction_date": nowdate()
- }):
+ # Sales Order Creation
+ if sub_category.is_billable and sub_category.item_code:
+ if not frappe.db.exists(
+ "Sales Order",
+ {
+ "compliance_agreement": agreement.name,
+ "compliance_sub_category": detail.compliance_sub_category,
+ "transaction_date": nowdate()
+ }
+ ):
try:
- item_name = frappe.db.get_value("Item", item_code, "item_name") if item_code else None
- if not item_code:
- continue
-
+ item_name = frappe.db.get_value("Item", sub_category.item_code, "item_name")
so = frappe.new_doc("Sales Order")
so.customer = agreement.customer
so.company = agreement.company
so.compliance_agreement = agreement.name
- so.compliance_sub_category = sub_cat
+ so.compliance_sub_category = detail.compliance_sub_category
so.transaction_date = nowdate()
so.delivery_date = nowdate()
if agreement.default_payment_terms_template:
so.payment_terms_template = agreement.default_payment_terms_template
-
so.append("items", {
- "item_code": item_code,
+ "item_code": sub_category.item_code,
"item_name": item_name,
"qty": 1,
- "rate": detail.rate or 0,
+ "rate": detail.rate or 0
})
-
so.insert(ignore_permissions=True)
so.submit()
if project:
project.db_set("sales_order", so.name)
so.db_set("project", project.name)
-
- frappe.db.set_value(
- "Compliance Category Details",
- detail.name,
- {
- "compliance_date": compliance_date,
- "next_compliance_date": next_compliance_date
- }
- )
-
except Exception:
frappe.log_error(frappe.get_traceback(), f"SO Creation Failed - {agreement.name}")
- else:
- frappe.db.set_value(
- "Compliance Category Details",
- detail.name,
- {
- "compliance_date": compliance_date,
- "next_compliance_date": next_compliance_date
- }
- )
-
-def create_project_from_template(sales_order, project_template, customer, company,
- compliance_sub_category, compliance_category_details_id,
- compliance_agreement, compliance_category, compliance_date=today()):
- """
- Create Project and Tasks from Project Template for Compliance Sub Category.
- """
- try:
- compliance_date = getdate(compliance_date)
- project_template_doc = frappe.get_doc("Project Template", project_template)
- sub_category_doc = frappe.get_doc('Compliance Sub Category', compliance_sub_category)
- repeat_on = frappe.db.get_value('Compliance Sub Category', compliance_sub_category, 'repeat_on')
- project_based_on_prior_phase = frappe.db.get_value('Compliance Sub Category', compliance_sub_category, 'project_based_on_prior_phase')
- previous_month_date = add_months(getdate(compliance_date), -1)
- naming_year = getdate(previous_month_date).year if project_based_on_prior_phase else getdate(compliance_date).year
- naming_month = getdate(previous_month_date).strftime("%B") if project_based_on_prior_phase else getdate(compliance_date).strftime("%B")
- if naming_month in ['January', 'February', 'March']:
- naming_quarter = 'Quarter 1'
- elif naming_month in ['April', 'May', 'June']:
- naming_quarter = 'Quarter 2'
- elif naming_month in ['July', 'August', 'September']:
- naming_quarter = 'Quarter 3'
- else:
- naming_quarter = 'Quarter 4'
- if repeat_on == "Yearly":
- naming = naming_year
- elif repeat_on == "Quarterly":
- naming = str(naming_year) + ' ' + naming_quarter
- else:
- naming = str(naming_year) + ' ' + naming_month
- project = frappe.new_doc('Project')
- project.company = company
- # project.cost_center = frappe.get_cached_value("Company", company, "cost_center")
- add_compliance_category_in_project_name = frappe.db.get_single_value(
- 'Compliance Settings', 'add_compliance_category_in_project_name'
- )
-
- if add_compliance_category_in_project_name:
- project.project_name = f"{customer}-{compliance_sub_category}-{naming}"
- else:
- sub_category_name = frappe.db.get_value(
- 'Compliance Sub Category',
- compliance_sub_category,
- 'sub_category'
+ frappe.db.set_value(
+ "Compliance Category Details",
+ detail.name,
+ {"compliance_date": compliance_date, "next_compliance_date": next_compliance_date}
)
- project.project_name = f"{customer}-{sub_category_name}-{naming}"
-
- project.customer = customer
- project.compliance_sub_category = compliance_sub_category
- project.expected_start_date = compliance_date
- project.custom_project_service = f"{compliance_sub_category}-{naming}"
- project.notes = f"{compliance_sub_category}-{naming}"
- project.category_type = frappe.db.get_value(
- 'Compliance Sub Category', compliance_sub_category, 'category_type'
- )
- project.department = frappe.db.get_value(
- 'Compliance Sub Category', compliance_sub_category, 'department'
- )
- project.sales_order = sales_order
- project.compliance_category = compliance_category
- project.compliance_agreement = compliance_agreement
-
- if project_template_doc.custom_project_duration:
- project.expected_end_date = add_days(compliance_date, project_template_doc.custom_project_duration)
-
- project.insert(ignore_permissions=True)
- project.save(ignore_permissions=True)
-
- # Assign ToDo to HOD
- if sub_category_doc.head_of_department:
- hod_user = frappe.db.get_value('Employee', sub_category_doc.head_of_department, 'user_id')
- if hod_user:
- create_todo("Project", project.name, hod_user, frappe.session.user,
- f"Project assigned to {sub_category_doc.head_of_department}")
-
- # Create Tasks from Template
- for template_task in project_template_doc.tasks:
- template_task_doc = frappe.get_doc('Task', template_task.task)
- task_doc = frappe.new_doc('Task')
- task_doc.compliance_sub_category = compliance_sub_category
- task_doc.subject = template_task.subject
- task_doc.project = project.name
- task_doc.company = project.company
- task_doc.project_name = project.project_name
- task_doc.category_type = project.category_type
- task_doc.exp_start_date = compliance_date
- task_doc.custom_serial_number = template_task.idx
-
- if template_task_doc.expected_time:
- task_doc.expected_time = template_task_doc.expected_time
- if template_task.custom_task_duration:
- task_doc.duration = template_task.custom_task_duration
- task_doc.exp_end_date = add_days(compliance_date, template_task.custom_task_duration)
-
- # Bring over additional fields from template task table
- if template_task.task_weightage:
- task_doc.task_weightage = template_task.task_weightage
- if template_task.has_external_dependencies:
- task_doc.has_external_dependencies = template_task.has_external_dependencies
- if template_task.send_email_notification_for_lag_time:
- task_doc.send_email_notification_for_lag_time = template_task.send_email_notification_for_lag_time
- if template_task.has_reimbursement:
- task_doc.has_reimbursement = template_task.has_reimbursement
-
- if template_task.custom_has_document:
- for documents in project_template_doc.custom_documents_required:
- if documents.task == template_task.task:
- for docs in documents.documents.split(', '):
- task_doc.append("custom_task_document_items", {
- "document": docs
- })
- task_doc.insert(ignore_permissions=True)
- assigned_users = []
-
- # Employee assignment
- if template_task.type == "Employee" and template_task.employee_or_group:
- user_id = frappe.db.get_value("Employee", template_task.employee_or_group, "user_id")
- if user_id:
- assigned_users.append(user_id)
-
- # Employee Group assignment
- elif template_task.type == "Employee Group" and template_task.employee_or_group:
- group = frappe.get_doc("Employee Group", template_task.employee_or_group)
- for emp in group.employee_list:
- if emp.user_id:
- assigned_users.append(emp.user_id)
-
- # Add HOD as notification
- hod_user = None
- if sub_category_doc.head_of_department:
- hod_user = frappe.db.get_value("Employee", sub_category_doc.head_of_department, "user_id")
-
- for user in assigned_users:
- create_todo("Task", task_doc.name, user, frappe.session.user,
- f"Task assigned: {task_doc.subject}")
-
- if hod_user and hod_user not in assigned_users:
- create_todo("Task", task_doc.name, hod_user, frappe.session.user,
- f"HOD notified for task: {task_doc.subject}")
-
- return project
-
- except Exception:
- frappe.log_error(frappe.get_traceback(), "Project Creation Failed")
- return None
-
-def update_status_on_customer_change(doc, method):
- """
- Trigger Compliance Agreement status update when customer is frozen/disabled or re-enabled
- """
- agreements = frappe.get_all(
- "Compliance Agreement",
- filters={"customer": doc.name, "docstatus": ["!=", 2]},
- pluck="name"
- )
- for agreement_name in agreements:
- self = frappe.get_doc("Compliance Agreement", agreement_name)
- self.update_compliance_agreement_status()
@frappe.whitelist()
def create_future_one_time_projects():
- """
- Creates Projects for One-Time Services where valid_from = TODAY
- """
today_date = getdate(today())
agreements = frappe.get_all(
"Compliance Agreement",
- filters={
- "docstatus": 1,
- "status": "Active",
- "valid_from": today_date
- },
- fields=["name", "customer", "company", "valid_from"]
+ filters={"docstatus": 1, "status": "Active", "valid_from": today_date},
+ fields=["name", "customer", "company"]
)
+
for agr in agreements:
doc = frappe.get_doc("Compliance Agreement", agr.name)
for row in doc.compliance_category_details:
if not row.compliance_sub_category:
continue
sub_cat = frappe.get_doc("Compliance Sub Category", row.compliance_sub_category)
- if sub_cat.allow_repeat:
- continue
- if not sub_cat.project_template:
- continue
- if frappe.db.exists("Project", {
- "compliance_agreement": doc.name,
- "compliance_sub_category": sub_cat.name
- }):
+
+ if sub_cat.allow_repeat or not sub_cat.project_template:
+ continue
+ if frappe.db.exists(
+ "Project",
+ {"compliance_agreement": doc.name, "compliance_sub_category": sub_cat.name}
+ ):
continue
+
try:
- create_project_from_template(
- sales_order=None,
- project_template=sub_cat.project_template,
- customer=doc.customer,
- company=doc.company,
- compliance_sub_category=sub_cat.name,
- compliance_category_details_id=row.name,
- compliance_agreement=doc.name,
- compliance_category=sub_cat.compliance_category or "",
- compliance_date=today_date
- )
-
+ create_compliance_project({
+ "compliance_sub_category": sub_cat.name,
+ "customer": doc.customer,
+ "company": doc.company,
+ "start_date": today_date,
+ "compliance_agreement": doc.name,
+ "compliance_category": sub_cat.compliance_category or "",
+ "priority": "Medium"
+ })
except Exception as e:
- frappe.log_error(
- f"Failed to create project for Agreement: {doc.name}, Sub Category: {sub_cat.name}",
- e
- )
- if not sub_cat.allow_repeat and sub_cat.is_billable:
- exists = frappe.db.exists(
+ frappe.log_error(f"Failed to create project: {e}")
+
+ # Billable Check
+ if sub_cat.is_billable:
+ if not frappe.db.exists(
"Sales Order",
{
"compliance_agreement": doc.name,
"compliance_sub_category": sub_cat.name,
"transaction_date": today_date
}
- )
- if not exists:
+ ):
so = frappe.new_doc("Sales Order")
so.customer = doc.customer
so.company = doc.company
@@ -922,28 +722,18 @@ def create_future_one_time_projects():
so.compliance_sub_category = sub_cat.name
so.transaction_date = today_date
so.delivery_date = today_date
-
if doc.default_payment_terms_template:
so.payment_terms_template = doc.default_payment_terms_template
-
- item_code = sub_cat.item_code
- item_name = frappe.db.get_value("Item", item_code, "item_name")
- rate = sub_cat.rate or 0
-
so.append("items", {
- "item_code": item_code,
- "item_name": item_name,
+ "item_code": sub_cat.item_code,
+ "item_name": frappe.db.get_value("Item", sub_cat.item_code, "item_name"),
"qty": 1,
- "rate": rate
+ "rate": sub_cat.rate or 0
})
-
so.insert(ignore_permissions=True)
so.submit()
- if row.project:
- frappe.db.set_value("Project", row.project, "sales_order", so.name)
- frappe.db.set_value("Sales Order", so.name, "project", row.project)
- doc.save(ignore_permissions=True)
-
+
+
@frappe.whitelist()
def create_sales_order_and_project_from_popup(
compliance_agreement,
@@ -951,87 +741,65 @@ def create_sales_order_and_project_from_popup(
compliance_date,
compliance_category_details_id
):
- """
- Create Project (always) and Sales Order (only if billable)
- and update compliance dates same as auto scheduler logic.
- """
-
compliance_date = getdate(compliance_date)
agreement = frappe.get_doc("Compliance Agreement", compliance_agreement)
subcat = frappe.db.get_value(
"Compliance Sub Category",
compliance_sub_category,
- [
- "item_code", "is_billable", "project_template",
- "compliance_category", "allow_repeat", "repeat_on", "day", "month"
- ],
- as_dict=True,
+ ["item_code", "is_billable", "project_template", "compliance_category", "allow_repeat", "repeat_on"],
+ as_dict=True
)
if not subcat:
frappe.throw("Compliance Sub Category not found")
+
detail = frappe.db.get_value(
"Compliance Category Details",
compliance_category_details_id,
["rate", "compliance_date", "next_compliance_date"],
as_dict=True
)
-
- rate = detail.rate or 0
- old_compliance = detail.compliance_date
- old_next = detail.next_compliance_date
- base_date = old_next or old_compliance or compliance_date
-
+ base_date = detail.next_compliance_date or detail.compliance_date or compliance_date
new_compliance_date = base_date
- new_next_date = None
-
- if subcat.allow_repeat:
- if subcat.repeat_on == "Monthly":
- new_next_date = add_months(base_date, 1)
-
- elif subcat.repeat_on == "Quarterly":
- new_next_date = add_months(base_date, 3)
-
- elif subcat.repeat_on == "Half Yearly":
- new_next_date = add_months(base_date, 6)
-
- elif subcat.repeat_on == "Yearly":
- new_next_date = add_months(base_date, 12)
+
+ step = {
+ "Monthly": 1,
+ "Quarterly": 3,
+ "Half Yearly": 6,
+ "Yearly": 12
+ }.get(subcat.repeat_on, 0) if subcat.allow_repeat else 0
+ new_next_date = add_months(base_date, step) if step > 0 else None
project = None
if subcat.project_template:
- project = create_project_from_template(
- sales_order=None,
- project_template=subcat.project_template,
- customer=agreement.customer,
- company=agreement.company,
- compliance_sub_category=compliance_sub_category,
- compliance_category_details_id=compliance_category_details_id,
- compliance_agreement=compliance_agreement,
- compliance_category=subcat.compliance_category,
- compliance_date=new_compliance_date,
- )
- if not subcat.is_billable:
+ project = create_compliance_project({
+ "compliance_sub_category": compliance_sub_category,
+ "customer": agreement.customer,
+ "company": agreement.company,
+ "start_date": new_compliance_date,
+ "compliance_agreement": compliance_agreement,
+ "compliance_category": subcat.compliance_category,
+ "priority": "Medium"
+ })
+ if not subcat.is_billable:
frappe.db.set_value(
"Compliance Category Details",
compliance_category_details_id,
- {
- "compliance_date": new_compliance_date,
- "next_compliance_date": new_next_date
- }
+ {"compliance_date": new_compliance_date, "next_compliance_date": new_next_date}
)
-
return f"Project Created: {project.name if project else 'No Template'} | Not Billable"
- exists = frappe.db.exists("Sales Order", {
- "compliance_agreement": compliance_agreement,
- "compliance_sub_category": compliance_sub_category,
- "transaction_date": new_compliance_date
- })
-
- if exists:
+ if frappe.db.exists(
+ "Sales Order",
+ {
+ "compliance_agreement": compliance_agreement,
+ "compliance_sub_category": compliance_sub_category,
+ "transaction_date": new_compliance_date
+ }
+ ):
return "Sales Order already exists for this date"
+
so = frappe.new_doc("Sales Order")
so.customer = agreement.customer
so.company = agreement.company
@@ -1039,18 +807,14 @@ def create_sales_order_and_project_from_popup(
so.compliance_sub_category = compliance_sub_category
so.transaction_date = new_compliance_date
so.delivery_date = new_compliance_date
-
if agreement.default_payment_terms_template:
so.payment_terms_template = agreement.default_payment_terms_template
-
- item_name = frappe.db.get_value("Item", subcat.item_code, "item_name")
so.append("items", {
"item_code": subcat.item_code,
- "item_name": item_name,
+ "item_name": frappe.db.get_value("Item", subcat.item_code, "item_name"),
"qty": 1,
- "rate": rate,
+ "rate": detail.rate or 0
})
-
so.insert(ignore_permissions=True)
so.submit()
@@ -1061,10 +825,6 @@ def create_sales_order_and_project_from_popup(
frappe.db.set_value(
"Compliance Category Details",
compliance_category_details_id,
- {
- "compliance_date": new_compliance_date,
- "next_compliance_date": new_next_date
- }
+ {"compliance_date": new_compliance_date, "next_compliance_date": new_next_date}
)
-
- return f"Sales Order Created: {so.name} | Project Created: {project.name if project else 'No Template'}"
+ return f"Sales Order Created: {so.name} | Project Created: {project.name if project else 'No Template'}"
\ No newline at end of file
diff --git a/one_compliance/one_compliance/utils.py b/one_compliance/one_compliance/utils.py
index 36f32a6e..4e61f369 100644
--- a/one_compliance/one_compliance/utils.py
+++ b/one_compliance/one_compliance/utils.py
@@ -1,10 +1,11 @@
import json
+from datetime import datetime, timedelta
import frappe
from frappe import _
from frappe.desk.form.assign_to import format_message_for_assign_to, get
from frappe.email.doctype.notification.notification import get_context
-from frappe.utils import date_diff, get_datetime, getdate
+from frappe.utils import date_diff, get_datetime, getdate, today, add_days, add_months
from frappe.utils.user import get_users_with_role
from frappe.desk.doctype.notification_log.notification_log import (
enqueue_create_notification,
@@ -13,6 +14,7 @@
)
from frappe.desk.form.document_follow import follow_document
from frappe.utils.data import strip_html
+from frappe.utils.data import cint
@frappe.whitelist()
@@ -562,3 +564,402 @@ def notify_assignment(assigned_by, allocated_to, doc_type, doc_name, action="CLO
enqueue_create_notification(allocated_to, notification_doc)
+
+
+# SHARED PROJECT & TASK UTILITIES
+
+def get_compliance_period_name(start_date, sub_category_doc):
+ """
+ Calculates the suffix for the project name (e.g., '2024 Quarter 1')
+ based on repeat settings and prior phase logic.
+ """
+ start_date = getdate(start_date)
+
+ project_based_on_prior_phase = sub_category_doc.project_based_on_prior_phase
+ repeat_on = sub_category_doc.repeat_on
+
+ # Adjust date if based on prior phase
+ # (e.g. Audit done in Feb for Jan data)
+ reference_date = (
+ add_months(start_date, -1)
+ if project_based_on_prior_phase
+ else start_date
+ )
+
+ naming_year = reference_date.year
+ naming_month = reference_date.strftime("%B")
+
+ month_to_quarter = {
+ **dict.fromkeys(["January", "February", "March"], "Quarter 1"),
+ **dict.fromkeys(["April", "May", "June"], "Quarter 2"),
+ **dict.fromkeys(["July", "August", "September"], "Quarter 3"),
+ **dict.fromkeys(["October", "November", "December"], "Quarter 4"),
+ }
+
+ naming_quarter = month_to_quarter.get(naming_month, "Quarter 1")
+
+ if repeat_on == "Yearly":
+ return str(naming_year)
+ elif repeat_on == "Quarterly":
+ return f"{naming_year} {naming_quarter}"
+ else:
+ return f"{naming_year} {naming_month}"
+
+
+def create_compliance_project(args):
+ """
+ Unified function to create a Project and its Tasks.
+
+ args: {
+ "compliance_sub_category": str,
+ "customer": str,
+ "company": str,
+ "start_date": date,
+ "sales_order": str (optional),
+ "compliance_agreement": str (optional),
+ "assign_to_employees": list (optional),
+ "is_premium": bool,
+ "priority": str,
+ "remark": str,
+ "custom_instructions": str,
+ "naming_override": dict (optional)
+ { "auto": bool, "custom_name": str }
+ }
+ """
+ sub_cat_name = args.get("compliance_sub_category")
+ sub_cat_doc = frappe.get_doc("Compliance Sub Category", {'item_code' : sub_cat_name})
+
+ if not sub_cat_doc.project_template:
+ frappe.throw(_(f"Project Template does not exist for {sub_cat_name}"))
+
+ template_doc = frappe.get_doc(
+ "Project Template",
+ sub_cat_doc.project_template
+ )
+
+ naming_suffix = get_compliance_period_name(
+ args.get("start_date"),
+ sub_cat_doc
+ )
+
+ # 1. Create Project Header
+ project = frappe.new_doc("Project")
+ project.company = args.get("company")
+ project.customer = args.get("customer")
+ project.compliance_sub_category = sub_cat_name
+ project.compliance_category = sub_cat_doc.compliance_category
+ project.expected_start_date = args.get("start_date")
+ project.priority = args.get("priority", "Medium")
+ project.custom_project_service = f"{sub_cat_name}-{naming_suffix}"
+ project.category_type = sub_cat_doc.category_type
+ project.department = sub_cat_doc.department
+
+ # Linkages
+ if args.get("sales_order"):
+ project.sales_order = args.get("sales_order")
+
+ if args.get("compliance_agreement"):
+ project.compliance_agreement = args.get("compliance_agreement")
+
+ # Optional fields
+ if args.get("remark"):
+ project.notes = args.get("remark")
+
+ if args.get("custom_instructions"):
+ project.custom_instructions = args.get("custom_instructions")
+
+ # Naming Logic
+ add_cat_in_name = frappe.db.get_single_value(
+ "Compliance Settings",
+ "add_compliance_category_in_project_name",
+ )
+
+ naming_config = args.get("naming_override", {})
+
+ customer_name = args.get("customer")
+
+ if args.get("sales_order"):
+ so_cust_name = frappe.db.get_value(
+ "Sales Order",
+ args.get("sales_order"),
+ "customer_name",
+ )
+ if so_cust_name:
+ customer_name = so_cust_name
+
+ if naming_config.get("auto", True):
+ mid_name = (
+ sub_cat_name
+ if add_cat_in_name
+ else sub_cat_doc.sub_category
+ )
+ project.project_name = (
+ f"{customer_name or ' '}-"
+ f"{mid_name}-"
+ f"{naming_suffix}"
+ )
+ else:
+ custom_name = naming_config.get("custom_name", "")
+ project.project_name = (
+ f"{custom_name or ' '}-"
+ f"{customer_name}-"
+ f"{sub_cat_name}-"
+ f"{naming_suffix}"
+ )
+
+ # Dates & Flags
+ duration = template_doc.custom_project_duration or 0
+ project.expected_end_date = add_days(
+ args.get("start_date"),
+ duration,
+ )
+
+ project.is_premium = (
+ 1
+ if (
+ args.get("is_premium")
+ and template_doc.has_premium_tasks
+ )
+ else 0
+ )
+
+ project.save(ignore_permissions=True)
+
+ # 2. HOD Assignment (Project Level)
+ head_of_department_user = None
+
+ if sub_cat_doc.head_of_department:
+ head_of_department_user = frappe.db.get_value(
+ "Employee",
+ {"employee": sub_cat_doc.head_of_department},
+ "user_id",
+ )
+
+ if head_of_department_user:
+ create_todo(
+ "Project",
+ project.name,
+ head_of_department_user,
+ frappe.session.user,
+ f"Project assigned to {sub_cat_doc.head_of_department}",
+ )
+
+ # 3. Task Generation
+ _generate_tasks_from_list(
+ project=project,
+ sub_cat_doc=sub_cat_doc,
+ template_doc=template_doc,
+ task_list=template_doc.tasks,
+ args=args,
+ head_of_department_user=head_of_department_user,
+ )
+
+ if args.get("is_premium") and hasattr(
+ template_doc, "premium_tasks"
+ ):
+ _generate_tasks_from_list(
+ project=project,
+ sub_cat_doc=sub_cat_doc,
+ template_doc=template_doc,
+ task_list=template_doc.premium_tasks,
+ args=args,
+ head_of_department_user=head_of_department_user,
+ is_premium=True,
+ )
+
+ return project
+
+
+def _generate_tasks_from_list(
+ project,
+ sub_cat_doc,
+ template_doc,
+ task_list,
+ args,
+ head_of_department_user,
+ is_premium=False,
+):
+ """Helper to loop through task list and create tasks"""
+
+ assign_employees = args.get("assign_to_employees", [])
+
+ for template_task in reversed(task_list):
+
+ # Avoid duplicates for premium reruns
+ if (
+ is_premium
+ and frappe.db.exists(
+ "Task",
+ {
+ "project": project.name,
+ "subject": template_task.subject,
+ },
+ )
+ ):
+ continue
+
+ t_doc_template = frappe.get_doc(
+ "Task",
+ template_task.task,
+ )
+
+ task = frappe.new_doc("Task")
+ task.compliance_sub_category = sub_cat_doc.name
+ task.subject = template_task.subject
+ task.project = project.name
+ task.company = project.company
+ task.project_name = project.project_name
+ task.category_type = project.category_type
+ task.exp_start_date = project.expected_start_date
+ task.custom_serial_number = template_task.idx
+ task.department = sub_cat_doc.department
+ task.task_weightage = (
+ template_task.task_weightage or 0
+ )
+ task.is_premium_task = 1 if is_premium else 0
+ task.has_reimbursement = (
+ template_task.has_reimbursement
+ )
+
+ # Time & Duration
+ if t_doc_template.expected_time:
+ task.expected_time = (
+ t_doc_template.expected_time
+ )
+
+ if template_task.custom_task_duration:
+ task.duration = (
+ template_task.custom_task_duration
+ )
+ task.exp_end_date = add_days(
+ project.expected_start_date,
+ template_task.custom_task_duration,
+ )
+
+ # External Dependencies
+ if template_task.has_external_dependencies:
+ task.has_external_dependencies = 1
+ task.send_email_notification_for_lag_time = (
+ template_task
+ .send_email_notification_for_lag_time
+ )
+
+ # Internal Dependencies
+ if t_doc_template.depends_on:
+ for depends_task in t_doc_template.depends_on:
+ dependent_task_name = frappe.db.get_value(
+ "Task",
+ {
+ "project": project.name,
+ "subject": depends_task.subject,
+ },
+ "name",
+ )
+
+ if dependent_task_name:
+ task.append(
+ "depends_on",
+ {"task": dependent_task_name},
+ )
+
+ # Documents
+ if (
+ template_task.custom_has_document
+ and hasattr(
+ template_doc,
+ "custom_documents_required",
+ )
+ ):
+ for documents in (
+ template_doc.custom_documents_required
+ ):
+ if documents.task == template_task.task:
+ for doc_name in documents.documents.split(
+ ", "
+ ):
+ task.append(
+ "custom_task_document_items",
+ {"document": doc_name},
+ )
+
+ task.save(ignore_permissions=True)
+
+ # 4. Task Assignment Logic
+
+ # A. Direct Employee Assignment
+ if assign_employees:
+ for emp_id in assign_employees:
+ user_id = frappe.db.get_value(
+ "Employee",
+ emp_id,
+ "user_id",
+ )
+
+ if (
+ user_id
+ and user_id
+ != head_of_department_user
+ ):
+ create_todo(
+ "Task",
+ task.name,
+ user_id,
+ frappe.session.user,
+ f"Task {task.name} Assigned",
+ )
+
+ # B. Template Based Assignment
+ elif (
+ template_task.type
+ and template_task.employee_or_group
+ ):
+ frappe.db.set_value(
+ "Task",
+ task.name,
+ "assigned_to",
+ template_task.employee_or_group,
+ )
+
+ targets = []
+
+ if template_task.type == "Employee":
+ user = frappe.db.get_value(
+ "Employee",
+ template_task.employee_or_group,
+ "user_id",
+ )
+ if user:
+ targets.append(user)
+
+ elif template_task.type == "Employee Group":
+ group = frappe.get_doc(
+ "Employee Group",
+ template_task.employee_or_group,
+ )
+ if group.employee_list:
+ targets = [
+ e.user_id
+ for e in group.employee_list
+ if e.user_id
+ ]
+
+ for user in targets:
+ if user != head_of_department_user:
+ create_todo(
+ "Task",
+ task.name,
+ user,
+ frappe.session.user,
+ f"Task {task.name} Assigned",
+ )
+
+ # C. HOD Notification
+ if head_of_department_user:
+ create_todo(
+ "Task",
+ task.name,
+ head_of_department_user,
+ frappe.session.user,
+ f"HOD Notification: "
+ f"Task {task.subject} created",
+ )