mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-16 16:08:39 +00:00
fix: keep source rate on re-fetch when maintain same rate is enabled
With "maintain same rate" on, re-fetching item details on a row mapped from a source document (e.g. a Purchase Order) pulled the latest Item Price, giving a rate the document can never be saved with. Skip the price list fetch for such rows and keep the source rate. Fixes frappe/erpnext#57436
This commit is contained in:
@@ -40,6 +40,17 @@ 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.
|
||||
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"),
|
||||
}
|
||||
|
||||
|
||||
def _preprocess_ctx(ctx):
|
||||
if not ctx.price_list:
|
||||
@@ -120,7 +131,11 @@ def get_item_details(
|
||||
if ctx.doctype in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]:
|
||||
ctx.customer = None
|
||||
|
||||
out.update(get_price_list_rate(ctx, item))
|
||||
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"))
|
||||
else:
|
||||
out.update(get_price_list_rate(ctx, item))
|
||||
|
||||
if (
|
||||
not out.price_list_rate
|
||||
@@ -188,6 +203,34 @@ def remove_standard_fields(out: frappe._dict):
|
||||
return out
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
"""
|
||||
source_fields = maintain_same_rate_source_fields.get(ctx.doctype)
|
||||
if not source_fields or not doc or ctx.get("is_return") or not maintain_same_rate_enabled(ctx):
|
||||
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
|
||||
return None
|
||||
|
||||
|
||||
def maintain_same_rate_enabled(ctx: ItemDetailsCtx) -> bool:
|
||||
if 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")))
|
||||
|
||||
if ctx.get("is_internal_customer"):
|
||||
return False
|
||||
return bool(cint(frappe.get_cached_value("Selling Settings", "None", "maintain_same_sales_rate")))
|
||||
|
||||
|
||||
def set_valuation_rate(out: frappe._dict, ctx: frappe._dict):
|
||||
from erpnext.selling.doctype.product_bundle.product_bundle import get_active_product_bundle
|
||||
|
||||
|
||||
@@ -124,3 +124,75 @@ class TestGetItemDetail(ERPNextTestSuite):
|
||||
dn.save()
|
||||
self.assertEqual(dn.items[0].batch_no, "BATCH01")
|
||||
self.assertEqual(dn.items[0].rate, 50)
|
||||
|
||||
def test_maintain_same_rate_keeps_source_rate_on_refetch(self):
|
||||
"""#57436: with "maintain same rate" on, re-fetching a PR row mapped from a
|
||||
PO must keep the PO rate instead of pulling a newer, higher Item Price.
|
||||
|
||||
The rate is validated on save, so it can never persist changed; assert the
|
||||
fetched rate directly to prove the newer Item Price is never picked up.
|
||||
"""
|
||||
from erpnext.buying.doctype.purchase_order.mapper import make_purchase_receipt
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
def set_maintain_same_rate(value):
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", value)
|
||||
frappe.clear_cache(doctype="Buying Settings")
|
||||
|
||||
set_maintain_same_rate(1)
|
||||
|
||||
item_code = make_item(properties={"is_stock_item": 1}).name
|
||||
po = create_purchase_order(item_code=item_code, qty=1, rate=100)
|
||||
|
||||
# The PO may auto-insert an Item Price at 100; bump it to the newer, higher rate.
|
||||
item_price = frappe.db.get_value(
|
||||
"Item Price", {"item_code": item_code, "price_list": "Standard Buying"}
|
||||
)
|
||||
if item_price:
|
||||
frappe.db.set_value("Item Price", item_price, "price_list_rate", 120)
|
||||
else:
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Item Price",
|
||||
"price_list": "Standard Buying",
|
||||
"item_code": item_code,
|
||||
"price_list_rate": 120,
|
||||
}
|
||||
).insert()
|
||||
|
||||
pr = make_purchase_receipt(po.name)
|
||||
pr.insert()
|
||||
|
||||
def fetch_price_list_rate():
|
||||
ctx = frappe._dict(
|
||||
{
|
||||
"item_code": item_code,
|
||||
"doctype": "Purchase Receipt",
|
||||
"name": pr.name,
|
||||
"company": pr.company,
|
||||
"supplier": pr.supplier,
|
||||
"currency": pr.currency,
|
||||
"conversion_rate": 1.0,
|
||||
"price_list": "Standard Buying",
|
||||
"price_list_currency": pr.currency,
|
||||
"plc_conversion_rate": 1.0,
|
||||
"warehouse": pr.items[0].warehouse,
|
||||
"uom": pr.items[0].uom,
|
||||
"stock_uom": pr.items[0].stock_uom,
|
||||
"qty": pr.items[0].qty,
|
||||
"child_doctype": pr.items[0].doctype,
|
||||
"child_docname": pr.items[0].name,
|
||||
"is_return": 0,
|
||||
"is_internal_supplier": 0,
|
||||
"ignore_pricing_rule": 1,
|
||||
}
|
||||
)
|
||||
return get_item_details(ctx, pr).get("price_list_rate")
|
||||
|
||||
# Rate stays at the PO rate; the newer Item Price (120) is not fetched.
|
||||
self.assertEqual(fetch_price_list_rate(), 100)
|
||||
|
||||
# Control: without the setting the newer Item Price would be fetched.
|
||||
set_maintain_same_rate(0)
|
||||
self.assertEqual(fetch_price_list_rate(), 120)
|
||||
|
||||
Reference in New Issue
Block a user