fix: preserve full source pricing on rate-locked rows

Restoring only price_list_rate on a mapped row dropped any manual discount or
margin, so re-running pricing produced a rate that differed from the source and
still failed the maintain-same-rate check on save.

Copy the source row's whole pricing block (rate, discount, margin) and skip
pricing rules for locked rows, in both get_item_details and the bulk
apply_price_list path.
This commit is contained in:
Jatin3128
2026-07-30 17:49:37 +05:30
parent 7f8b77bdbb
commit 3a0be8c9d0
2 changed files with 90 additions and 16 deletions

View File

@@ -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

View File

@@ -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")