diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index a7924288058..ab27ddb50aa 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -341,14 +341,17 @@ class StatusUpdater(Document): ): return - 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["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.' + ) else: - action_msg = _( - 'To allow over billing, update "Over Billing Allowance" in Accounts Settings or the Item.' - ) + action_msg = None frappe.throw( _( diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 0fdfbef279b..7ded266c62a 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -428,3 +428,4 @@ 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 +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..faa99fcd2ca --- /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..3f30664e39b 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,8 @@ 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( - "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/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 210f4715815..e72b595f824 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -828,7 +828,7 @@ class TestQuotation(FrappeTestCase): # 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]: @@ -842,7 +842,7 @@ class TestQuotation(FrappeTestCase): 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/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index 74c4670063e..27d318de4b0 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,12 +695,23 @@ "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", 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() diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index db4d60b56f6..13759d0f7f7 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, FrappeTestCase): def tearDown(self): frappe.set_user("Administrator") + @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