diff --git a/one_compliance/hooks.py b/one_compliance/hooks.py index 39f06aed..c8cbeaf2 100644 --- a/one_compliance/hooks.py +++ b/one_compliance/hooks.py @@ -122,30 +122,31 @@ # Hook on document methods and events doc_events = { - 'Project Template':{ - 'after_insert': 'one_compliance.one_compliance.doc_events.project_template.update_project_template', - 'on_trash': 'one_compliance.one_compliance.doc_events.project_template.on_trash', - 'validate': 'one_compliance.one_compliance.doc_events.project_template.validate', - }, - 'Task':{ - 'on_update':[ - 'one_compliance.one_compliance.doc_events.task.task_on_update', - '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' - ], - 'validate':[ - 'one_compliance.one_compliance.doc_events.task.append_users_to_project', - 'one_compliance.one_compliance.doc_events.task.set_task_status_to_hold', - ], - 'autoname': 'one_compliance.one_compliance.doc_events.task.autoname', - 'after_insert':'one_compliance.one_compliance.doc_events.task.set_task_readiness_flow_on_creation', - }, - 'Project':{ - 'on_update': 'one_compliance.one_compliance.doc_events.project.project_on_update', + 'Project Template':{ + 'after_insert': 'one_compliance.one_compliance.doc_events.project_template.update_project_template', + 'on_trash': 'one_compliance.one_compliance.doc_events.project_template.on_trash', + 'validate': 'one_compliance.one_compliance.doc_events.project_template.validate', + }, + 'Task':{ + 'on_update':[ + 'one_compliance.one_compliance.doc_events.task.task_on_update', + '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.on_update', + ], + 'validate':[ + 'one_compliance.one_compliance.doc_events.task.append_users_to_project', + 'one_compliance.one_compliance.doc_events.task.set_task_status_to_hold', + ], + 'autoname': 'one_compliance.one_compliance.doc_events.task.autoname', + 'after_insert':'one_compliance.one_compliance.doc_events.task.set_task_readiness_flow_on_creation', + }, + 'Project':{ + 'on_update': 'one_compliance.one_compliance.doc_events.project.project_on_update', 'after_insert': 'one_compliance.one_compliance.doc_events.project.create_commission_purchase_invoice', - }, + }, 'Customer':{ 'on_update':[ 'one_compliance.one_compliance.doc_events.customer.customer_on_update', diff --git a/one_compliance/one_compliance/doc_events/task.py b/one_compliance/one_compliance/doc_events/task.py index c448e1d0..8b19a7f6 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, @@ -205,6 +206,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) @@ -387,37 +407,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): @@ -553,14 +611,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 @@ -746,3 +822,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 db41a340..dc71cf41 100644 --- a/one_compliance/public/js/task.js +++ b/one_compliance/public/js/task.js @@ -24,19 +24,27 @@ frappe.ui.form.on("Task", { 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); + 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); @@ -49,6 +57,7 @@ frappe.ui.form.on("Task", { 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: [ @@ -60,25 +69,33 @@ let customer_credentials = function (frm) { get_query: function () { return { filters: { - compliance_sub_category: frm.doc.compliance_sub_category, + 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", + 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", @@ -89,7 +106,7 @@ let customer_credentials = function (frm) { }, { label: "Password", - fieldame: "password", + fieldname: "password", fieldtype: "Data", read_only: 1, default: r.message[1], @@ -103,21 +120,27 @@ let customer_credentials = function (frm) { 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(); }); }; @@ -125,8 +148,10 @@ let customer_credentials = function (frm) { 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", @@ -135,19 +160,27 @@ let customer_document = function (frm) { options: "Compliance Sub Category", }, ], + primary_action_label: "View Document", + primary_action(values) { frappe.call({ - method: "one_compliance.one_compliance.utils.view_customer_documents", + method: + "one_compliance.one_compliance.utils.view_customer_documents", + args: { customer: customer, - compliance_sub_category: values.compliance_sub_category, + 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", @@ -157,21 +190,27 @@ let customer_document = function (frm) { 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(); }); }; @@ -179,12 +218,14 @@ let customer_document = function (frm) { 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", + options: + "Open\nWorking\nPending Review\nCompleted\nHold", default: "Completed", }, { @@ -199,18 +240,28 @@ let update_status = function (frm) { 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(); @@ -220,275 +271,225 @@ let update_status = function (frm) { }); }, }); + d.set_value("completed_by", frappe.session.user); d.show(); }; function handle_task_checklist(frm) { - if (!frm.doc.checklist_template || frm.doc.status === 'Completed' || frm.doc.status === 'Cancelled' || frm.doc.status === 'Hold') { + if ( + !frm.doc.checklist_template || + frm.doc.status === "Completed" || + frm.doc.status === "Cancelled" || + frm.doc.status === "Hold" + ) { return; } - // Add button to the form (standalone, not under Actions) - frm.add_custom_button(__('Open Checklist'), function() { - // Fetch the checklist template + frm.add_custom_button(__("Open Checklist"), function () { frappe.call({ - method: 'frappe.client.get', + method: "frappe.client.get", + args: { - doctype: 'Task Checklist Template', - name: frm.doc.checklist_template + doctype: "Task Checklist Template", + name: frm.doc.checklist_template, }, - callback: function(r) { + + callback: function (r) { if (r.message) { show_checklist_popup(frm, r.message); } - } + }, }); }); } function show_checklist_popup(frm, template) { - // Get existing checklist items from the task or create from template let checklist_items = []; - - if (frm.doc.task_checklist_template && frm.doc.task_checklist_template.length > 0) { - // Use existing checklist items from the task - checklist_items = frm.doc.task_checklist_template.map((item, index) => ({ - checklist_item: item.checklist_item, - completed: item.completed || 0, - idx: item.idx || (index + 1) - })); + + if ( + frm.doc.task_checklist_template && + frm.doc.task_checklist_template.length > 0 + ) { + checklist_items = frm.doc.task_checklist_template.map( + (item, index) => ({ + checklist_item: item.checklist_item, + completed: item.completed || 0, + idx: item.idx || index + 1, + }) + ); } else { - // Create from template (first time) checklist_items = template.checklist.map((item, index) => ({ checklist_item: item.checklist_item, completed: 0, - idx: item.idx || (index + 1) + idx: item.idx || index + 1, })); } - let checklist_html = generate_checklist_html(checklist_items, template.template_name); + let checklist_html = generate_checklist_html( + checklist_items, + template.template_name + ); let checklist_popup = new frappe.ui.Dialog({ - title: __('Task Checklist'), + title: __("Task Checklist"), + fields: [ { - fieldname: 'checklist_html', - fieldtype: 'HTML', - } + fieldname: "checklist_html", + fieldtype: "HTML", + }, ], - primary_action_label: __('Save & Close'), - primary_action: function() { + + primary_action_label: __("Save & Close"), + + primary_action: function () { save_checklist(frm, checklist_items, checklist_popup); }, - secondary_action_label: __('Cancel'), - size: 'large' + + secondary_action_label: __("Cancel"), + size: "large", }); - checklist_popup.fields_dict.checklist_html.$wrapper.html(checklist_html); - - // Add event listeners for checkboxes - checklist_popup.fields_dict.checklist_html.$wrapper.find('.checklist-checkbox').on('change', function() { - let idx = parseInt($(this).data('idx')); - let is_checked = $(this).is(':checked'); - - // Update the checklist_items array - let item = checklist_items.find(i => i.idx === idx); - if (item) { - item.completed = is_checked ? 1 : 0; - } + checklist_popup.fields_dict.checklist_html.$wrapper.html( + checklist_html + ); - // Update the visual state - let label = $(this).siblings('label'); - let itemDiv = $(this).closest('.checklist-item'); - - if (is_checked) { - itemDiv.addClass('completed'); - label.css({ - 'text-decoration': 'line-through', - 'color': '#888' - }); - // Add checkmark animation - itemDiv.css('transform', 'scale(0.98)'); - setTimeout(() => itemDiv.css('transform', 'scale(1)'), 200); - } else { - itemDiv.removeClass('completed'); - label.css({ - 'text-decoration': 'none', - 'color': '#495057' - }); - } + checklist_popup.fields_dict.checklist_html.$wrapper + .find(".checklist-checkbox") + .on("change", function () { + let idx = parseInt($(this).data("idx")); + let is_checked = $(this).is(":checked"); - // Update progress - update_progress(checklist_popup, checklist_items); - }); + let item = checklist_items.find((i) => i.idx === idx); - // Add click event for labels to toggle checkbox - checklist_popup.fields_dict.checklist_html.$wrapper.find('.checklist-item label').on('click', function(e) { - e.preventDefault(); - let idx = parseInt($(this).data('idx')); - let checkbox = checklist_popup.fields_dict.checklist_html.$wrapper.find(`.checklist-checkbox[data-idx="${idx}"]`); - checkbox.prop('checked', !checkbox.prop('checked')).trigger('change'); - }); + if (item) { + item.completed = is_checked ? 1 : 0; + } + + update_progress(checklist_popup, checklist_items); + }); update_progress(checklist_popup, checklist_items); + checklist_popup.show(); } function generate_checklist_html(items, template_name) { - let completed_count = items.filter(i => i.completed === 1).length; + let completed_count = items.filter( + (i) => i.completed === 1 + ).length; + let total = items.length; - let percentage = total > 0 ? Math.round((completed_count / total) * 100) : 0; + + let percentage = + total > 0 + ? Math.round((completed_count / total) * 100) + : 0; let html = ` -
-
-
-

