mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-16 07:58:38 +00:00
fix: read the locked rate from the persisted source row
get_rate_locked_source_row returned the mutable target row, so an unsaved rate or discount edit on a mapped row was preserved on re-fetch instead of the source pricing, and the document still failed maintain-same-rate on save. Read the pricing straight from the linked source row in the database, and cover the edit-then-refresh case with a test.
This commit is contained in:
@@ -40,17 +40,28 @@ 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.
|
||||
# For each transaction, the child-row link field(s) that point to the source
|
||||
# document item, mapped to that source item doctype. When "maintain same rate" is
|
||||
# on, a mapped row keeps the persisted source pricing (read straight from that row),
|
||||
# so an unsaved edit on the target row can never lock in a non-source rate.
|
||||
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"),
|
||||
"Purchase Order": {"supplier_quotation_item": "Supplier Quotation Item"},
|
||||
"Purchase Receipt": {"purchase_order_item": "Purchase Order Item"},
|
||||
"Purchase Invoice": {"po_detail": "Purchase Order Item", "pr_detail": "Purchase Receipt Item"},
|
||||
"Sales Order": {"quotation_item": "Quotation Item"},
|
||||
"Delivery Note": {"so_detail": "Sales Order Item", "si_detail": "Sales Invoice Item"},
|
||||
"Sales Invoice": {"so_detail": "Sales Order Item", "dn_detail": "Delivery Note Item"},
|
||||
}
|
||||
|
||||
LOCKED_RATE_FIELDS = [
|
||||
"price_list_rate",
|
||||
"rate",
|
||||
"discount_percentage",
|
||||
"discount_amount",
|
||||
"margin_type",
|
||||
"margin_rate_or_amount",
|
||||
]
|
||||
|
||||
|
||||
def _preprocess_ctx(ctx):
|
||||
if not ctx.price_list:
|
||||
@@ -204,11 +215,11 @@ def remove_standard_fields(out: frappe._dict):
|
||||
|
||||
|
||||
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.
|
||||
"""Return the persisted source-document row a mapped target row is locked to.
|
||||
|
||||
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.
|
||||
The rate is read from the linked source row in the database (not the mutable
|
||||
target row), so a re-fetch always restores the source pricing the maintain-same-
|
||||
rate validator checks against, even after an unsaved edit on the target row.
|
||||
"""
|
||||
if isinstance(doc, str):
|
||||
doc = json.loads(doc)
|
||||
@@ -218,8 +229,12 @@ def get_rate_locked_source_row(ctx: ItemDetailsCtx, doc) -> frappe._dict | None:
|
||||
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
|
||||
if not row:
|
||||
return None
|
||||
|
||||
for link_field, source_doctype in source_fields.items():
|
||||
if source_name := row.get(link_field):
|
||||
return frappe.db.get_value(source_doctype, source_name, LOCKED_RATE_FIELDS, as_dict=True)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -315,8 +315,8 @@ class TestGetItemDetail(ERPNextTestSuite):
|
||||
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
|
||||
"""A mapped source row with a discount has rate != price_list_rate. Re-fetch must
|
||||
return 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
|
||||
@@ -331,21 +331,22 @@ class TestGetItemDetail(ERPNextTestSuite):
|
||||
frappe.clear_cache(doctype="Buying Settings")
|
||||
|
||||
try:
|
||||
po = create_purchase_order(item_code=item_code, rate=90, qty=1)
|
||||
# source PO carries the discount: list rate 100, 10% off, effective rate 90
|
||||
frappe.flags.dont_fetch_price_list_rate = True
|
||||
po = create_purchase_order(item_code=item_code, qty=1, do_not_save=True)
|
||||
po.buying_price_list = price_list
|
||||
po.items[0].price_list_rate = 100
|
||||
po.items[0].discount_percentage = 10
|
||||
po.items[0].rate = 90
|
||||
po.insert()
|
||||
po.submit()
|
||||
frappe.flags.dont_fetch_price_list_rate = False
|
||||
|
||||
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,
|
||||
}
|
||||
{"name": row_name, "item_code": item_code, "purchase_order_item": po.items[0].name}
|
||||
],
|
||||
}
|
||||
ctx = frappe._dict(
|
||||
@@ -375,3 +376,38 @@ class TestGetItemDetail(ERPNextTestSuite):
|
||||
finally:
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", original)
|
||||
frappe.clear_cache(doctype="Buying Settings")
|
||||
frappe.flags.dont_fetch_price_list_rate = False
|
||||
|
||||
def test_refetch_restores_source_rate_after_target_edit(self):
|
||||
"""Editing a mapped row's rate then re-fetching must restore the persisted source
|
||||
rate (read from the linked row), not lock in the edit, so the document still saves.
|
||||
"""
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
|
||||
item = "_Test Item"
|
||||
original = frappe.db.get_single_value("Buying Settings", "maintain_same_rate")
|
||||
original_action = frappe.db.get_single_value("Buying Settings", "maintain_same_rate_action")
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate_action", "Stop")
|
||||
frappe.clear_cache(doctype="Buying Settings")
|
||||
|
||||
try:
|
||||
po = create_purchase_order(item_code=item, qty=1, rate=90)
|
||||
pr = make_purchase_receipt(po.name)
|
||||
pr.insert()
|
||||
|
||||
# user edits the mapped row to a non-source rate
|
||||
pr.items[0].price_list_rate = 200
|
||||
pr.items[0].rate = 200
|
||||
|
||||
# a re-fetch must restore the persisted source (PO) rate, not keep the edit
|
||||
pr.process_item_selection(item_idx=pr.items[0].idx)
|
||||
self.assertEqual(flt(pr.items[0].rate), 90)
|
||||
pr.save() # must not raise the maintain-same-rate check
|
||||
finally:
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", original)
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate_action", original_action)
|
||||
frappe.clear_cache(doctype="Buying Settings")
|
||||
|
||||
Reference in New Issue
Block a user