From 7f8b77bdbbb4027da8beac5a8b1ff6fe1633e8db Mon Sep 17 00:00:00 2001 From: Jatin3128 Date: Thu, 30 Jul 2026 12:42:00 +0530 Subject: [PATCH] fix: keep source rate on bulk apply_price_list when maintain same rate is on The single-row re-fetch guard skipped the bulk apply_price_list path, so changing the price list, party, or conversion rate on a mapped transaction re-fetched current Item Prices and overwrote the mapped rates, breaking the maintain-same-rate check on save. Guard apply_price_list_on_item with the same source-row lookup, and resolve the parent doctype via ctx.parenttype since the bulk path carries the child doctype in ctx.doctype. --- erpnext/stock/get_item_details.py | 16 ++++- erpnext/stock/tests/test_get_item_details.py | 63 ++++++++++++++++++++ 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 8cf2fae0d83..cc8ed891770 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -210,7 +210,10 @@ def get_rate_locked_source_row(ctx: ItemDetailsCtx, doc) -> frappe._dict | None: 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 isinstance(doc, str): + doc = json.loads(doc) + + source_fields = maintain_same_rate_source_fields.get(ctx.parenttype or ctx.doctype) if not source_fields or not doc or ctx.get("is_return") or not maintain_same_rate_enabled(ctx): return None @@ -221,7 +224,7 @@ def get_rate_locked_source_row(ctx: ItemDetailsCtx, doc) -> frappe._dict | None: def maintain_same_rate_enabled(ctx: ItemDetailsCtx) -> bool: - if ctx.doctype in purchase_doctypes: + if (ctx.parenttype or 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"))) @@ -1694,7 +1697,14 @@ def apply_price_list(ctx: ItemDetailsCtx, as_doc: bool = False, doc: Document | def apply_price_list_on_item(ctx, doc=None): item_doc = frappe.get_cached_doc("Item", ctx.item_code) - item_details = get_price_list_rate(ctx, item_doc) + + source_row = get_rate_locked_source_row(ctx, doc) + if source_row: + item_details = frappe._dict( + price_list_rate=flt(source_row.get("price_list_rate")) or flt(source_row.get("rate")) + ) + else: + item_details = get_price_list_rate(ctx, item_doc) ctx.conversion_factor = flt(ctx.conversion_factor) or get_conversion_factor(ctx.item_code, ctx.uom).get( "conversion_factor", 1 diff --git a/erpnext/stock/tests/test_get_item_details.py b/erpnext/stock/tests/test_get_item_details.py index 7f92aa0f3fa..e251323a54b 100644 --- a/erpnext/stock/tests/test_get_item_details.py +++ b/erpnext/stock/tests/test_get_item_details.py @@ -196,3 +196,66 @@ class TestGetItemDetail(ERPNextTestSuite): # Control: without the setting the newer Item Price would be fetched. set_maintain_same_rate(0) self.assertEqual(fetch_price_list_rate(), 120) + + def test_apply_price_list_keeps_source_rate_when_maintain_same_rate(self): + """#57436: the bulk apply_price_list path (price list / party / conversion rate + change) must also keep the source rate on mapped rows, not just re-fetch of a + single row. Here a PR row carries its PO rate (175) while the current price list + rate is 100; the bulk apply must keep 175. + """ + from frappe.utils import flt, nowdate + + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + from erpnext.stock.get_item_details import apply_price_list + + item_code = "_Test Item" + price_list = "_Test Buying Price List" + + original = frappe.db.get_single_value("Buying Settings", "maintain_same_rate") + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1) + frappe.clear_cache(doctype="Buying Settings") + + try: + po = create_purchase_order(item_code=item_code, rate=175, qty=1) + + row_name = "pr-row-1" + pr_doc = { + "doctype": "Purchase Receipt", + "items": [ + { + "name": row_name, + "item_code": item_code, + "purchase_order_item": po.items[0].name, + "price_list_rate": 175, + "rate": 175, + } + ], + } + ctx = frappe._dict( + doctype="Purchase Receipt", + supplier=po.supplier, + company=po.company, + currency=po.currency, + conversion_rate=1.0, + price_list=price_list, + plc_conversion_rate=1.0, + transaction_date=nowdate(), + items=[ + frappe._dict( + doctype="Purchase Receipt Item", + parenttype="Purchase Receipt", + item_code=item_code, + child_docname=row_name, + qty=1, + uom=po.items[0].uom, + stock_uom=po.items[0].stock_uom, + conversion_factor=1.0, + ) + ], + ) + + result = apply_price_list(ctx, doc=pr_doc) + self.assertEqual(flt(result["children"][0].get("price_list_rate")), 175) + finally: + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", original) + frappe.clear_cache(doctype="Buying Settings")