${frappe.utils.escape_html(template_name)}

- - ${completed_count}/${total} Tasks - -
-
-
-
-
-
- ${percentage}% Complete +
+

${template_name}

+ +
+
+ ${percentage}%
- -
`; - items.forEach((item, index) => { - let checked = item.completed ? 'checked' : ''; - let completed_class = item.completed ? 'completed' : ''; - let text_style = item.completed ? 'text-decoration: line-through; color: #888;' : 'color: #495057;'; - + items.forEach((item) => { html += ` -
-
- -
-
-
- - `; + html += `
`; return html; } function update_progress(dialog, items) { - let completed = items.filter(i => i.completed === 1).length; + let completed = items.filter( + (i) => i.completed === 1 + ).length; + let total = items.length; - let percentage = total > 0 ? Math.round((completed / total) * 100) : 0; - dialog.$wrapper.find('.progress-bar').css('width', percentage + '%').attr('aria-valuenow', percentage); - dialog.$wrapper.find('.progress-percentage').text(percentage + '%'); - dialog.$wrapper.find('.template-header span').text(`${completed}/${total} Tasks`); + let percentage = + total > 0 + ? Math.round((completed / total) * 100) + : 0; + + dialog.$wrapper + .find(".progress-bar") + .css("width", percentage + "%") + .text(percentage + "%"); } function save_checklist(frm, checklist_items, dialog) { - // Clear existing checklist items - frm.clear_table('task_checklist_template'); + frm.clear_table("task_checklist_template"); + + checklist_items.forEach((item) => { + let row = frm.add_child("task_checklist_template"); - // Add updated checklist items - checklist_items.forEach(item => { - let row = frm.add_child('task_checklist_template'); row.checklist_item = item.checklist_item; row.completed = item.completed; - row.completed_on = item.completed ? frappe.datetime.now_date() : null; - row.completed_by = item.completed ? frappe.session.user : null; + row.completed_on = item.completed + ? frappe.datetime.now_date() + : null; + + row.completed_by = item.completed + ? frappe.session.user + : null; + row.idx = item.idx; }); - // Refresh the table - frm.refresh_field('task_checklist_template'); - - // Check if all items are completed - let all_completed = checklist_items.every(item => item.completed === 1); - - if (all_completed && frm.doc.status !== 'Completed') { - frappe.show_alert({ - message: __('🎉 All checklist items completed! Task marked as complete.'), - indicator: 'green' - }, 5); - frm.set_value('status', 'Completed'); - // Set completed_on to current date - frm.set_value('completed_on', frappe.datetime.now_date()); + frm.refresh_field("task_checklist_template"); + + let all_completed = checklist_items.every( + (item) => item.completed === 1 + ); + + if (all_completed && frm.doc.status !== "Completed") { + frappe.show_alert( + { + message: __( + "🎉 All checklist items completed! Task marked as complete." + ), + indicator: "green", + }, + 5 + ); + + frm.set_value("status", "Completed"); + + frm.set_value( + "completed_on", + frappe.datetime.now_date() + ); } - // Save the document frm.save().then(() => { - frappe.show_alert({ - message: __('✓ Checklist saved successfully'), - indicator: 'green' - }, 3); + frappe.show_alert( + { + message: __("✓ Checklist saved successfully"), + indicator: "green", + }, + 3 + ); + dialog.hide(); }); -} +} \ No newline at end of file