From 39f6d8ffb68466fb4af4cfa58bebdbb35814ed42 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhodawala <99460106+Abdeali099@users.noreply.github.com> Date: Mon, 2 Jun 2025 14:49:15 +0530 Subject: [PATCH] fix: Handle duplicate Items qty in Quotation fix: Handle duplicate Items qty in Quotation --- .../selling/doctype/quotation/quotation.py | 71 ++++++++++--------- .../doctype/quotation/test_quotation.py | 46 ++++++++++++ 2 files changed, 85 insertions(+), 32 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 5eebead98d2..0ad6bf6bf2a 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -175,29 +175,22 @@ class Quotation(SellingController): ) def get_ordered_status(self): - status = "Open" - ordered_items = frappe._dict( - frappe.db.get_all( - "Sales Order Item", - {"prevdoc_docname": self.name, "docstatus": 1}, - ["item_code", "sum(qty)"], - group_by="item_code", - as_list=1, - ) - ) + ordered_items = get_ordered_items(self.name) if not ordered_items: - return status + return "Open" - has_alternatives = any(row.is_alternative for row in self.get("items")) - self._items = self.get_valid_items() if has_alternatives else self.get("items") + self._items = ( + self.get_valid_items() + if any(row.is_alternative for row in self.get("items")) + else self.get("items") + ) - if any(row.qty > ordered_items.get(row.item_code, 0.0) for row in self._items): - status = "Partially Ordered" - else: - status = "Ordered" + for row in self._items: + if row.name not in ordered_items or row.qty > ordered_items[row.name]: + return "Partially Ordered" - return status + return "Ordered" def get_valid_items(self): """ @@ -372,15 +365,7 @@ def make_sales_order(source_name: str, target_doc=None): def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): customer = _make_customer(source_name, ignore_permissions) - ordered_items = frappe._dict( - frappe.db.get_all( - "Sales Order Item", - {"prevdoc_docname": source_name, "docstatus": 1}, - ["item_code", "sum(qty)"], - group_by="item_code", - as_list=1, - ) - ) + ordered_items = get_ordered_items(source_name) selected_rows = [x.get("name") for x in frappe.flags.get("args", {}).get("selected_items", [])] @@ -418,7 +403,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): target.run_method("calculate_taxes_and_totals") def update_item(obj, target, source_parent): - balance_qty = obj.qty if is_unit_price_row(obj) else obj.qty - ordered_items.get(obj.item_code, 0.0) + balance_qty = obj.qty if is_unit_price_row(obj) else obj.qty - ordered_items.get(obj.name, 0.0) target.qty = balance_qty if balance_qty > 0 else 0 target.stock_qty = flt(target.qty) * flt(obj.conversion_factor) @@ -434,10 +419,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): 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 """ - balance_qty = item.qty - ordered_items.get(item.item_code, 0.0) - has_valid_qty: bool = (balance_qty > 0) or is_unit_price_row(item) - - if not has_valid_qty: + if not ((item.qty > ordered_items.get(item.name, 0.0)) or is_unit_price_row(item)): return False if not selected_rows: @@ -604,3 +586,28 @@ def handle_mandatory_error(e, customer, lead_name): 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): + """ + 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, + ) + ) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 9f982329710..3a84ef4fa1c 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -826,6 +826,52 @@ class TestQuotation(IntegrationTestCase): quotation.reload() self.assertEqual(quotation.status, "Ordered") + def test_duplicate_items_in_quotation(self): + from erpnext.selling.doctype.quotation.quotation import make_sales_order + from erpnext.stock.doctype.item.test_item import make_item + + # 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) + + # duplicate items + for qty in [1, 1, 2, 3]: + quotation.append("items", {"item_code": "_Test Item", "qty": qty, "rate": 100}) + + quotation.append("items", {"item_code": "_Test Item 2", "qty": 5, "rate": 100}) + + quotation.submit() + + sales_order = make_sales_order(quotation.name) + 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[-1].qty, 5) + + # Row 1: 10, Row 4: 1, Row 5: 1 + sales_order.items[0].qty = 10 + sales_order.items[3].qty = 1 + sales_order.items[4].qty = 1 + sales_order.submit() + + quotation.reload() + self.assertEqual(quotation.status, "Partially Ordered") + + sales_order_2 = make_sales_order(quotation.name) + sales_order_2.delivery_date = nowdate() + self.assertEqual(len(sales_order_2.items), 2) + self.assertEqual(sales_order_2.items[0].qty, 1) + self.assertEqual(sales_order_2.items[1].qty, 2) + + self.assertEqual(sales_order_2.items[0].quotation_item, quotation.items[3].name) + self.assertEqual(sales_order_2.items[1].quotation_item, quotation.items[4].name) + + sales_order_2.submit() + quotation.reload() + self.assertEqual(quotation.status, "Ordered") + def enable_calculate_bundle_price(enable=1): selling_settings = frappe.get_doc("Selling Settings")