From 4cc306d2d88e39646b93209774a5a7e0cf9c29d6 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 30 Jan 2026 16:37:08 +0530 Subject: [PATCH 1/2] fix: validate over ordering of quotation --- erpnext/controllers/status_updater.py | 2 +- erpnext/patches.txt | 1 + .../set_ordered_qty_in_quotation_item.py | 16 +++++++++++ .../selling/doctype/quotation/quotation.py | 27 +++++-------------- .../quotation_item/quotation_item.json | 17 ++++++++++-- .../doctype/quotation_item/quotation_item.py | 1 + .../doctype/sales_order/sales_order.py | 11 ++++++++ 7 files changed, 52 insertions(+), 23 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 652ce697711..70eb459112f 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -443,7 +443,7 @@ class StatusUpdater(Document): ): return - if args["source_dt"] != "Pick List 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.' diff --git a/erpnext/patches.txt b/erpnext/patches.txt index eb7c7605aac..3fdd00237e7 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -460,3 +460,4 @@ erpnext.patches.v16_0.set_post_change_gl_entries_on_pos_settings erpnext.patches.v15_0.create_accounting_dimensions_in_advance_taxes_and_charges 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 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 05a8fb08e50..fd6f6ec812a 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -450,7 +450,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"}, @@ -553,6 +556,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 @@ -613,27 +618,9 @@ def handle_mandatory_error(e, customer, lead_name): frappe.throw(message, title=_("Mandatory Missing")) -@frappe.whitelist() 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( - "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 ) ) diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index 8ff14f2063a..92d7895c57b 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -27,6 +27,7 @@ "uom", "conversion_factor", "stock_qty", + "ordered_qty", "available_quantity_section", "actual_qty", "column_break_ylrv", @@ -694,19 +695,31 @@ "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": [], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} 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 a43877f923a..876b11459b4 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -194,6 +194,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() @@ -481,6 +491,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() self.delete_removed_delivery_schedule_items() From 36f1e3572c488475185d3f7307c6f898b85c7ad5 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 30 Jan 2026 19:16:39 +0530 Subject: [PATCH 2/2] fix: test cases --- erpnext/selling/doctype/quotation/test_quotation.py | 4 ++-- erpnext/selling/doctype/sales_order/test_sales_order.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 364d1bee981..93772e026f8 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -933,7 +933,7 @@ class TestQuotation(IntegrationTestCase): # item code same but description different make_item("_Test Item 2", {"is_stock_item": 1}) - quotation = make_quotation(qty=1, rate=100, do_not_submit=1) + quotation = make_quotation(qty=10, rate=100, do_not_submit=1) # duplicate items for qty in [1, 1, 2, 3]: @@ -947,7 +947,7 @@ class TestQuotation(IntegrationTestCase): sales_order.delivery_date = nowdate() self.assertEqual(len(sales_order.items), 6) - self.assertEqual(sales_order.items[0].qty, 1) + self.assertEqual(sales_order.items[0].qty, 10) self.assertEqual(sales_order.items[-1].qty, 5) # Row 1: 10, Row 4: 1, Row 5: 1 diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 4d0d43915a7..bc325cdab34 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -57,6 +57,7 @@ class TestSalesOrder(AccountsTestMixin, IntegrationTestCase): frappe.db.rollback() frappe.set_user("Administrator") + @IntegrationTestCase.change_settings("Selling Settings", {"allow_negative_rates_for_items": 1}) def test_sales_order_with_negative_rate(self): """ Test if negative rate is allowed in Sales Order via doc submission and update items