From cfff10463cfe8f435188e2a3f1d963be7428a8d6 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 29 May 2026 12:16:53 +0530 Subject: [PATCH] refactor(quotation): move mapping functions to mapper.py --- erpnext/selling/doctype/quotation/mapper.py | 280 +++++++++++++++++ .../selling/doctype/quotation/quotation.py | 284 +----------------- 2 files changed, 290 insertions(+), 274 deletions(-) create mode 100644 erpnext/selling/doctype/quotation/mapper.py diff --git a/erpnext/selling/doctype/quotation/mapper.py b/erpnext/selling/doctype/quotation/mapper.py new file mode 100644 index 00000000000..166bd5278ab --- /dev/null +++ b/erpnext/selling/doctype/quotation/mapper.py @@ -0,0 +1,280 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import json + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.model.mapper import get_mapped_doc +from frappe.utils import cint, flt, getdate, nowdate + + +@frappe.whitelist() +def make_sales_order( + source_name: str, target_doc: str | Document | None = None, args: str | dict | None = None +): + if not frappe.db.get_singles_value( + "Selling Settings", "allow_sales_order_creation_for_expired_quotation" + ): + quotation = frappe.db.get_value( + "Quotation", source_name, ["transaction_date", "valid_till"], as_dict=1 + ) + if quotation.valid_till and ( + quotation.valid_till < quotation.transaction_date or quotation.valid_till < getdate(nowdate()) + ): + frappe.throw(_("Validity period of this quotation has ended.")) + + return _make_sales_order(source_name, target_doc, args=args) + + +def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, args=None): + if args is None: + args = {} + if isinstance(args, str): + args = json.loads(args) + + customer = _make_customer(source_name, ignore_permissions) + ordered_items = get_ordered_items(source_name) + + selected_rows = [x.get("name") for x in frappe.flags.get("args", {}).get("selected_items", [])] + + # 0 qty is accepted, as the qty uncertain for some items + has_unit_price_items = frappe.db.get_value("Quotation", source_name, "has_unit_price_items") + + def is_unit_price_row(source) -> bool: + return has_unit_price_items and source.qty == 0 + + def set_missing_values(source, target): + if customer: + target.customer = customer.name + target.customer_name = customer.customer_name + + # sales team + if not target.get("sales_team"): + for d in customer.get("sales_team") or []: + target.append( + "sales_team", + { + "sales_person": d.sales_person, + "allocated_percentage": d.allocated_percentage or None, + "commission_rate": d.commission_rate, + }, + ) + + if source.referral_sales_partner: + target.sales_partner = source.referral_sales_partner + target.commission_rate = frappe.get_value( + "Sales Partner", source.referral_sales_partner, "commission_rate" + ) + + 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): + balance_stock_qty = obj.stock_qty - ordered_items.get(obj.name, 0.0) + target.stock_qty = balance_stock_qty if balance_stock_qty > 0 else 0 + target.qty = flt(target.stock_qty) / flt(obj.conversion_factor) + + if obj.against_blanket_order: + target.against_blanket_order = obj.against_blanket_order + target.blanket_order = obj.blanket_order + target.blanket_order_rate = obj.blanket_order_rate + + def can_map_row(item) -> bool: + """ + Row mapping from Quotation to Sales order: + 1. If no selections, map all non-alternative rows (that sum up to the grand total) + 2. If selections: Is Alternative Item/Has Alternative Item: Map if selected and adequate qty + 3. If no selections: Simple row: Map if adequate qty + """ + if not ((item.stock_qty > ordered_items.get(item.name, 0.0)) or is_unit_price_row(item)): + return False + + if not selected_rows: + return not item.is_alternative + + if selected_rows and (item.is_alternative or item.has_alternative_item): + return item.name in selected_rows + + # Simple row + return True + + def select_item(d): + filtered_items = args.get("filtered_children", []) + child_filter = d.name in filtered_items if filtered_items else True + return child_filter + + automatically_fetch_payment_terms = cint( + frappe.get_single_value("Accounts Settings", "automatically_fetch_payment_terms") + ) + + doclist = get_mapped_doc( + "Quotation", + source_name, + { + "Quotation": { + "doctype": "Sales Order", + "validation": {"docstatus": ["=", 1]}, + "field_no_map": ["payment_terms_template"], + }, + "Quotation Item": { + "doctype": "Sales Order Item", + "field_map": {"parent": "prevdoc_docname", "name": "quotation_item"}, + "postprocess": update_item, + "condition": lambda d: can_map_row(d) and select_item(d), + }, + "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, + ) + + if automatically_fetch_payment_terms: + from erpnext.accounts.services.payment_schedule import PaymentScheduleService + + PaymentScheduleService(doclist).set_payment_schedule() + + return doclist + + +@frappe.whitelist() +def make_sales_invoice( + source_name: str, target_doc: str | Document | None = None, args: str | dict | None = None +): + return _make_sales_invoice(source_name, target_doc, args=args) + + +def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, args=None): + if args is None: + args = {} + if isinstance(args, str): + args = json.loads(args) + + 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) + + def select_item(d): + filtered_items = args.get("filtered_children", []) + child_filter = d.name in filtered_items if filtered_items else True + return child_filter + + doclist = get_mapped_doc( + "Quotation", + source_name, + { + "Quotation": {"doctype": "Sales Invoice", "validation": {"docstatus": ["=", 1]}}, + "Quotation Item": { + "doctype": "Sales Invoice Item", + "postprocess": update_item, + "condition": lambda row: not row.is_alternative and select_item(row), + }, + "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 _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) + elif quotation.quotation_to == "CRM Deal": + customer_name = frappe.get_value("Customer", {"crm_deal": quotation.party_name}) + if customer_name: + return frappe.get_doc("Customer", customer_name) + + # Check if a Customer already exists for the Lead or Prospect. + 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 no Customer exists, create a new Customer or Prospect. + 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 get_ordered_items(quotation: str) -> frappe._dict: + return frappe._dict( + frappe.get_all( + "Quotation Item", + {"docstatus": 1, "parent": quotation, "ordered_qty": (">", 0)}, + ["name", "ordered_qty"], + as_list=True, + ) + ) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index e453ae546fd..864106613bb 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -2,16 +2,22 @@ # License: GNU General Public License v3. See license.txt -import json - import frappe from frappe import _ from frappe.model.document import Document -from frappe.model.mapper import get_mapped_doc -from frappe.utils import cint, flt, getdate, nowdate +from frappe.utils import getdate, nowdate from erpnext.controllers.selling_controller import SellingController +from .mapper import ( + _make_sales_order, + create_customer_from_lead, + create_customer_from_prospect, + get_ordered_items, + make_sales_invoice, + make_sales_order, +) + form_grid_templates = {"items": "templates/form_grid/item_grid.html"} @@ -356,137 +362,6 @@ def get_list_context(context=None): return list_context -@frappe.whitelist() -def make_sales_order( - source_name: str, target_doc: str | Document | None = None, args: str | dict | None = None -): - if not frappe.db.get_singles_value( - "Selling Settings", "allow_sales_order_creation_for_expired_quotation" - ): - quotation = frappe.db.get_value( - "Quotation", source_name, ["transaction_date", "valid_till"], as_dict=1 - ) - if quotation.valid_till and ( - quotation.valid_till < quotation.transaction_date or quotation.valid_till < getdate(nowdate()) - ): - frappe.throw(_("Validity period of this quotation has ended.")) - - return _make_sales_order(source_name, target_doc, args=args) - - -def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, args=None): - if args is None: - args = {} - if isinstance(args, str): - args = json.loads(args) - - customer = _make_customer(source_name, ignore_permissions) - ordered_items = get_ordered_items(source_name) - - selected_rows = [x.get("name") for x in frappe.flags.get("args", {}).get("selected_items", [])] - - # 0 qty is accepted, as the qty uncertain for some items - has_unit_price_items = frappe.db.get_value("Quotation", source_name, "has_unit_price_items") - - def is_unit_price_row(source) -> bool: - return has_unit_price_items and source.qty == 0 - - def set_missing_values(source, target): - if customer: - target.customer = customer.name - target.customer_name = customer.customer_name - - # sales team - if not target.get("sales_team"): - for d in customer.get("sales_team") or []: - target.append( - "sales_team", - { - "sales_person": d.sales_person, - "allocated_percentage": d.allocated_percentage or None, - "commission_rate": d.commission_rate, - }, - ) - - if source.referral_sales_partner: - target.sales_partner = source.referral_sales_partner - target.commission_rate = frappe.get_value( - "Sales Partner", source.referral_sales_partner, "commission_rate" - ) - - 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): - balance_stock_qty = obj.stock_qty - ordered_items.get(obj.name, 0.0) - target.stock_qty = balance_stock_qty if balance_stock_qty > 0 else 0 - target.qty = flt(target.stock_qty) / flt(obj.conversion_factor) - - if obj.against_blanket_order: - target.against_blanket_order = obj.against_blanket_order - target.blanket_order = obj.blanket_order - target.blanket_order_rate = obj.blanket_order_rate - - def can_map_row(item) -> bool: - """ - Row mapping from Quotation to Sales order: - 1. If no selections, map all non-alternative rows (that sum up to the grand total) - 2. If selections: Is Alternative Item/Has Alternative Item: Map if selected and adequate qty - 3. If no selections: Simple row: Map if adequate qty - """ - if not ((item.stock_qty > ordered_items.get(item.name, 0.0)) or is_unit_price_row(item)): - return False - - if not selected_rows: - return not item.is_alternative - - if selected_rows and (item.is_alternative or item.has_alternative_item): - return item.name in selected_rows - - # Simple row - return True - - def select_item(d): - filtered_items = args.get("filtered_children", []) - child_filter = d.name in filtered_items if filtered_items else True - return child_filter - - automatically_fetch_payment_terms = cint( - frappe.get_single_value("Accounts Settings", "automatically_fetch_payment_terms") - ) - - doclist = get_mapped_doc( - "Quotation", - source_name, - { - "Quotation": { - "doctype": "Sales Order", - "validation": {"docstatus": ["=", 1]}, - "field_no_map": ["payment_terms_template"], - }, - "Quotation Item": { - "doctype": "Sales Order Item", - "field_map": {"parent": "prevdoc_docname", "name": "quotation_item"}, - "postprocess": update_item, - "condition": lambda d: can_map_row(d) and select_item(d), - }, - "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, - ) - - if automatically_fetch_payment_terms: - from erpnext.accounts.services.payment_schedule import PaymentScheduleService - - PaymentScheduleService(doclist).set_payment_schedule() - - return doclist - - def set_expired_status(): # filter out submitted non expired quotations whose validity has been ended cond = "`tabQuotation`.docstatus = 1 and `tabQuotation`.status NOT IN ('Expired', 'Lost') and `tabQuotation`.valid_till < %s" @@ -507,142 +382,3 @@ def set_expired_status(): }, (nowdate()), ) - - -@frappe.whitelist() -def make_sales_invoice( - source_name: str, target_doc: str | Document | None = None, args: str | dict | None = None -): - return _make_sales_invoice(source_name, target_doc, args=args) - - -def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, args=None): - if args is None: - args = {} - if isinstance(args, str): - args = json.loads(args) - - 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) - - def select_item(d): - filtered_items = args.get("filtered_children", []) - child_filter = d.name in filtered_items if filtered_items else True - return child_filter - - doclist = get_mapped_doc( - "Quotation", - source_name, - { - "Quotation": {"doctype": "Sales Invoice", "validation": {"docstatus": ["=", 1]}}, - "Quotation Item": { - "doctype": "Sales Invoice Item", - "postprocess": update_item, - "condition": lambda row: not row.is_alternative and select_item(row), - }, - "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 _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) - elif quotation.quotation_to == "CRM Deal": - customer_name = frappe.get_value("Customer", {"crm_deal": quotation.party_name}) - if customer_name: - return frappe.get_doc("Customer", customer_name) - - # Check if a Customer already exists for the Lead or Prospect. - 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 no Customer exists, create a new Customer or Prospect. - 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 get_ordered_items(quotation: str): - return frappe._dict( - frappe.get_all( - "Quotation Item", - {"docstatus": 1, "parent": quotation, "ordered_qty": (">", 0)}, - ["name", "ordered_qty"], - as_list=True, - ) - )