From 3452e7ac133818ed6b68442f6cd42d02b9d07a37 Mon Sep 17 00:00:00 2001 From: NaseeraVP Date: Tue, 18 Nov 2025 16:39:59 +0530 Subject: [PATCH] fix: Project Completion Date --- one_compliance/hooks.py | 3 +- .../one_compliance/doc_events/task.py | 150 +++++-- one_compliance/public/js/task.js | 418 +++++++++--------- 3 files changed, 337 insertions(+), 234 deletions(-) diff --git a/one_compliance/hooks.py b/one_compliance/hooks.py index 538ec15a..1a954e93 100644 --- a/one_compliance/hooks.py +++ b/one_compliance/hooks.py @@ -132,7 +132,8 @@ 'one_compliance.one_compliance.doc_events.task.make_sales_invoice', 'one_compliance.one_compliance.doc_events.task.subtask_on_update', 'one_compliance.one_compliance.doc_events.task.on_task_update', - 'one_compliance.one_compliance.doc_events.task.enable_customer_on_task_completion' + 'one_compliance.one_compliance.doc_events.task.enable_customer_on_task_completion', + 'one_compliance.one_compliance.doc_events.task.on_update', ], 'validate':[ 'one_compliance.one_compliance.doc_events.task.append_users_to_project', diff --git a/one_compliance/one_compliance/doc_events/task.py b/one_compliance/one_compliance/doc_events/task.py index e1ea8edb..efd535d0 100644 --- a/one_compliance/one_compliance/doc_events/task.py +++ b/one_compliance/one_compliance/doc_events/task.py @@ -10,6 +10,7 @@ from frappe.utils.data import format_date from frappe.utils.nestedset import NestedSet from erpnext.projects.doctype.task.task import check_if_child_exists, CircularReferenceError +from frappe.utils import getdate from one_compliance.one_compliance.utils import ( create_project_completion_todos, @@ -191,6 +192,25 @@ def on_update(self): self.unassign_todo() self.populate_depends_on() + def before_validate(self): + self.update_project_expected_end_date() + + def update_project_expected_end_date(self): + ''' + Update the project's expected end date if the task's completion date is later. + ''' + if not self.project or not self.completed_on: + return + current_eed = frappe.db.get_value("Project", self.project, "expected_end_date") + + if getdate(current_eed) < getdate(self.completed_on): + frappe.db.set_value( + "Project", + self.project, + "expected_end_date", + self.completed_on + ) + def unassign_todo(self): if self.status == "Completed": close_all_assignments(self.doctype, self.name) @@ -298,7 +318,6 @@ def update_status(self): self.db_set("status", "Overdue", update_modified=False) self.update_project() - @frappe.whitelist() def append_users_to_project(doc, method): if doc.assigned_to and doc.project: @@ -335,37 +354,75 @@ def add_project_user_if_not_exists(project, user_id): @frappe.whitelist() def task_on_update(doc, method): set_task_time_line(doc) + if doc.status == 'Completed': - if frappe.db.get_single_value("Compliance Settings", "enable_task_complete_notification_for_director"): + if frappe.db.get_single_value( + "Compliance Settings", + "enable_task_complete_notification_for_director" + ): task_complete_notification_for_director(doc) + if doc.custom_is_payable: create_journal_entry(doc) - if doc.project: - if frappe.db.exists('Project', doc.project): - project = frappe.get_doc ('Project', doc.project) - if project.status == 'Completed': - if not frappe.db.get_value("Sales Order", project.sales_order, "custom_is_rework"): - create_project_completion_todos(project.sales_order, project.project_name) - # send_project_completion_mail = frappe.db.get_value('Customer', project.customer, 'send_project_completion_mail') - # if send_project_completion_mail: - # email_id = frappe.db.get_value('Customer', project.customer, 'email_id') - # if email_id: - # project_complete_notification_for_customer(project, email_id) - # Check if this task is a dependency for other tasks - dependent_tasks = frappe.get_all('Task Depends On', filters={'task': doc.name}, fields=['parent']) + + if doc.project and frappe.db.exists("Project", doc.project): + + project_status, sales_order = frappe.db.get_value( + "Project", + doc.project, + ["status", "sales_order"] + ) + + if project_status == "Completed": + + if not sales_order or not frappe.db.exists("Sales Order", sales_order): + sales_order = frappe.db.exists( + "Sales Order", + {"project": doc.project, "docstatus": 1} + ) + if not sales_order: + frappe.throw( + _(f"Sales Order not found for {doc.project}") + ) + + if not frappe.db.get_value( + "Sales Order", + sales_order, + "custom_is_rework" + ): + create_project_completion_todos(sales_order, doc.project) + + dependent_tasks = frappe.get_all( + 'Task Depends On', + filters={'task': doc.name}, + fields=['parent'] + ) + for dependent_task in dependent_tasks: - task = frappe.get_doc('Task', dependent_task.parent) + task_name = dependent_task.parent + + current_status = frappe.db.get_value("Task", task_name, "status") + all_dependencies_completed = True - # Check if all dependent tasks are completed - for dependency in task.depends_on: - dependency_doc = frappe.get_doc('Task', dependency.task) - if dependency_doc.status != 'Completed': + + dependencies = frappe.get_all( + "Task Depends On", + filters={"parent": task_name}, + fields=["task"] + ) + + for dependency in dependencies: + dep_status = frappe.db.get_value( + "Task", + dependency.task, + "status" + ) + if dep_status != "Completed": all_dependencies_completed = False break - # If all dependencies are completed, mark the dependent task as completed - if all_dependencies_completed and task.status != 'Completed': - task.status = 'Completed' - task.save() + + if all_dependencies_completed and current_status != "Completed": + frappe.db.set_value("Task", task_name, "status", "Completed") @frappe.whitelist() def task_complete_notification_for_director(doc): @@ -501,14 +558,32 @@ def create_sales_order(project, rate, sub_category_doc, payment_terms=None, subm frappe.msgprint("Sales Order {0} Created against {1}".format(new_sales_order.name, project.name), alert=True) @frappe.whitelist() -def update_task_status(task_id, status, completed_by, completed_on): - # Load the task document from the database +def update_task_status(task_id, status, completed_by, completed_on, comment=None): + ''' + Update the status and completion details of a Task and optionally add comments + to both the Task and its linked Project. + ''' + task_doc = frappe.get_doc("Task", task_id) task_doc.completed_on = frappe.utils.getdate(completed_on) task_doc.status = status task_doc.completed_by = completed_by + if comment: + task_prefix = frappe.bold(_("Reason for updating task status:")) + task_comment = f"{task_prefix}
{frappe.utils.escape_html(comment)}" + task_doc.add_comment("Comment", task_comment) + + if task_doc.project: + formatted_date = frappe.utils.formatdate(completed_on, "dd-mm-yyyy") + project_prefix = frappe.bold(_("Reason for Updating Expected End Date:")) + project_comment = ( + f"{project_prefix}
" + f"Task {task_doc.name} Completed on {formatted_date}." + ) + project_doc = frappe.get_doc("Project", task_doc.project) + project_doc.add_comment("Comment", project_comment) + task_doc.save() - frappe.db.commit() frappe.msgprint("Task Status has been set to {0}".format(status), alert=True) return True @@ -694,3 +769,24 @@ def enable_customer_on_task_completion(doc, method): customer.aml_compliance_checked = 1 customer.save(ignore_permissions=True) frappe.msgprint(f"Customer {customer.name} has been enabled after AML compliance task completion.") + +def on_update(doc, method): + ''' + Update the project's expected end date if the task's completion date is later. + ''' + if not doc.project or not doc.completed_on: + return + + project = frappe.get_doc("Project", doc.project) + + if project.expected_end_date < doc.completed_on: + project.db_set('expected_end_date', doc.completed_on) + task_completed_date = getdate(doc.completed_on) + project_eed = getdate(project.expected_end_date) + + if task_completed_date > project_eed: + project.db_set("expected_end_date", task_completed_date) + comment_content = '

Reason for updating project completion date:

' + comment_content += f"Expected End Date updated to {doc.completed_on} based on Task {doc.name} completion." + project.add_comment(text=_(comment_content)) + diff --git a/one_compliance/public/js/task.js b/one_compliance/public/js/task.js index a7b861fa..e7dcd451 100644 --- a/one_compliance/public/js/task.js +++ b/one_compliance/public/js/task.js @@ -1,223 +1,229 @@ frappe.ui.form.on('Task',{ refresh(frm){ - let roles = frappe.user_roles; - let current_user = frappe.session.user; + let roles = frappe.user_roles; + let current_user = frappe.session.user; - // Call a custom method to check permission - frappe.call({ - method: 'one_compliance.one_compliance.doc_events.task.check_readiness_edit_permission', - args: { - user: current_user - }, - callback: function(r) { - if (r.message === true) { - frm.set_df_property('readiness_status', 'read_only', 0); - } else { - frm.set_df_property('readiness_status', 'read_only', 1); - } - } - }); + // Call a custom method to check permission + frappe.call({ + method: 'one_compliance.one_compliance.doc_events.task.check_readiness_edit_permission', + args: { + user: current_user + }, + callback: function(r) { + if (r.message === true) { + frm.set_df_property('readiness_status', 'read_only', 0); + } else { + frm.set_df_property('readiness_status', 'read_only', 1); + } + } + }); - if(roles.includes('Compliance Manager') || roles.includes('Director')){ - if(!frm.is_new() && frm.doc.is_template == 0){ - frm.add_custom_button('View Credential', () => { - customer_credentials(frm) - }); - frm.add_custom_button('View Document', () => { - customer_document(frm) - }); - } - } - if(!frm.is_new()){ - frm.add_custom_button('Status Update', () => { - update_status(frm) - }); - } - if(frm.doc.custom_task_document_items){ - frm.set_df_property('custom_task_document_items', 'read_only', 0); - } - if(frm.doc.project){ - frm.set_df_property('is_group','hidden',1); - frm.set_df_property('is_template','hidden',1); - } + if(roles.includes('Compliance Manager') || roles.includes('Director')){ + if(!frm.is_new() && frm.doc.is_template == 0){ + frm.add_custom_button('View Credential', () => { + customer_credentials(frm) + }); + frm.add_custom_button('View Document', () => { + customer_document(frm) + }); + } + } + if(!frm.is_new()){ + frm.add_custom_button('Status Update', () => { + update_status(frm) + }); + } + if(frm.doc.custom_task_document_items){ + frm.set_df_property('custom_task_document_items', 'read_only', 0); + } + if(frm.doc.project){ + frm.set_df_property('is_group','hidden',1); + frm.set_df_property('is_template','hidden',1); + } } }); let customer_credentials = function (frm) { frappe.db.get_value('Project', frm.doc.project, 'customer') .then(r =>{ - let customer = r.message.customer; - let d = new frappe.ui.Dialog({ - title: 'Enter details', - fields: [ - { - label: 'Purpose', - fieldname: 'purpose', - fieldtype: 'Link', - options: 'Credential Type', - get_query: function () { - return { - filters: { - 'compliance_sub_category':frm.doc.compliance_sub_category - } - }; - } - } - ], - primary_action_label: 'View Credential', - primary_action(values) { - frappe.call({ - method:'one_compliance.one_compliance.utils.view_credential_details', - args:{ - 'customer': customer, - 'purpose': values.purpose - }, - callback:function(r){ - if (r.message){ - d.hide(); - let newd = new frappe.ui.Dialog({ - title: 'Credential details', - fields: [ - { - label: 'Username', - fieldname: 'username', - fieldtype: 'Data', - read_only: 1, - default:r.message[0] - }, - { - label: 'Password', - fieldame: 'password', - fieldtype: 'Data', - read_only: 1, - default:r.message[1] - }, - { - label: 'Url', - fieldname: 'url', - fieldtype: 'Data', - options: 'URL', - read_only: 1, - default:r.message[2] - } - ], - primary_action_label: 'Close', - primary_action(value) { - newd.hide(); - }, - secondary_action_label : 'Go To URL', - secondary_action(value){ - window.open(r.message[2]) - } - }); - newd.show(); - } - } - }) - } - }); - d.show(); - }) - } + let customer = r.message.customer; + let d = new frappe.ui.Dialog({ + title: 'Enter details', + fields: [ + { + label: 'Purpose', + fieldname: 'purpose', + fieldtype: 'Link', + options: 'Credential Type', + get_query: function () { + return { + filters: { + 'compliance_sub_category':frm.doc.compliance_sub_category + } + }; + } + } + ], + primary_action_label: 'View Credential', + primary_action(values) { + frappe.call({ + method:'one_compliance.one_compliance.utils.view_credential_details', + args:{ + 'customer': customer, + 'purpose': values.purpose + }, + callback:function(r){ + if (r.message){ + d.hide(); + let newd = new frappe.ui.Dialog({ + title: 'Credential details', + fields: [ + { + label: 'Username', + fieldname: 'username', + fieldtype: 'Data', + read_only: 1, + default:r.message[0] + }, + { + label: 'Password', + fieldame: 'password', + fieldtype: 'Data', + read_only: 1, + default:r.message[1] + }, + { + label: 'Url', + fieldname: 'url', + fieldtype: 'Data', + options: 'URL', + read_only: 1, + default:r.message[2] + } + ], + primary_action_label: 'Close', + primary_action(value) { + newd.hide(); + }, + secondary_action_label : 'Go To URL', + secondary_action(value){ + window.open(r.message[2]) + } + }); + newd.show(); + } + } + }) + } + }); + d.show(); + }) + } let customer_document = function (frm) { frappe.db.get_value('Project', frm.doc.project, 'customer') .then(r =>{ - let customer = r.message.customer; - let d = new frappe.ui.Dialog({ - title: 'Enter details', - fields: [ - { - label: 'Compliance Sub Category', - fieldname: 'compliance_sub_category', - fieldtype: 'Link', - options: 'Compliance Sub Category' - } - ], - primary_action_label: 'View Document', - primary_action(values) { - frappe.call({ - method:'one_compliance.one_compliance.utils.view_customer_documents', - args:{ - 'customer': customer, - 'compliance_sub_category': values.compliance_sub_category - }, - callback:function(r){ - if (r.message){ - d.hide(); - let newd = new frappe.ui.Dialog({ - title: 'Document details', - fields: [ - { - label: 'Document Attachment', - fieldname: 'document_attachment', - fieldtype: 'Data', - read_only: 1, - default:r.message[0] - }, - ], - primary_action_label: 'Close', - primary_action(value) { - newd.hide(); - }, - secondary_action_label : 'Download', - secondary_action(value){ - window.open(r.message[0]) - } - }); - newd.show(); - } - } - }) - } - }); - d.show(); - }) - } + let customer = r.message.customer; + let d = new frappe.ui.Dialog({ + title: 'Enter details', + fields: [ + { + label: 'Compliance Sub Category', + fieldname: 'compliance_sub_category', + fieldtype: 'Link', + options: 'Compliance Sub Category' + } + ], + primary_action_label: 'View Document', + primary_action(values) { + frappe.call({ + method:'one_compliance.one_compliance.utils.view_customer_documents', + args:{ + 'customer': customer, + 'compliance_sub_category': values.compliance_sub_category + }, + callback:function(r){ + if (r.message){ + d.hide(); + let newd = new frappe.ui.Dialog({ + title: 'Document details', + fields: [ + { + label: 'Document Attachment', + fieldname: 'document_attachment', + fieldtype: 'Data', + read_only: 1, + default:r.message[0] + }, + ], + primary_action_label: 'Close', + primary_action(value) { + newd.hide(); + }, + secondary_action_label : 'Download', + secondary_action(value){ + window.open(r.message[0]) + } + }); + newd.show(); + } + } + }) + } + }); + d.show(); + }) + } let update_status = function(frm){ - let d = new frappe.ui.Dialog({ - title: 'Enter details', - fields: [ - { - label: 'Status', - fieldname: 'status', - fieldtype: 'Select', - options: 'Open\nWorking\nPending Review\nCompleted\nHold', - default: 'Completed' - }, - { - label: 'Completed By', - fieldname: 'completed_by', - fieldtype: 'Link', - options: 'User' - }, - { - label: 'Completed On', - fieldname: 'completed_on', - fieldtype: 'Date', - default: 'Today' - }, - ], - primary_action_label: 'Update', - primary_action(values) { - frappe.call({ - method: 'one_compliance.one_compliance.doc_events.task.update_task_status', - args: { - 'task_id': frm.doc.name, - 'status': values.status, - 'completed_by': values.completed_by, - 'completed_on': values.completed_on - }, - callback: function(r){ - if (r.message){ - d.hide(); - frm.reload_doc(); - } - } - }); - }, - }); - d.set_value('completed_by', frappe.session.user); -d.show(); + let d = new frappe.ui.Dialog({ + title: 'Enter details', + fields: [ + { + label: 'Status', + fieldname: 'status', + fieldtype: 'Select', + options: 'Open\nWorking\nPending Review\nCompleted\nHold', + default: 'Completed' + }, + { + label: 'Completed By', + fieldname: 'completed_by', + fieldtype: 'Link', + options: 'User' + }, + { + label: 'Completed On', + fieldname: 'completed_on', + fieldtype: 'Date', + default: 'Today' + }, + { + label: 'Comment', + fieldname: 'comment', + fieldtype: 'Small Text' + } + ], + primary_action_label: 'Update', + primary_action(values) { + frappe.call({ + method: 'one_compliance.one_compliance.doc_events.task.update_task_status', + args: { + 'task_id': frm.doc.name, + 'status': values.status, + 'completed_by': values.completed_by, + 'completed_on': values.completed_on, + 'comment': values.comment + }, + callback: function(r){ + if (r.message){ + d.hide(); + frm.reload_doc(); + } + } + }); + }, + }); + d.set_value('completed_by', frappe.session.user); + d.show(); };