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", + )