From 0e60750bd848aa7c5550900c2055ea04d7799611 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 30 Jan 2026 16:37:08 +0530 Subject: [PATCH] fix: validate over ordering of quotation (cherry picked from commit 4cc306d2d88e39646b93209774a5a7e0cf9c29d6) # Conflicts: # erpnext/controllers/status_updater.py # erpnext/patches.txt # erpnext/selling/doctype/quotation/quotation.py # erpnext/selling/doctype/quotation_item/quotation_item.json --- erpnext/controllers/status_updater.py | 12 ++++++++++ erpnext/patches.txt | 6 +++++ .../set_ordered_qty_in_quotation_item.py | 16 +++++++++++++ .../selling/doctype/quotation/quotation.py | 24 ++++++++----------- .../quotation_item/quotation_item.json | 19 ++++++++++++++- .../doctype/quotation_item/quotation_item.py | 1 + .../doctype/sales_order/sales_order.py | 11 +++++++++ 7 files changed, 74 insertions(+), 15 deletions(-) create mode 100644 erpnext/patches/v16_0/set_ordered_qty_in_quotation_item.py diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index a7924288058..1115381b10f 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -341,10 +341,22 @@ class StatusUpdater(Document): ): return +<<<<<<< HEAD if qty_or_amount == "qty": action_msg = _( 'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.' ) +======= + if args["source_dt"] != "Pick List Item" and args["target_dt"] != "Quotation Item": + if qty_or_amount == "qty": + action_msg = _( + 'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.' + ) + else: + action_msg = _( + 'To allow over billing, update "Over Billing Allowance" in Accounts Settings or the Item.' + ) +>>>>>>> 4cc306d2d8 (fix: validate over ordering of quotation) else: action_msg = _( 'To allow over billing, update "Over Billing Allowance" in Accounts Settings or the Item.' diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 0fdfbef279b..7af2e39eb98 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -428,3 +428,9 @@ execute:frappe.db.set_single_value("Accounts Settings", "show_party_balance", 1) execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1) erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter #2025-12-11 erpnext.patches.v15_0.create_accounting_dimensions_in_advance_taxes_and_charges +<<<<<<< HEAD +======= +execute:frappe.delete_doc_if_exists("Workspace Sidebar", "Opening & Closing") +erpnext.patches.v16_0.migrate_transaction_deletion_task_flags_to_status # 2 +erpnext.patches.v16_0.set_ordered_qty_in_quotation_item +>>>>>>> 4cc306d2d8 (fix: validate over ordering of quotation) diff --git a/erpnext/patches/v16_0/set_ordered_qty_in_quotation_item.py b/erpnext/patches/v16_0/set_ordered_qty_in_quotation_item.py new file mode 100644 index 00000000000..93a6323eb6f --- /dev/null +++ b/erpnext/patches/v16_0/set_ordered_qty_in_quotation_item.py @@ -0,0 +1,16 @@ +import frappe + + +def execute(): + data = frappe.get_all( + "Sales Order Item", + filters={"quotation_item": ["is", "set"], "docstatus": 1}, + fields=["quotation_item", {"SUM": "stock_qty", "as": "ordered_qty"}], + group_by="quotation_item", + ) + if data: + frappe.db.auto_commit_on_many_writes = 1 + frappe.db.bulk_update( + "Quotation Item", {d.quotation_item: {"ordered_qty": d.ordered_qty} for d in data} + ) + frappe.db.auto_commit_on_many_writes = 0 diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index d6b2fe73cac..7d67807aee1 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -446,7 +446,10 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar "Quotation", source_name, { - "Quotation": {"doctype": "Sales Order", "validation": {"docstatus": ["=", 1]}}, + "Quotation": { + "doctype": "Sales Order", + "validation": {"docstatus": ["=", 1]}, + }, "Quotation Item": { "doctype": "Sales Order Item", "field_map": {"parent": "prevdoc_docname", "name": "quotation_item"}, @@ -549,6 +552,8 @@ def _make_customer(source_name, ignore_permissions=False): if quotation.quotation_to == "Customer": return frappe.get_doc("Customer", quotation.party_name) + elif quotation.quotation_to == "CRM Deal": + return frappe.get_doc("Customer", {"crm_deal": quotation.party_name}) # Check if a Customer already exists for the Lead or Prospect. existing_customer = None @@ -610,25 +615,16 @@ def handle_mandatory_error(e, customer, lead_name): def get_ordered_items(quotation: str): - """ - Returns a dict of ordered items with their total qty based on quotation row name. - - In `Sales Order Item`, `quotation_item` is the row name of `Quotation Item`. - - Example: - ``` - { - "refsdjhd2": 10, - "ygdhdshrt": 5, - } - ``` - """ return frappe._dict( frappe.get_all( +<<<<<<< HEAD "Sales Order Item", filters={"prevdoc_docname": quotation, "docstatus": 1}, fields=["quotation_item", "sum(qty)"], group_by="quotation_item", as_list=1, +======= + "Quotation Item", {"docstatus": 1, "parent": quotation}, ["name", "ordered_qty"], as_list=True +>>>>>>> 4cc306d2d8 (fix: validate over ordering of quotation) ) ) diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index 74c4670063e..2f723e08120 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -24,6 +24,7 @@ "uom", "conversion_factor", "stock_qty", + "ordered_qty", "available_quantity_section", "actual_qty", "column_break_ylrv", @@ -694,18 +695,34 @@ "print_hide": 1, "read_only": 1, "report_hide": 1 + }, + { + "default": "0", + "fieldname": "ordered_qty", + "fieldtype": "Float", + "hidden": 1, + "label": "Ordered Qty", + "no_copy": 1, + "non_negative": 1, + "read_only": 1, + "reqd": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2025-08-26 20:31:47.775890", + "modified": "2026-01-30 12:56:08.320190", "modified_by": "Administrator", "module": "Selling", "name": "Quotation Item", "owner": "Administrator", "permissions": [], +<<<<<<< HEAD "sort_field": "modified", +======= + "row_format": "Dynamic", + "sort_field": "creation", +>>>>>>> 4cc306d2d8 (fix: validate over ordering of quotation) "sort_order": "DESC", "states": [], "track_changes": 1 diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.py b/erpnext/selling/doctype/quotation_item/quotation_item.py index bbdd8643593..9ab265c885c 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.py +++ b/erpnext/selling/doctype/quotation_item/quotation_item.py @@ -48,6 +48,7 @@ class QuotationItem(Document): margin_type: DF.Literal["", "Percentage", "Amount"] net_amount: DF.Currency net_rate: DF.Currency + ordered_qty: DF.Float page_break: DF.Check parent: DF.Data parentfield: DF.Data diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index a757efffa4e..a1047c11a96 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -185,6 +185,16 @@ class SalesOrder(SellingController): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.status_updater = [ + { + "source_dt": "Sales Order Item", + "target_dt": "Quotation Item", + "join_field": "quotation_item", + "target_field": "ordered_qty", + "target_ref_field": "stock_qty", + "source_field": "stock_qty", + } + ] def onload(self) -> None: super().onload() @@ -419,6 +429,7 @@ class SalesOrder(SellingController): frappe.throw(_("Row #{0}: Set Supplier for item {1}").format(d.idx, d.item_code)) def on_submit(self): + super().update_prevdoc_status() self.check_credit_limit() self.update_reserved_qty()