diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index cc8ed891770..aace6a69d94 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -133,18 +133,18 @@ def get_item_details( 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")) + lock_source_rate(out, source_row) else: out.update(get_price_list_rate(ctx, item)) - if ( - not out.price_list_rate - and ctx.transaction_type == "selling" - and frappe.get_single_value("Selling Settings", "fallback_to_default_price_list") - ): - fallback_args = ctx.copy() - fallback_args.price_list = frappe.get_single_value("Selling Settings", "selling_price_list") - out.update(get_price_list_rate(fallback_args, item)) + if ( + not out.price_list_rate + and ctx.transaction_type == "selling" + and frappe.get_single_value("Selling Settings", "fallback_to_default_price_list") + ): + fallback_args = ctx.copy() + fallback_args.price_list = frappe.get_single_value("Selling Settings", "selling_price_list") + out.update(get_price_list_rate(fallback_args, item)) ctx.customer = current_customer @@ -159,9 +159,8 @@ def get_item_details( if ctx.get(key) is None: ctx[key] = value - data = get_pricing_rule_for_item(ctx, doc=doc, for_validate=for_validate) - - out.update(data) + if not source_row: + out.update(get_pricing_rule_for_item(ctx, doc=doc, for_validate=for_validate)) if ( frappe.get_single_value("Stock Settings", "auto_create_serial_and_batch_bundle_for_outward") @@ -234,6 +233,19 @@ def maintain_same_rate_enabled(ctx: ItemDetailsCtx) -> bool: return bool(cint(frappe.get_cached_value("Selling Settings", "None", "maintain_same_sales_rate"))) +def lock_source_rate(out: frappe._dict, source_row) -> None: + """Copy the source row's whole pricing block onto out so a mapped row keeps its + exact rate. Pricing rules are skipped for these rows, so nothing re-derives it and + the manual discount or margin that made rate differ from price_list_rate survives. + """ + out.price_list_rate = flt(source_row.get("price_list_rate")) or flt(source_row.get("rate")) + out.rate = flt(source_row.get("rate")) + out.discount_percentage = flt(source_row.get("discount_percentage")) + out.discount_amount = flt(source_row.get("discount_amount")) + out.margin_type = source_row.get("margin_type") + out.margin_rate_or_amount = flt(source_row.get("margin_rate_or_amount")) + + def set_valuation_rate(out: frappe._dict, ctx: frappe._dict): from erpnext.selling.doctype.product_bundle.product_bundle import get_active_product_bundle @@ -1700,9 +1712,8 @@ def apply_price_list_on_item(ctx, doc=None): 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")) - ) + item_details = frappe._dict() + lock_source_rate(item_details, source_row) else: item_details = get_price_list_rate(ctx, item_doc) @@ -1711,7 +1722,8 @@ def apply_price_list_on_item(ctx, doc=None): ) ctx.stock_qty = flt(ctx.qty) * flt(ctx.conversion_factor) - item_details.update(get_pricing_rule_for_item(ctx, doc=doc)) + if not source_row: + item_details.update(get_pricing_rule_for_item(ctx, doc=doc)) return item_details diff --git a/erpnext/stock/tests/test_get_item_details.py b/erpnext/stock/tests/test_get_item_details.py index e251323a54b..a719a7fb4c0 100644 --- a/erpnext/stock/tests/test_get_item_details.py +++ b/erpnext/stock/tests/test_get_item_details.py @@ -259,3 +259,65 @@ class TestGetItemDetail(ERPNextTestSuite): finally: frappe.db.set_single_value("Buying Settings", "maintain_same_rate", original) frappe.clear_cache(doctype="Buying Settings") + + def test_maintain_same_rate_keeps_source_discount_on_refetch(self): + """A mapped row with a manual discount has rate != price_list_rate. Re-fetch must + keep the source's rate and discount, not just the pre-discount price, or the + recomputed rate diverges from the reference and fails maintain-same-rate on save. + """ + from frappe.utils import flt + + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + + 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=90, qty=1) + + row_name = "pr-row-1" + # price_list_rate 100 with a 10% discount gives the effective rate 90. + pr_doc = { + "doctype": "Purchase Receipt", + "items": [ + { + "name": row_name, + "item_code": item_code, + "purchase_order_item": po.items[0].name, + "price_list_rate": 100, + "rate": 90, + "discount_percentage": 10, + } + ], + } + ctx = frappe._dict( + item_code=item_code, + doctype="Purchase Receipt", + company=po.company, + supplier=po.supplier, + currency=po.currency, + conversion_rate=1.0, + price_list=price_list, + price_list_currency=po.currency, + plc_conversion_rate=1.0, + warehouse="_Test Warehouse - _TC", + uom=po.items[0].uom, + stock_uom=po.items[0].stock_uom, + qty=1, + child_docname=row_name, + is_return=0, + is_internal_supplier=0, + ignore_pricing_rule=1, + ) + + out = get_item_details(ctx, pr_doc) + self.assertEqual(flt(out.get("price_list_rate")), 100) + self.assertEqual(flt(out.get("rate")), 90) + self.assertEqual(flt(out.get("discount_percentage")), 10) + finally: + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", original) + frappe.clear_cache(doctype="Buying Settings")