fix: standalone sales invoice return should not fallback to item master for valuation rate

(cherry picked from commit a85a0aef52)
This commit is contained in:
Mihir Kandoi
2026-02-23 14:52:23 +05:30
committed by Mergify
parent 407bf7ec2e
commit 6e1a8083a5
5 changed files with 46 additions and 40 deletions

View File

@@ -843,6 +843,7 @@
"fieldtype": "Currency",
"label": "Incoming Rate (Costing)",
"no_copy": 1,
"non_negative": 1,
"options": "Company:company:default_currency",
"print_hide": 1
},
@@ -1009,7 +1010,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2026-02-15 21:08:57.341638",
"modified": "2026-02-23 14:37:14.853941",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",

View File

@@ -444,6 +444,7 @@ class TestGrossProfit(IntegrationTestCase):
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(

View File

@@ -498,10 +498,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"
@@ -510,6 +534,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:
@@ -541,27 +567,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
@@ -583,11 +589,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 (

View File

@@ -1910,6 +1910,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,
@@ -1970,23 +1971,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))
):
@@ -2016,8 +2014,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."""

View File

@@ -237,7 +237,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,
)