From 553b55b2f0ec97c78a42190b8634791307f52f46 Mon Sep 17 00:00:00 2001 From: pandiyan Date: Mon, 22 Jun 2026 23:06:09 +0530 Subject: [PATCH 1/2] fix: skip qty over-allowance check for non-stock items only --- erpnext/controllers/status_updater.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 65a2ec58c50..2e3b6635da3 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -384,15 +384,17 @@ class StatusUpdater(Document): def fetch_items_with_pending_qty(self, args, item_field, items): doctype = frappe.qb.DocType(args["target_dt"]) - item_field = doctype[item_field] + item_field_col = doctype[item_field] target_ref_field = doctype[args["target_ref_field"]] target_field = doctype[args["target_field"]] - return ( + is_qty_check = "qty" in args["target_ref_field"] + + query = ( frappe.qb.from_(doctype) .select( doctype.name, - item_field.as_("item_code"), + item_field_col.as_("item_code"), target_ref_field, target_field, doctype.parenttype, @@ -401,9 +403,18 @@ class StatusUpdater(Document): .where(target_ref_field < target_field) .where(doctype.name.isin(items)) .where(doctype.docstatus == 1) - .run(as_dict=True) ) + if is_qty_check: + item_table = frappe.qb.DocType("Item") + query = ( + query.join(item_table) + .on(item_table.name == item_field_col) + .where(item_table.is_stock_item == 1) + ) + + return query.run(as_dict=True) + def check_overflow_with_allowance(self, item, args): """ Checks if there is overflow considering a relaxation allowance. From 733e24faef444ab3bf2291d2ee21f94b13c8745d Mon Sep 17 00:00:00 2001 From: pandiyan Date: Tue, 23 Jun 2026 09:08:53 +0530 Subject: [PATCH 2/2] test: add tests for non stock item over billing against so/po --- .../purchase_invoice/test_purchase_invoice.py | 18 ++++++++ .../sales_invoice/test_sales_invoice.py | 45 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 8ad37f7f9d5..17afc03dde1 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -3046,6 +3046,24 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin): # Test 4 - Since this PI is overbilled by 130% and only 120% is allowed, it will fail self.assertRaises(frappe.ValidationError, pi.submit) + @ERPNextTestSuite.change_settings("Accounts Settings", {"over_billing_allowance": 0}) + def test_non_stock_item_over_billing_against_po_is_blocked(self): + service_item = create_item( + "_Test Service Item Non Stock PI", + is_stock_item=0, + is_purchase_item=1, + ).name + + po = create_purchase_order(item_code=service_item, qty=5, rate=100, do_not_save=False) + po.submit() + + pi = make_pi_from_po(po.name) + pi.items[0].qty = 10 # overbill by 100 % + pi.save() + + with self.assertRaises(frappe.ValidationError): + pi.submit() + def test_discount_percentage_not_set_when_amount_is_manually_set(self): pi = make_purchase_invoice(do_not_save=True) discount_amount = 7 diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 57cfcddbd98..990f7e89d6f 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -4126,6 +4126,51 @@ class TestSalesInvoice(ERPNextTestSuite): self.assertIn("cannot overbill", str(err.exception).lower()) dn.cancel() + @ERPNextTestSuite.change_settings("Accounts Settings", {"over_billing_allowance": 0}) + def test_non_stock_item_over_billing_against_so_is_blocked(self): + from erpnext.selling.doctype.sales_order.mapper import make_sales_invoice as make_si_from_so + from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + + service_item = create_item( + "_Test Service Item Non Stock SI", + is_stock_item=0, + ).name + + so = make_sales_order(item_code=service_item, qty=5, rate=100) + so.submit() + + si = make_si_from_so(so.name) + si.items[0].qty = 10 # overbill by 100 % + si.save() + + with self.assertRaises(frappe.ValidationError): + si.submit() + + @ERPNextTestSuite.change_settings("Accounts Settings", {"over_billing_allowance": 0}) + def test_non_stock_item_over_billing_against_so_from_quotation_is_blocked(self): + from erpnext.selling.doctype.quotation.mapper import make_sales_order as make_so_from_quotation + from erpnext.selling.doctype.quotation.test_quotation import make_quotation + from erpnext.selling.doctype.sales_order.mapper import make_sales_invoice as make_si_from_so + + service_item = create_item( + "_Test Service Item Non Stock SI Quot", + is_stock_item=0, + ).name + + quotation = make_quotation(item_code=service_item, qty=5, rate=100) + + so = make_so_from_quotation(quotation.name) + so.delivery_date = frappe.utils.add_days(frappe.utils.today(), 7) + so.insert() + so.submit() + + si = make_si_from_so(so.name) + si.items[0].qty = 10 # overbill by 100 % + si.save() + + with self.assertRaises(frappe.ValidationError): + si.submit() + @ERPNextTestSuite.change_settings( "Accounts Settings", {