mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-17 00:18:39 +00:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user