From 5500283a92f59fc91d056503380766c5a5b24b35 Mon Sep 17 00:00:00 2001 From: Jishnu-Epics Date: Sat, 21 Jun 2025 10:44:18 +0530 Subject: [PATCH] feat:updated all changes --- virtual_pro/events/quotation.py | 228 +++++++++++++++++- virtual_pro/events/sales_invoice.py | 187 +++++++++++++- virtual_pro/events/sales_order.py | 194 --------------- virtual_pro/hooks.py | 18 +- virtual_pro/public/js/quotation.js | 15 ++ virtual_pro/public/js/task_list.js | 179 ++++++++++++++ .../doctype/child_steps/child_steps.json | 7 +- .../virtual_pro/doctype/enquiry/enquiry.js | 19 +- .../virtual_pro/doctype/enquiry/enquiry.json | 22 +- .../virtual_pro/doctype/enquiry/enquiry.py | 39 ++- .../doctype/enquiry/enquiry_dashboard.py | 3 +- .../doctype/service_list/__init__.py | 0 .../doctype/service_list/service_list.js | 8 + .../doctype/service_list/service_list.json | 44 ++++ .../doctype/service_list/service_list.py | 9 + .../doctype/service_list/test_service_list.py | 9 + .../service_request/service_request.js | 74 +++++- .../service_request/service_request.json | 23 +- .../service_request/service_request.py | 13 +- .../service_request_dashboard.py | 2 +- .../service_request/service_request_list.js | 6 +- 21 files changed, 855 insertions(+), 244 deletions(-) delete mode 100644 virtual_pro/events/sales_order.py create mode 100644 virtual_pro/public/js/quotation.js create mode 100644 virtual_pro/public/js/task_list.js create mode 100644 virtual_pro/virtual_pro/doctype/service_list/__init__.py create mode 100644 virtual_pro/virtual_pro/doctype/service_list/service_list.js create mode 100644 virtual_pro/virtual_pro/doctype/service_list/service_list.json create mode 100644 virtual_pro/virtual_pro/doctype/service_list/service_list.py create mode 100644 virtual_pro/virtual_pro/doctype/service_list/test_service_list.py diff --git a/virtual_pro/events/quotation.py b/virtual_pro/events/quotation.py index 494f5db..c11bc27 100644 --- a/virtual_pro/events/quotation.py +++ b/virtual_pro/events/quotation.py @@ -1,10 +1,230 @@ import frappe +from frappe import _ +from frappe.model.mapper import get_mapped_doc +from frappe.utils import flt, getdate, nowdate def update_service_request(doc, method): - sp = frappe.get_doc("Service Request", doc.custom_service_request) + sp = frappe.get_doc("Enquiry", doc.custom_enquiry) if doc.docstatus == 1: - sp.db_set('status', "To Sales Order") + sp.db_set('quotation', doc.name) elif doc.docstatus == 2: - sp.db_set('status', "To Quotation") - sp.save() \ No newline at end of file + sp.db_set('quotation', "") + sp.save() + + +@frappe.whitelist() +def make_sales_invoice(source_name, target_doc=None): + return _make_sales_invoice(source_name, target_doc) + + +def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): + customer = _make_customer(source_name, ignore_permissions) + + def set_missing_values(source, target): + if customer: + target.customer = customer.name + target.customer_name = customer.customer_name + + target.flags.ignore_permissions = ignore_permissions + target.run_method("set_missing_values") + target.run_method("calculate_taxes_and_totals") + + def update_item(obj, target, source_parent): + target.cost_center = None + target.stock_qty = flt(obj.qty) * flt(obj.conversion_factor) + + balance_qty = get_quotation_item_balance_qty(obj.name) + if balance_qty <= 0: + return None + target.qty = min(obj.qty, balance_qty) # Use balance or original qty, whichever is smaller + + def condition_check(row): + # Only include items that have balance quantity and are not alternative + if row.is_alternative: + return False + balance_qty = get_quotation_item_balance_qty(row.name) + return balance_qty > 0 + + doclist = get_mapped_doc( + "Quotation", + source_name, + { + "Quotation": {"doctype": "Sales Invoice", "validation": {"docstatus": ["=", 1]}}, + "Quotation Item": { + "doctype": "Sales Invoice Item", + "field_map": {"parent": "custom_quotation", "name": "custom_quotation_item"}, # Fixed typo + "postprocess": update_item, + "condition": condition_check, + }, + "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True}, + "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, + }, + target_doc, + set_missing_values, + ignore_permissions=ignore_permissions, + ) + + return doclist + + +def get_quotation_item_balance_qty(quotation_item_id): + quotation_item = frappe.get_doc("Quotation Item", quotation_item_id) + + invoiced_qty = frappe.db.sql(""" + SELECT SUM(qty) as total_qty + FROM `tabSales Invoice Item` + WHERE custom_quotation_item = %s + AND docstatus = 1 + """, (quotation_item_id,), as_dict=True) + + total_invoiced = invoiced_qty[0].total_qty or 0 + balance_qty = quotation_item.qty - total_invoiced + + return max(0, balance_qty) + + +def _make_customer(source_name, ignore_permissions=False): + quotation = frappe.db.get_value( + "Quotation", + source_name, + ["order_type", "quotation_to", "party_name", "customer_name"], + as_dict=1, + ) + + if quotation.quotation_to == "Customer": + return frappe.get_doc("Customer", quotation.party_name) + + existing_customer = None + if quotation.quotation_to == "Lead": + existing_customer = frappe.db.get_value("Customer", {"lead_name": quotation.party_name}) + elif quotation.quotation_to == "Prospect": + existing_customer = frappe.db.get_value("Customer", {"prospect_name": quotation.party_name}) + + if existing_customer: + return frappe.get_doc("Customer", existing_customer) + + if quotation.quotation_to == "Lead": + return create_customer_from_lead(quotation.party_name, ignore_permissions=ignore_permissions) + elif quotation.quotation_to == "Prospect": + return create_customer_from_prospect(quotation.party_name, ignore_permissions=ignore_permissions) + + return None + + +def create_customer_from_lead(lead_name, ignore_permissions=False): + from erpnext.crm.doctype.lead.lead import _make_customer + + customer = _make_customer(lead_name, ignore_permissions=ignore_permissions) + customer.flags.ignore_permissions = ignore_permissions + + try: + customer.insert() + return customer + except frappe.MandatoryError as e: + handle_mandatory_error(e, customer, lead_name) + + +def create_customer_from_prospect(prospect_name, ignore_permissions=False): + from erpnext.crm.doctype.prospect.prospect import make_customer as make_customer_from_prospect + + customer = make_customer_from_prospect(prospect_name) + customer.flags.ignore_permissions = ignore_permissions + + try: + customer.insert() + return customer + except frappe.MandatoryError as e: + handle_mandatory_error(e, customer, prospect_name) + + +def handle_mandatory_error(e, customer, lead_name): + from frappe.utils import get_link_to_form + + mandatory_fields = e.args[0].split(":")[1].split(",") + mandatory_fields = [_(customer.meta.get_label(field.strip())) for field in mandatory_fields] + + frappe.local.message_log = [] + message = _("Could not auto create Customer due to the following missing mandatory field(s):") + "
" + message += "
" + message += _("Please create Customer from Lead {0}.").format(get_link_to_form("Lead", lead_name)) + + frappe.throw(message, title=_("Mandatory Missing")) + + +def validate_quotation_balance_qty(doc, method): + """Validate that sales invoice items don't exceed quotation balance""" + for item in doc.items: + if item.custom_quotation and item.custom_quotation_item: + quotation_item = frappe.get_doc("Quotation Item", item.custom_quotation_item) + + invoiced_qty = frappe.db.sql(""" + SELECT SUM(qty) as total_qty + FROM `tabSales Invoice Item` + WHERE custom_quotation_item = %s + AND docstatus = 1 + AND name != %s + """, (item.custom_quotation_item, item.name), as_dict=True) + + total_invoiced = invoiced_qty[0].total_qty or 0 + balance_qty = quotation_item.qty - total_invoiced + + if item.qty > balance_qty: + frappe.throw(f"Row {item.idx}: Item {item.item_code} - Cannot invoice {item.qty} qty. Balance quantity available: {balance_qty}") + + +def update_quotation_status(doc, method): + processed_quotations = set() + + for item in doc.items: + if item.custom_quotation and item.custom_quotation not in processed_quotations: + quotation = frappe.get_doc("Quotation", item.custom_quotation) + processed_quotations.add(item.custom_quotation) + + if doc.docstatus == 1: + fully_converted_items = 0 + + for quotation_item in quotation.items: + converted_qty = frappe.db.sql(""" + SELECT SUM(qty) as total_qty + FROM `tabSales Invoice Item` + WHERE custom_quotation_item = %s + AND docstatus = 1 + """, (quotation_item.name,), as_dict=True) + + total_converted = converted_qty[0].total_qty or 0 + + if total_converted >= quotation_item.qty: + fully_converted_items += 1 + + total_quotation_items = len(quotation.items) + if fully_converted_items >= total_quotation_items: + quotation.db_set('status', "Ordered") + elif fully_converted_items > 0: + quotation.db_set('status', "Partially Ordered") + + elif doc.docstatus == 2: + fully_converted_items = 0 + + for quotation_item in quotation.items: + converted_qty = frappe.db.sql(""" + SELECT SUM(qty) as total_qty + FROM `tabSales Invoice Item` + WHERE custom_quotation_item = %s + AND docstatus = 1 + """, (quotation_item.name,), as_dict=True) + + total_converted = converted_qty[0].total_qty or 0 + + if total_converted >= quotation_item.qty: + fully_converted_items += 1 + + total_quotation_items = len(quotation.items) + if fully_converted_items >= total_quotation_items: + quotation.db_set('status', "Ordered") + elif fully_converted_items > 0: + quotation.db_set('status', "Partially Ordered") + else: + quotation.db_set('status', "Open") + + quotation.save() \ No newline at end of file diff --git a/virtual_pro/events/sales_invoice.py b/virtual_pro/events/sales_invoice.py index d041f1d..a2cf561 100644 --- a/virtual_pro/events/sales_invoice.py +++ b/virtual_pro/events/sales_invoice.py @@ -1,10 +1,195 @@ import frappe +from frappe import _ def update_service_request(doc, method): sp = frappe.get_doc("Service Request", doc.custom_service_request) if doc.docstatus == 1: sp.db_set('status', "Completed") + sp.db_set('sales_invoice', doc.name) elif doc.docstatus == 2: sp.db_set('status', "To Sales Invoice") - sp.save() \ No newline at end of file + sp.db_set('sales_invoice', " ") + sp.save() + + +def create_tasks(doc, method): + + if not doc.custom_enquiry: + frappe.log_error("No custom_service_request found") + return + + # Fix: Use get_value with proper field name + step = frappe.db.get_value("Enquiry", doc.custom_enquiry, "services") + if not step: + frappe.log_error(f"No services found for Service Request: {doc.custom_enquiry}") + return + + + try: + service = frappe.get_doc("Services", step) + except Exception as e: + frappe.log_error(f"Error loading service doc: {e}") + return + + def get_users_by_role(role): + if not role: + return [] + + # Get only actual users, not reports or other doctypes + users = frappe.get_all("Has Role", + filters={ + "role": role, + "parenttype": "User" # This ensures we only get User records + }, + fields=["parent"] + ) + + user_list = [] + for user_doc in users: + user = user_doc.parent + if frappe.db.exists("User", user): + user_data = frappe.db.get_value("User", user, ["enabled", "user_type"], as_dict=True) + if user_data and user_data.enabled == 1 and user_data.user_type == "System User": + user_list.append(user) + + frappe.log_error(f"Active users for role {role}: {user_list}") + return user_list + + def get_user_by_email(email): + """Check if email exists as an active user and return the user""" + if not email: + return None + + try: + # Check if user exists with this email + if frappe.db.exists("User", email): + user_data = frappe.db.get_value("User", email, ["enabled", "user_type"], as_dict=True) + if user_data and user_data.enabled == 1 and user_data.user_type == "System User": + frappe.log_error(f"Found active user for email: {email}") + return email + else: + frappe.log_error(f"User {email} exists but is not active or not system user") + return None + else: + frappe.log_error(f"No user found with email: {email}") + return None + except Exception as e: + frappe.log_error(f"Error checking user for email {email}: {e}") + return None + + def get_assigned_users(step_row): + """Get users to assign based on email or role""" + users = [] + + # First check if there's an email (CC field) in the step + if hasattr(step_row, 'cc') and step_row.cc: + user = get_user_by_email(step_row.cc) + if user: + users.append(user) + frappe.log_error(f"Assigned user by email: {user}") + else: + frappe.log_error(f"Email {step_row.cc} not found as user, falling back to role assignment") + + # If no user found by email, fall back to role assignment + if not users and hasattr(step_row, 'assign_by_role') and step_row.assign_by_role: + users = get_users_by_role(step_row.assign_by_role) + frappe.log_error(f"Assigned users by role {step_row.assign_by_role}: {users}") + + return users + + def create_task_and_todos(parent_task_name=None): + if hasattr(service, 'parent_steps') and service.parent_steps: + + for parent in service.parent_steps: + request = frappe.get_doc("Service Request", {"enquiry":doc.custom_enquiry}) + + parent_users = get_assigned_users(parent) + + # Create parent task + try: + parent_task = frappe.get_doc({ + "doctype": "Task", + "subject": parent.step_name, + "status": "Open", + "project": doc.project if hasattr(doc, 'project') and doc.project else None, + "description": f"Task created from Service {parent.step_name}", + "custom_service_request": request.name, + "is_group": 1, + }) + parent_task.insert(ignore_permissions=True) + frappe.log_error(f"Parent task created: {parent_task.name}") + + # Create ToDos for parent task + for user in parent_users: + try: + todo = frappe.get_doc({ + "doctype": "ToDo", + "description": parent_task.subject, + "reference_type": "Task", + "reference_name": parent_task.name, + "status": "Open", + "assigned_by": frappe.session.user, + "allocated_to": user + }) + todo.insert(ignore_permissions=True) + frappe.log_error(f"ToDo created for user {user}") + except Exception as e: + frappe.log_error(f"Error creating ToDo for user {user}: {e}") + + # Process child steps for this parent + if hasattr(service, 'child_steps') and service.child_steps: + for child in service.child_steps: + if hasattr(child, 'parent_step') and child.parent_step == parent.step_name: + create_child_task(child, parent_task.name) + + except Exception as e: + frappe.log_error(f"Error creating parent task {parent.step_name}: {e}") + + elif hasattr(service, 'child_steps') and service.child_steps: + frappe.log_error(f"Processing {len(service.child_steps)} child steps without parents") + + for child in service.child_steps: + if not hasattr(child, 'parent_step') or not child.parent_step: + create_child_task(child, parent_task_name) + + def create_child_task(child, parent_task_name=None): + users = get_assigned_users(child) + request = frappe.get_doc("Service Request", {"enquiry":doc.custom_enquiry}) + + task_doc = { + "doctype": "Task", + "subject": child.step_name, + "status": "Open", + "description": f"Task created from Service {child.step_name}", + "custom_service_request": request.name, + } + + if hasattr(doc, 'project') and doc.project: + task_doc["project"] = doc.project + + if parent_task_name: + task_doc["parent_task"] = parent_task_name + + task = frappe.get_doc(task_doc) + task.insert(ignore_permissions=True) + + for user in users: + try: + todo = frappe.get_doc({ + "doctype": "ToDo", + "description": task.subject, + "reference_type": "Task", + "reference_name": task.name, + "status": "Open", + "assigned_by": frappe.session.user, + "allocated_to": user + }) + todo.insert(ignore_permissions=True) + frappe.log_error(f"ToDo created for user {user} on task {task.name}") + except Exception as e: + frappe.log_error(f"Error creating ToDo for user {user} on task {task.name}: {e}") + + create_task_and_todos() + + frappe.msgprint(_("Tasks and ToDos created and assigned by email/role.")) \ No newline at end of file diff --git a/virtual_pro/events/sales_order.py b/virtual_pro/events/sales_order.py deleted file mode 100644 index d42042a..0000000 --- a/virtual_pro/events/sales_order.py +++ /dev/null @@ -1,194 +0,0 @@ -import frappe -from frappe import _ - -def update_service_request(doc, method): - sp = frappe.get_doc("Service Request", doc.custom_service_request) - if doc.docstatus == 1: - sp.db_set('status', "To Sales Invoice") - elif doc.docstatus == 2: - sp.db_set('status', "To Sales Order") - sp.save() - - -def create_tasks(doc, method): - frappe.log_error(f"create_tasks called for doc: {doc.name}") - - if not doc.custom_service_request: - frappe.log_error("No custom_service_request found") - return - - # Fix: Use get_value with proper field name - step = frappe.db.get_value("Service Request", doc.custom_service_request, "services") - if not step: - frappe.log_error(f"No services found for Service Request: {doc.custom_service_request}") - return - - frappe.log_error(f"Found service: {step}") - - try: - service = frappe.get_doc("Services", step) - frappe.log_error(f"Service doc loaded: {service.name}") - except Exception as e: - frappe.log_error(f"Error loading service doc: {e}") - return - - def get_users_by_role(role): - if not role: - return [] - - # Get only actual users, not reports or other doctypes - users = frappe.get_all("Has Role", - filters={ - "role": role, - "parenttype": "User" # This ensures we only get User records - }, - fields=["parent"] - ) - - user_list = [] - for user_doc in users: - user = user_doc.parent - if frappe.db.exists("User", user): - user_data = frappe.db.get_value("User", user, ["enabled", "user_type"], as_dict=True) - if user_data and user_data.enabled == 1 and user_data.user_type == "System User": - user_list.append(user) - - frappe.log_error(f"Active users for role {role}: {user_list}") - return user_list - - def get_user_by_email(email): - """Check if email exists as an active user and return the user""" - if not email: - return None - - try: - # Check if user exists with this email - if frappe.db.exists("User", email): - user_data = frappe.db.get_value("User", email, ["enabled", "user_type"], as_dict=True) - if user_data and user_data.enabled == 1 and user_data.user_type == "System User": - frappe.log_error(f"Found active user for email: {email}") - return email - else: - frappe.log_error(f"User {email} exists but is not active or not system user") - return None - else: - frappe.log_error(f"No user found with email: {email}") - return None - except Exception as e: - frappe.log_error(f"Error checking user for email {email}: {e}") - return None - - def get_assigned_users(step_row): - """Get users to assign based on email or role""" - users = [] - - # First check if there's an email (CC field) in the step - if hasattr(step_row, 'cc') and step_row.cc: - user = get_user_by_email(step_row.cc) - if user: - users.append(user) - frappe.log_error(f"Assigned user by email: {user}") - else: - frappe.log_error(f"Email {step_row.cc} not found as user, falling back to role assignment") - - # If no user found by email, fall back to role assignment - if not users and hasattr(step_row, 'assign_by_role') and step_row.assign_by_role: - users = get_users_by_role(step_row.assign_by_role) - frappe.log_error(f"Assigned users by role {step_row.assign_by_role}: {users}") - - return users - - def create_task_and_todos(parent_task_name=None): - if hasattr(service, 'parent_steps') and service.parent_steps: - - for parent in service.parent_steps: - - parent_users = get_assigned_users(parent) - - # Create parent task - try: - parent_task = frappe.get_doc({ - "doctype": "Task", - "subject": parent.step_name, - "status": "Open", - "project": doc.project if hasattr(doc, 'project') and doc.project else None, - "description": f"Task created from Service {parent.step_name}", - "custom_service_request": doc.custom_service_request, - "is_group": 1, - }) - parent_task.insert(ignore_permissions=True) - frappe.log_error(f"Parent task created: {parent_task.name}") - - # Create ToDos for parent task - for user in parent_users: - try: - todo = frappe.get_doc({ - "doctype": "ToDo", - "description": parent_task.subject, - "reference_type": "Task", - "reference_name": parent_task.name, - "status": "Open", - "assigned_by": frappe.session.user, - "allocated_to": user - }) - todo.insert(ignore_permissions=True) - frappe.log_error(f"ToDo created for user {user}") - except Exception as e: - frappe.log_error(f"Error creating ToDo for user {user}: {e}") - - # Process child steps for this parent - if hasattr(service, 'child_steps') and service.child_steps: - for child in service.child_steps: - if hasattr(child, 'parent_step') and child.parent_step == parent.step_name: - create_child_task(child, parent_task.name) - - except Exception as e: - frappe.log_error(f"Error creating parent task {parent.step_name}: {e}") - - elif hasattr(service, 'child_steps') and service.child_steps: - frappe.log_error(f"Processing {len(service.child_steps)} child steps without parents") - - for child in service.child_steps: - if not hasattr(child, 'parent_step') or not child.parent_step: - create_child_task(child, parent_task_name) - - def create_child_task(child, parent_task_name=None): - users = get_assigned_users(child) - - task_doc = { - "doctype": "Task", - "subject": child.step_name, - "status": "Open", - "description": f"Task created from Service {child.step_name}", - "custom_service_request": doc.custom_service_request, - } - - if hasattr(doc, 'project') and doc.project: - task_doc["project"] = doc.project - - if parent_task_name: - task_doc["parent_task"] = parent_task_name - - task = frappe.get_doc(task_doc) - task.insert(ignore_permissions=True) - - # Create ToDos for assigned users - for user in users: - try: - todo = frappe.get_doc({ - "doctype": "ToDo", - "description": task.subject, - "reference_type": "Task", - "reference_name": task.name, - "status": "Open", - "assigned_by": frappe.session.user, - "allocated_to": user - }) - todo.insert(ignore_permissions=True) - frappe.log_error(f"ToDo created for user {user} on task {task.name}") - except Exception as e: - frappe.log_error(f"Error creating ToDo for user {user} on task {task.name}: {e}") - - create_task_and_todos() - - frappe.msgprint(_("Tasks and ToDos created and assigned by email/role.")) \ No newline at end of file diff --git a/virtual_pro/hooks.py b/virtual_pro/hooks.py index e67fece..c6e26cb 100644 --- a/virtual_pro/hooks.py +++ b/virtual_pro/hooks.py @@ -43,8 +43,8 @@ # page_js = {"page" : "public/js/file.js"} # include js in doctype views -# doctype_js = {"doctype" : "public/js/doctype.js"} -# doctype_list_js = {"doctype" : "public/js/doctype_list.js"} +doctype_js = {"Quotation" : "public/js/quotation.js",} +doctype_list_js = {"Task" : "public/js/task_list.js"} # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} @@ -142,15 +142,13 @@ "on_submit": "virtual_pro.events.quotation.update_service_request", "on_cancel": "virtual_pro.events.quotation.update_service_request" }, -"Sales Order": { - "on_submit": ["virtual_pro.events.sales_order.update_service_request", - "virtual_pro.events.sales_order.create_tasks"], - - "on_cancel": "virtual_pro.events.sales_order.update_service_request" -}, "Sales Invoice": { - "on_submit": "virtual_pro.events.sales_invoice.update_service_request", - "on_cancel": "virtual_pro.events.sales_invoice.update_service_request" + "on_submit": ["virtual_pro.events.sales_order.create_tasks", + "virtual_pro.events.sales_invoice.update_service_request", + "virtual_pro.events.quotation.update_quotation_status"], + + "on_cancel": ["virtual_pro.events.sales_invoice.update_service_request", + "virtual_pro.events.quotation.update_quotation_status"] }, "ToDo": { "before_save":"virtual_pro.events.todo.before_save" diff --git a/virtual_pro/public/js/quotation.js b/virtual_pro/public/js/quotation.js new file mode 100644 index 0000000..8c4529b --- /dev/null +++ b/virtual_pro/public/js/quotation.js @@ -0,0 +1,15 @@ +frappe.ui.form.on('Quotation', { + refresh: function(frm) { + if (frm.doc.docstatus === 1 && frm.doc.status !== "Closed" && frm.doc.status !== "Lost" && frm.doc.status !== "Ordered") { + frm.add_custom_button("Sales Invoice", function() { + frappe.model.open_mapped_doc({ + method: "virtual_pro.events.quotation.make_sales_invoice", + frm: frm + }); + }, __('Create')); + } + setTimeout(() => { + frm.remove_custom_button('Sales Order', 'Create'); + }, 500); + } +}); \ No newline at end of file diff --git a/virtual_pro/public/js/task_list.js b/virtual_pro/public/js/task_list.js new file mode 100644 index 0000000..dd8861f --- /dev/null +++ b/virtual_pro/public/js/task_list.js @@ -0,0 +1,179 @@ +frappe.listview_settings["Task"] = { + add_fields: [ + "project", + "status", + "priority", + "exp_start_date", + "exp_end_date", + "subject", + "progress", + "depends_on_tasks", + ], + filters: [["status", "=", "Open"]], + + onload: function (listview) { + var method = "erpnext.projects.doctype.task.task.set_multiple_status"; + + listview.page.add_menu_item(__("Set as Open"), function () { + listview.call_for_selected_items(method, { status: "Open" }); + }); + + listview.page.add_menu_item(__("Set as Completed"), function () { + listview.call_for_selected_items(method, { status: "Completed" }); + }); + }, + + get_indicator: function (doc) { + var colors = { + Open: "orange", + Overdue: "red", + "Pending Review": "orange", + Working: "orange", + Completed: "green", + Cancelled: "dark grey", + Template: "blue", + }; + return [__(doc.status), colors[doc.status], "status,=," + doc.status]; + }, + + gantt_custom_popup_html: function (ganttobj, task) { + let html = ` + + ${ganttobj.name} + + `; + + if (task.project) { + html += `

