From 71beb6b588605f8c1d58431c798bbbce8318e2fb Mon Sep 17 00:00:00 2001 From: Jatin3128 Date: Mon, 27 Jul 2026 11:49:50 +0530 Subject: [PATCH] fix: keep source rate on re-fetch when maintain same rate is enabled With "maintain same rate" on, re-fetching item details on a row mapped from a source document (e.g. a Purchase Order) pulled the latest Item Price, giving a rate the document can never be saved with. Skip the price list fetch for such rows and keep the source rate. Fixes frappe/erpnext#57436 --- erpnext/stock/get_item_details.py | 45 +++++++++++- erpnext/stock/tests/test_get_item_details.py | 72 ++++++++++++++++++++ 2 files changed, 116 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 9b6c6117398..8cf2fae0d83 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -40,6 +40,17 @@ purchase_doctypes = [ NOT_APPLICABLE_TAX = "N/A" +# Item rows mapped from these source documents must keep the source rate when +# "maintain same rate" is enabled, so a newer Item Price is never fetched onto them. +maintain_same_rate_source_fields = { + "Purchase Order": ("supplier_quotation_item",), + "Purchase Receipt": ("purchase_order_item",), + "Purchase Invoice": ("po_detail", "pr_detail"), + "Sales Order": ("quotation_item", "prevdoc_docname"), + "Delivery Note": ("so_detail", "si_detail"), + "Sales Invoice": ("so_detail", "dn_detail"), +} + def _preprocess_ctx(ctx): if not ctx.price_list: @@ -120,7 +131,11 @@ def get_item_details( if ctx.doctype in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]: ctx.customer = None - out.update(get_price_list_rate(ctx, item)) + source_row = get_rate_locked_source_row(ctx, doc) + if source_row: + out.price_list_rate = flt(source_row.get("price_list_rate")) or flt(source_row.get("rate")) + else: + out.update(get_price_list_rate(ctx, item)) if ( not out.price_list_rate @@ -188,6 +203,34 @@ def remove_standard_fields(out: frappe._dict): return out +def get_rate_locked_source_row(ctx: ItemDetailsCtx, doc) -> frappe._dict | None: + """Return the current item row when its rate is locked to a source document. + + With "maintain same rate" enabled, a row mapped from a source (e.g. a Purchase + Order) can only be saved at the source rate. Fetching a newer Item Price here + would set a rate the document can never be saved with, so keep the source rate. + """ + source_fields = maintain_same_rate_source_fields.get(ctx.doctype) + if not source_fields or not doc or ctx.get("is_return") or not maintain_same_rate_enabled(ctx): + return None + + row = next((d for d in doc.get("items") or [] if d.get("name") == ctx.child_docname), None) + if row and any(row.get(field) for field in source_fields): + return row + return None + + +def maintain_same_rate_enabled(ctx: ItemDetailsCtx) -> bool: + if ctx.doctype in purchase_doctypes: + if ctx.get("is_internal_supplier"): + return False + return bool(cint(frappe.get_cached_value("Buying Settings", "None", "maintain_same_rate"))) + + if ctx.get("is_internal_customer"): + return False + return bool(cint(frappe.get_cached_value("Selling Settings", "None", "maintain_same_sales_rate"))) + + def set_valuation_rate(out: frappe._dict, ctx: frappe._dict): from erpnext.selling.doctype.product_bundle.product_bundle import get_active_product_bundle diff --git a/erpnext/stock/tests/test_get_item_details.py b/erpnext/stock/tests/test_get_item_details.py index f9513fb5743..7f92aa0f3fa 100644 --- a/erpnext/stock/tests/test_get_item_details.py +++ b/erpnext/stock/tests/test_get_item_details.py @@ -124,3 +124,75 @@ class TestGetItemDetail(ERPNextTestSuite): dn.save() self.assertEqual(dn.items[0].batch_no, "BATCH01") self.assertEqual(dn.items[0].rate, 50) + + def test_maintain_same_rate_keeps_source_rate_on_refetch(self): + """#57436: with "maintain same rate" on, re-fetching a PR row mapped from a + PO must keep the PO rate instead of pulling a newer, higher Item Price. + + The rate is validated on save, so it can never persist changed; assert the + fetched rate directly to prove the newer Item Price is never picked up. + """ + from erpnext.buying.doctype.purchase_order.mapper import make_purchase_receipt + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + from erpnext.stock.doctype.item.test_item import make_item + + def set_maintain_same_rate(value): + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", value) + frappe.clear_cache(doctype="Buying Settings") + + set_maintain_same_rate(1) + + item_code = make_item(properties={"is_stock_item": 1}).name + po = create_purchase_order(item_code=item_code, qty=1, rate=100) + + # The PO may auto-insert an Item Price at 100; bump it to the newer, higher rate. + item_price = frappe.db.get_value( + "Item Price", {"item_code": item_code, "price_list": "Standard Buying"} + ) + if item_price: + frappe.db.set_value("Item Price", item_price, "price_list_rate", 120) + else: + frappe.get_doc( + { + "doctype": "Item Price", + "price_list": "Standard Buying", + "item_code": item_code, + "price_list_rate": 120, + } + ).insert() + + pr = make_purchase_receipt(po.name) + pr.insert() + + def fetch_price_list_rate(): + ctx = frappe._dict( + { + "item_code": item_code, + "doctype": "Purchase Receipt", + "name": pr.name, + "company": pr.company, + "supplier": pr.supplier, + "currency": pr.currency, + "conversion_rate": 1.0, + "price_list": "Standard Buying", + "price_list_currency": pr.currency, + "plc_conversion_rate": 1.0, + "warehouse": pr.items[0].warehouse, + "uom": pr.items[0].uom, + "stock_uom": pr.items[0].stock_uom, + "qty": pr.items[0].qty, + "child_doctype": pr.items[0].doctype, + "child_docname": pr.items[0].name, + "is_return": 0, + "is_internal_supplier": 0, + "ignore_pricing_rule": 1, + } + ) + return get_item_details(ctx, pr).get("price_list_rate") + + # Rate stays at the PO rate; the newer Item Price (120) is not fetched. + self.assertEqual(fetch_price_list_rate(), 100) + + # Control: without the setting the newer Item Price would be fetched. + set_maintain_same_rate(0) + self.assertEqual(fetch_price_list_rate(), 120)