From 07eb5c714a34c1025a8ad50c1169029a945bba66 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:54:53 +0000 Subject: [PATCH] Merge pull request #52897 from frappe/mergify/bp/version-15-hotfix/pr-52878 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: standalone sales invoice return should not fallback to item mast… (backport #52878) --- .../sales_invoice_item.json | 5 +- .../report/gross_profit/test_gross_profit.py | 1 + erpnext/controllers/selling_controller.py | 55 +++++++++++-------- erpnext/stock/stock_ledger.py | 24 ++++---- erpnext/stock/utils.py | 3 +- 5 files changed, 47 insertions(+), 41 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index a5b93eae931..a2f30159e95 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -840,6 +840,7 @@ "fieldtype": "Currency", "label": "Incoming Rate (Costing)", "no_copy": 1, + "non_negative": 1, "options": "Company:company:default_currency", "print_hide": 1 }, @@ -983,7 +984,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2025-03-12 16:33:55.503777", + "modified": "2026-02-23 14:37:14.853941", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", @@ -993,4 +994,4 @@ "sort_field": "creation", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/accounts/report/gross_profit/test_gross_profit.py b/erpnext/accounts/report/gross_profit/test_gross_profit.py index 49ca61e950d..35f24df015f 100644 --- a/erpnext/accounts/report/gross_profit/test_gross_profit.py +++ b/erpnext/accounts/report/gross_profit/test_gross_profit.py @@ -439,6 +439,7 @@ class TestGrossProfit(FrappeTestCase): qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True ) sinv.is_return = 1 + sinv.items[0].allow_zero_valuation_rate = 1 sinv = sinv.save().submit() filters = frappe._dict( diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index c2a9afadcf0..a8c4a2733fc 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -483,10 +483,34 @@ class SellingController(StockController): sales_order.update_reserved_qty(so_item_rows) def set_incoming_rate(self): + def reset_incoming_rate(): + old_item = next( + ( + item + for item in (old_doc.get("items") + (old_doc.get("packed_items") or [])) + if item.name == d.name + ), + None, + ) + if old_item: + old_qty = flt(old_item.get("stock_qty") or old_item.get("actual_qty") or old_item.get("qty")) + if ( + old_item.item_code != d.item_code + or old_item.warehouse != d.warehouse + or old_qty != qty + or old_item.serial_no != d.serial_no + or get_serial_nos(old_item.serial_and_batch_bundle) + != get_serial_nos(d.serial_and_batch_bundle) + or old_item.batch_no != d.batch_no + or get_batch_nos(old_item.serial_and_batch_bundle) + != get_batch_nos(d.serial_and_batch_bundle) + ): + d.incoming_rate = 0 + if self.doctype not in ("Delivery Note", "Sales Invoice"): return - from erpnext.stock.serial_batch_bundle import get_batch_nos + from erpnext.stock.serial_batch_bundle import get_batch_nos, get_serial_nos allow_at_arms_length_price = frappe.get_cached_value( "Stock Settings", None, "allow_internal_transfer_at_arms_length_price" @@ -495,6 +519,8 @@ class SellingController(StockController): "Selling Settings", "set_zero_rate_for_expired_batch" ) + is_standalone = self.is_return and not self.return_against + old_doc = self.get_doc_before_save() items = self.get("items") + (self.get("packed_items") or []) for d in items: @@ -526,27 +552,7 @@ class SellingController(StockController): qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty")) if old_doc: - old_item = next( - ( - item - for item in (old_doc.get("items") + (old_doc.get("packed_items") or [])) - if item.name == d.name - ), - None, - ) - if old_item: - old_qty = flt( - old_item.get("stock_qty") or old_item.get("actual_qty") or old_item.get("qty") - ) - if ( - old_item.item_code != d.item_code - or old_item.warehouse != d.warehouse - or old_qty != qty - or old_item.batch_no != d.batch_no - or get_batch_nos(old_item.serial_and_batch_bundle) - != get_batch_nos(d.serial_and_batch_bundle) - ): - d.incoming_rate = 0 + reset_incoming_rate() if ( not d.incoming_rate @@ -565,11 +571,12 @@ class SellingController(StockController): "voucher_type": self.doctype, "voucher_no": self.name, "voucher_detail_no": d.name, - "allow_zero_valuation": d.get("allow_zero_valuation"), + "allow_zero_valuation": d.get("allow_zero_valuation_rate"), "batch_no": d.batch_no, "serial_no": d.serial_no, }, - raise_error_if_no_rate=False, + raise_error_if_no_rate=is_standalone, + fallbacks=not is_standalone, ) if ( diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index ae3730d4897..d524f644a0f 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1920,6 +1920,7 @@ def get_valuation_rate( allow_zero_rate=False, currency=None, company=None, + fallbacks=True, raise_error_if_no_rate=True, batch_no=None, serial_and_batch_bundle=None, @@ -1982,23 +1983,20 @@ def get_valuation_rate( ): return flt(last_valuation_rate[0][0]) - # If negative stock allowed, and item delivered without any incoming entry, - # system does not found any SLE, then take valuation rate from Item - valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate") - - if not valuation_rate: - # try Item Standard rate - valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate") - - if not valuation_rate: - # try in price list - valuation_rate = frappe.db.get_value( + if fallbacks: + # If negative stock allowed, and item delivered without any incoming entry, + # system does not found any SLE, then take valuation rate from Item + if rate := ( + frappe.db.get_value("Item", item_code, "valuation_rate") + or frappe.db.get_value("Item", item_code, "standard_rate") + or frappe.db.get_value( "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate" ) + ): + return flt(rate) if ( not allow_zero_rate - and not valuation_rate and raise_error_if_no_rate and cint(erpnext.is_perpetual_inventory_enabled(company)) ): @@ -2028,8 +2026,6 @@ def get_valuation_rate( frappe.throw(msg=msg, title=_("Valuation Rate Missing")) - return valuation_rate - def update_qty_in_future_sle(args, allow_negative_stock=False): """Recalculate Qty after Transaction in future SLEs based on current SLE.""" diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 58ecb24db48..0c03e350d02 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -240,7 +240,7 @@ def _create_bin(item_code, warehouse): @frappe.whitelist() -def get_incoming_rate(args, raise_error_if_no_rate=True): +def get_incoming_rate(args, raise_error_if_no_rate=True, fallbacks: bool = True): """Get Incoming Rate based on valuation method""" from erpnext.stock.stock_ledger import get_previous_sle, get_valuation_rate @@ -325,6 +325,7 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): args.get("allow_zero_valuation"), currency=erpnext.get_company_currency(args.get("company")), company=args.get("company"), + fallbacks=fallbacks, raise_error_if_no_rate=raise_error_if_no_rate, )