${__("Project")}: + + ${task.project} + +

`; + } + html += `

+ ${__("Progress")}: + ${ganttobj.progress}% +

`; + + if (task._assign) { + const assign_list = JSON.parse(task._assign); + const assignment_wrapper = ` + Assigned to: + + ${assign_list.map((user) => frappe.user_info(user).fullname).join(", ")} + + `; + html += assignment_wrapper; + } + + return `
${html}
`; + }, + + // Add Status update button + button: { + show: function (doc) { + return true; // Always show the button + }, + get_label: function () { + return __(' Status'); + }, + get_description: function (doc) { + return __("Update Status of Task: " + doc.name); + }, + action: function (doc) { + console.log("Update Status Clicked for Task:", doc.name); + + // Simple dialog with just status and comment + let d = new frappe.ui.Dialog({ + title: __('Update Task Status: {0}', [doc.name]), + fields: [ + { + label: 'New Status', + fieldname: 'new_status', + fieldtype: 'Select', + options: [ + "Open", + "Working", + "Pending Review", + "Overdue", + "Completed", + "Cancelled" + ], + reqd: 1, + default: get_suggested_next_status(doc.status) + }, + { + label: 'Comments', + fieldname: 'comments', + fieldtype: 'Small Text', + description: 'Add comments about the status change' + } + ], + size: 'small', + primary_action_label: __('Update'), + primary_action: function() { + var data = d.get_values(); + + if (data.new_status === doc.status) { + frappe.msgprint(__("Please select a different status")); + return; + } + + // Update task status + frappe.call({ + method: "erpnext.projects.doctype.task.task.set_multiple_status", + args: { + names: [doc.name], + status: data.new_status + }, + freeze: true, + freeze_message: __("Updating..."), + callback: function(r) { + if (!r.exc) { + // Add comment if provided + if (data.comments) { + frappe.call({ + method: "frappe.desk.form.utils.add_comment", + args: { + reference_doctype: "Task", + reference_name: doc.name, + content: data.comments, + comment_email: frappe.session.user, + comment_by: frappe.session.user_fullname + } + }); + } + + frappe.show_alert({ + message: __("Status updated to {0}", [data.new_status]), + indicator: 'green' + }); + + // Refresh the list + cur_list.refresh(); + d.hide(); + } + } + }); + } + }); + + d.show(); + }, + }, +}; + +// Helper function to suggest next logical status +function get_suggested_next_status(current_status) { + const status_flow = { + "Open": "Working", + "Working": "Pending Review", + "Pending Review": "Completed", + "Overdue": "Working", + "Completed": "Open", + "Cancelled": "Open" + }; + return status_flow[current_status] || "Working"; +} \ No newline at end of file diff --git a/virtual_pro/virtual_pro/doctype/child_steps/child_steps.json b/virtual_pro/virtual_pro/doctype/child_steps/child_steps.json index 57be9b9..fe8bae3 100644 --- a/virtual_pro/virtual_pro/doctype/child_steps/child_steps.json +++ b/virtual_pro/virtual_pro/doctype/child_steps/child_steps.json @@ -34,15 +34,16 @@ }, { "fieldname": "step_name", - "fieldtype": "Data", + "fieldtype": "Link", "in_list_view": 1, - "label": "Step Name" + "label": "Step Name", + "options": "Service List" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-06-17 15:14:02.481320", + "modified": "2025-06-21 08:13:49.956331", "modified_by": "Administrator", "module": "Virtual Pro", "name": "Child Steps", diff --git a/virtual_pro/virtual_pro/doctype/enquiry/enquiry.js b/virtual_pro/virtual_pro/doctype/enquiry/enquiry.js index 0166113..2c1dad7 100644 --- a/virtual_pro/virtual_pro/doctype/enquiry/enquiry.js +++ b/virtual_pro/virtual_pro/doctype/enquiry/enquiry.js @@ -2,12 +2,19 @@ frappe.ui.form.on('Enquiry', { refresh: function(frm) { frm.clear_custom_buttons(); - // Show "Change Status" button only if submitted if (frm.doc.docstatus === 1) { frm.add_custom_button('Change Status', () => { show_status_dialog(frm); }); + + frm.add_custom_button("Create Quotation", function() { + frappe.model.open_mapped_doc({ + method: "virtual_pro.virtual_pro.doctype.enquiry.enquiry.make_quotation", + frm: frm + }); + },__('Create')); } + cur_frm.page.set_inner_btn_group_as_primary(__("Create")); } }); @@ -31,9 +38,9 @@ function show_status_dialog(frm) { if (values.new_status === 'Lost Enquiry') { d.hide(); show_lost_enquiry_reason_dialog(frm, values.new_status); - } else if (values.new_status === 'Interested') { + } else if (values.new_status === 'Converted') { d.hide(); - update_status(frm, values.new_status, true); // pass flag to create Service Request + update_status(frm, values.new_status, true); // Create Service Request when status is Converted } else { update_status(frm, values.new_status); d.hide(); @@ -137,14 +144,14 @@ function auto_create_service_request(frm) { }, callback: function(r) { if (r.message) { - frappe.show_alert(`Service Request ${r.message.name} created`); + frappe.show_alert(`✅ Service Request ${r.message.name} created successfully`); frm.reload_doc(); } else { - frappe.msgprint("Could not create Service Request."); + frappe.msgprint("❌ Could not create Service Request."); } } }); } } }); -} +} \ No newline at end of file diff --git a/virtual_pro/virtual_pro/doctype/enquiry/enquiry.json b/virtual_pro/virtual_pro/doctype/enquiry/enquiry.json index 7036185..c4b6a9a 100644 --- a/virtual_pro/virtual_pro/doctype/enquiry/enquiry.json +++ b/virtual_pro/virtual_pro/doctype/enquiry/enquiry.json @@ -10,8 +10,10 @@ "company", "customer_company_name", "nationality", + "source", "care_of", "remarks", + "quotation", "column_break_qeup", "posting_date", "services", @@ -102,8 +104,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Nationality", - "options": "Nationality", - "reqd": 1 + "options": "Nationality" }, { "fieldname": "scope", @@ -160,13 +161,28 @@ "fieldtype": "Small Text", "in_list_view": 1, "label": "Lost Reason" + }, + { + "allow_on_submit": 1, + "fieldname": "quotation", + "fieldtype": "Link", + "label": "Quotation Reference", + "options": "Quotation", + "read_only": 1 + }, + { + "fieldname": "source", + "fieldtype": "Select", + "label": "Source", + "options": "\nGoogle Ads\nSEO\nMeta Ads\nReference\nVpro clients", + "reqd": 1 } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-06-19 07:52:59.434677", + "modified": "2025-06-21 08:02:03.388270", "modified_by": "Administrator", "module": "Virtual Pro", "name": "Enquiry", diff --git a/virtual_pro/virtual_pro/doctype/enquiry/enquiry.py b/virtual_pro/virtual_pro/doctype/enquiry/enquiry.py index 4b04b6a..0bf2ec0 100644 --- a/virtual_pro/virtual_pro/doctype/enquiry/enquiry.py +++ b/virtual_pro/virtual_pro/doctype/enquiry/enquiry.py @@ -1,9 +1,46 @@ # Copyright (c) 2025, sammish and contributors # For license information, please see license.txt -# import frappe +import frappe from frappe.model.document import Document +from frappe.model.mapper import get_mapped_doc class Enquiry(Document): pass + +@frappe.whitelist() +def make_quotation(source_name, target_doc=None): + + def set_missing_values(source, target): + + source_data = frappe.get_doc("Services", source.services) + + for item in source_data.service_items: + target.append("items", { + 'item_code': item.item_name, + 'uom': item.uom, + 'qty': item.qty, + 'price_list_rate': item.default_rate, + 'rate': item.default_rate, + }) + + doc = get_mapped_doc( + "Enquiry", + source_name, + { + "Enquiry": { + "doctype": "Quotation", + "field_map": { + "customer_company_name": "party_name", + "posting_date": "transaction_date", + } + } + }, + target_doc, + set_missing_values + ) + + + doc.insert(ignore_permissions=True) + return doc \ No newline at end of file diff --git a/virtual_pro/virtual_pro/doctype/enquiry/enquiry_dashboard.py b/virtual_pro/virtual_pro/doctype/enquiry/enquiry_dashboard.py index 79a4e57..5d99057 100644 --- a/virtual_pro/virtual_pro/doctype/enquiry/enquiry_dashboard.py +++ b/virtual_pro/virtual_pro/doctype/enquiry/enquiry_dashboard.py @@ -4,11 +4,12 @@ def get_data(): return { "fieldname": "enquiry", "non_standard_fieldnames": { + "Quotation": "custom_enquiry", "Service Request": "enquiry" }, "transactions": [ - {"label": _("Reference"), "items": ["Service Request"]}, + {"label": _("Reference"), "items": ["Service Request","Quotation"]}, ] } \ No newline at end of file diff --git a/virtual_pro/virtual_pro/doctype/service_list/__init__.py b/virtual_pro/virtual_pro/doctype/service_list/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/virtual_pro/virtual_pro/doctype/service_list/service_list.js b/virtual_pro/virtual_pro/doctype/service_list/service_list.js new file mode 100644 index 0000000..e129c3c --- /dev/null +++ b/virtual_pro/virtual_pro/doctype/service_list/service_list.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, sammish and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Service List", { +// refresh(frm) { + +// }, +// }); diff --git a/virtual_pro/virtual_pro/doctype/service_list/service_list.json b/virtual_pro/virtual_pro/doctype/service_list/service_list.json new file mode 100644 index 0000000..619485f --- /dev/null +++ b/virtual_pro/virtual_pro/doctype/service_list/service_list.json @@ -0,0 +1,44 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-06-21 08:12:21.096312", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "service" + ], + "fields": [ + { + "fieldname": "service", + "fieldtype": "Data", + "label": "Service" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-06-21 08:12:54.824379", + "modified_by": "Administrator", + "module": "Virtual Pro", + "name": "Service List", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/virtual_pro/virtual_pro/doctype/service_list/service_list.py b/virtual_pro/virtual_pro/doctype/service_list/service_list.py new file mode 100644 index 0000000..f91dbce --- /dev/null +++ b/virtual_pro/virtual_pro/doctype/service_list/service_list.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, sammish and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class ServiceList(Document): + pass diff --git a/virtual_pro/virtual_pro/doctype/service_list/test_service_list.py b/virtual_pro/virtual_pro/doctype/service_list/test_service_list.py new file mode 100644 index 0000000..20ab251 --- /dev/null +++ b/virtual_pro/virtual_pro/doctype/service_list/test_service_list.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, sammish and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestServiceList(FrappeTestCase): + pass diff --git a/virtual_pro/virtual_pro/doctype/service_request/service_request.js b/virtual_pro/virtual_pro/doctype/service_request/service_request.js index dd3f7b9..c920951 100644 --- a/virtual_pro/virtual_pro/doctype/service_request/service_request.js +++ b/virtual_pro/virtual_pro/doctype/service_request/service_request.js @@ -6,11 +6,16 @@ frappe.ui.form.on("Service Request", { refresh: function(frm) { frm.toggle_display("create_project", !frm.doc.project_id); - if (frm.doc.docstatus === 1 && frm.doc.status === "To Quotation") { - frm.add_custom_button("Create Quotation", function() { + if (frm.doc.docstatus === 1 && frm.doc.status === "To Sales Invoice") { + frm.add_custom_button("Sales Invoice", function() { + if (frm.doc.quotation) { + create_sales_invoice_from_quotation(frm); + }else { create_quotation_from_service_request(frm); - }); + } + },__('Create')); } + cur_frm.page.set_inner_btn_group_as_primary(__("Create")); }, create_project: function(frm) { @@ -49,10 +54,71 @@ function create_project_call(frm) { function create_quotation_from_service_request(frm) { frappe.model.open_mapped_doc({ - method: "virtual_pro.virtual_pro.doctype.service_request.service_request.mak_quotation", + method: "virtual_pro.virtual_pro.doctype.service_request.service_request.mak_sales_invoice", args: { source_name: frm.doc.name }, frm: frm }); } + +function create_sales_invoice_from_quotation(frm) { + // Check if quotation exists + if (!frm.doc.quotation) { + frappe.msgprint({ + title: __('Missing Quotation'), + message: __('No quotation linked to this Service Request'), + indicator: 'red' + }); + return; + } + + frappe.call({ + method: "frappe.client.get", + args: { + doctype: "Quotation", + name: frm.doc.quotation + }, + callback: function(r) { + if (r.message) { + let quotation = r.message; + + // Check if quotation is submitted + if (quotation.docstatus !== 1) { + frappe.msgprint({ + title: __('Invalid Quotation'), + message: __('Quotation {0} is not submitted', [frm.doc.quotation]), + indicator: 'orange' + }); + return; + } + + // Check quotation status + if (quotation.status === 'Ordered' || quotation.status === 'Closed' || quotation.status === 'Lost') { + frappe.msgprint({ + title: __('Quotation Status'), + message: __('Quotation {0} status is {1}. Cannot create Sales Invoice', [frm.doc.quotation, quotation.status]), + indicator: 'orange' + }); + return; + } + + // Create Sales Invoice from Quotation + frappe.model.open_mapped_doc({ + method: "virtual_pro.events.quotation.make_sales_invoice", + source_name: frm.doc.quotation, + get_query_filters: { + company: frm.doc.company || frappe.defaults.get_default("Company") + } + }); + + } else { + frappe.msgprint({ + title: __('Quotation Not Found'), + message: __('Quotation {0} does not exist', [frm.doc.quotation]), + indicator: 'red' + }); + } + } + }); +} diff --git a/virtual_pro/virtual_pro/doctype/service_request/service_request.json b/virtual_pro/virtual_pro/doctype/service_request/service_request.json index 29567b8..78d9baf 100644 --- a/virtual_pro/virtual_pro/doctype/service_request/service_request.json +++ b/virtual_pro/virtual_pro/doctype/service_request/service_request.json @@ -11,6 +11,7 @@ "customer_company_name", "remarks", "enquiry", + "quotation", "column_break_qumr", "posting_date", "services", @@ -19,7 +20,8 @@ "project_id", "project_name", "more_tab", - "status" + "status", + "sales_invoice" ], "fields": [ { @@ -99,10 +101,11 @@ }, { "allow_on_submit": 1, + "default": "To Sales Invoice", "fieldname": "status", "fieldtype": "Select", "label": "status", - "options": "Open\nTo Quotation\nTo Sales Order\nTo Sales Invoice\nCompleted", + "options": "To Sales Invoice\nCompleted", "read_only": 1 }, { @@ -111,13 +114,27 @@ "label": "Enquiry Reference", "options": "Enquiry", "read_only": 1 + }, + { + "fieldname": "quotation", + "fieldtype": "Link", + "label": "Quotation", + "options": "Quotation" + }, + { + "allow_on_submit": 1, + "fieldname": "sales_invoice", + "fieldtype": "Link", + "label": "Sales Invoice", + "options": "Sales Invoice", + "read_only": 1 } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-06-18 10:45:58.739447", + "modified": "2025-06-21 08:04:24.102731", "modified_by": "Administrator", "module": "Virtual Pro", "name": "Service Request", diff --git a/virtual_pro/virtual_pro/doctype/service_request/service_request.py b/virtual_pro/virtual_pro/doctype/service_request/service_request.py index 3a433a9..dd832bd 100644 --- a/virtual_pro/virtual_pro/doctype/service_request/service_request.py +++ b/virtual_pro/virtual_pro/doctype/service_request/service_request.py @@ -8,11 +8,6 @@ class ServiceRequest(Document): def on_submit(self): - self.db_set("status", "To Quotation") - if self.enquiry: - frappe.db.set_value("Enquiry", self.enquiry, "status", "Converted") - frappe.msgprint(f"Enquiry {self.enquiry} marked as Converted.") - if not self.project_id: frappe.throw(_("Project is not set. Please create a project first.")) @@ -53,7 +48,7 @@ def generate_project_name_with_sr(base_name): @frappe.whitelist() -def mak_quotation(source_name, target_doc=None): +def mak_sales_invoice(source_name, target_doc=None): def set_missing_values(source, target): @@ -73,10 +68,12 @@ def set_missing_values(source, target): source_name, { "Service Request": { - "doctype": "Quotation", + "doctype": "Sales Invoice", "field_map": { - "customer_company_name": "party_name", + "customer_company_name": "customer", "project_id": "project", + "posting_date": "due_date", + "enquiry": "custom_enquiry", } } }, diff --git a/virtual_pro/virtual_pro/doctype/service_request/service_request_dashboard.py b/virtual_pro/virtual_pro/doctype/service_request/service_request_dashboard.py index 313358a..80a795c 100644 --- a/virtual_pro/virtual_pro/doctype/service_request/service_request_dashboard.py +++ b/virtual_pro/virtual_pro/doctype/service_request/service_request_dashboard.py @@ -4,7 +4,7 @@ def get_data(): return { "fieldname": "service_request", "non_standard_fieldnames": { - "Quotation": "custom_service_request", + "Sales Invoice": "custom_enquiry", "Task": "custom_service_request" }, diff --git a/virtual_pro/virtual_pro/doctype/service_request/service_request_list.js b/virtual_pro/virtual_pro/doctype/service_request/service_request_list.js index b9ef1ad..d6e9626 100644 --- a/virtual_pro/virtual_pro/doctype/service_request/service_request_list.js +++ b/virtual_pro/virtual_pro/doctype/service_request/service_request_list.js @@ -1,13 +1,9 @@ frappe.listview_settings["Service Request"] = { add_fields: ["status"], get_indicator: function (doc) { - const status = doc.status || "Open"; + const status = doc.status || "To Sales Invoice"; switch (status) { - case "To Quotation": - return ["To Quotation", "blue", "status,=,To Quotation"]; - case "To Sales Order": - return ["To Sales Order", "blue", "status,=,To Sales Order"]; case "To Sales Invoice": return ["To Sales Invoice", "blue", "status,=,To Sales Invoice"]; case "Completed":