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 += "
- " + "
- ".join(mandatory_fields) + "
"
+ 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":