Merge pull request #46317 from frappe/mergify/bp/version-15-hotfix/pr-45947

fix: set landed cost based on purchase invoice rate (backport #45947)
This commit is contained in:
Mihir Kandoi
2025-03-05 16:58:41 +05:30
committed by GitHub
8 changed files with 221 additions and 21 deletions

View File

@@ -2482,6 +2482,76 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
item.reload()
self.assertEqual(item.last_purchase_rate, 0)
def test_adjust_incoming_rate_from_pi_with_multi_currency_and_partial_billing(self):
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
pr = make_purchase_receipt(
qty=10, rate=10, currency="USD", do_not_save=1, supplier="_Test Supplier USD"
)
pr.conversion_rate = 5300
pr.save()
pr.submit()
incoming_rate = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"incoming_rate",
)
self.assertEqual(incoming_rate, 53000) # Asserting to confirm if the default calculation is correct
pi = create_purchase_invoice_from_receipt(pr.name)
for row in pi.items:
row.qty = 1
pi.save()
pi.submit()
incoming_rate = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"incoming_rate",
)
# Test 1 : Incoming rate should not change as only the qty has changed and not the rate (this was not the case before)
self.assertEqual(incoming_rate, 53000)
pi = create_purchase_invoice_from_receipt(pr.name)
for row in pi.items:
row.qty = 1
row.rate = 9
pi.save()
pi.submit()
incoming_rate = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"incoming_rate",
)
# Test 2 : Rate in new PI is lower than PR, so incoming rate should also be lower
self.assertEqual(incoming_rate, 50350)
pi = create_purchase_invoice_from_receipt(pr.name)
for row in pi.items:
row.qty = 1
row.rate = 12
pi.save()
pi.submit()
incoming_rate = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"incoming_rate",
)
# Test 3 : Rate in new PI is higher than PR, so incoming rate should also be higher
self.assertEqual(incoming_rate, 54766.667)
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
def test_opening_invoice_rounding_adjustment_validation(self):
pi = make_purchase_invoice(do_not_save=1)
pi.items[0].rate = 99.98

View File

@@ -333,7 +333,7 @@ class BuyingController(SubcontractingController):
net_rate
+ item.item_tax_amount
+ flt(item.landed_cost_voucher_amount)
+ flt(item.get("rate_difference_with_purchase_invoice"))
+ flt(item.get("amount_difference_with_purchase_invoice"))
) / qty_in_stock_uom
else:
item.valuation_rate = 0.0

View File

@@ -398,3 +398,5 @@ erpnext.patches.v14_0.update_posting_datetime
erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes
erpnext.patches.v15_0.rename_sla_fields
erpnext.patches.v15_0.update_query_report
erpnext.patches.v15_0.rename_field_from_rate_difference_to_amount_difference
erpnext.patches.v15_0.recalculate_amount_difference_field

View File

@@ -0,0 +1,80 @@
import frappe
from frappe.query_builder.functions import Sum
from frappe.utils import flt
from erpnext.accounts.utils import get_fiscal_year
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import adjust_incoming_rate_for_pr
def execute():
table = frappe.qb.DocType("Purchase Receipt Item")
parent = frappe.qb.DocType("Purchase Receipt")
query = (
frappe.qb.from_(table)
.join(parent)
.on(table.parent == parent.name)
.select(
table.parent,
table.name,
table.amount,
table.billed_amt,
table.amount_difference_with_purchase_invoice,
table.rate,
table.qty,
parent.conversion_rate,
)
.where((table.amount_difference_with_purchase_invoice != 0) & (table.docstatus == 1))
)
try:
if fiscal_year_dates := get_fiscal_year(frappe.utils.datetime.date.today()):
query.where(parent.posting_date.between(fiscal_year_dates[1], fiscal_year_dates[2]))
except Exception:
return
if result := query.run(as_dict=True):
item_wise_billed_qty = get_billed_qty_against_purchase_receipt([item.name for item in result])
purchase_receipts = set()
precision = frappe.get_precision("Purchase Receipt Item", "amount")
for item in result:
adjusted_amt = 0.0
if (
item.billed_amt is not None
and item.amount is not None
and item_wise_billed_qty.get(item.name)
):
adjusted_amt = (
flt(item.billed_amt / item_wise_billed_qty.get(item.name)) - flt(item.rate)
) * item.qty
adjusted_amt = flt(
adjusted_amt * flt(item.conversion_rate),
precision,
)
if adjusted_amt != item.amount_difference_with_purchase_invoice:
frappe.db.set_value(
"Purchase Receipt Item",
item.name,
"amount_difference_with_purchase_invoice",
adjusted_amt,
update_modified=False,
)
purchase_receipts.add(item.parent)
for pr in purchase_receipts:
adjust_incoming_rate_for_pr(frappe.get_doc("Purchase Receipt", pr))
def get_billed_qty_against_purchase_receipt(pr_names):
table = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(table)
.select(table.pr_detail, Sum(table.qty).as_("qty"))
.where((table.pr_detail.isin(pr_names)) & (table.docstatus == 1))
)
invoice_data = query.run(as_list=1)
if not invoice_data:
return frappe._dict()
return frappe._dict(invoice_data)

View File

@@ -0,0 +1,17 @@
import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
frappe.db.set_value(
"DocField",
{"parent": "Purchase Receipt Item", "fieldname": "rate_difference_with_purchase_invoice"},
"label",
"Amount Difference with Purchase Invoice",
)
rename_field(
"Purchase Receipt Item",
"rate_difference_with_purchase_invoice",
"amount_difference_with_purchase_invoice",
)
frappe.clear_cache(doctype="Purchase Receipt Item")

View File

@@ -424,6 +424,14 @@ class PurchaseReceipt(BuyingController):
self.delete_auto_created_batches()
self.set_consumed_qty_in_subcontract_order()
def before_cancel(self):
super().before_cancel()
self.remove_amount_difference_with_purchase_invoice()
def remove_amount_difference_with_purchase_invoice(self):
for item in self.items:
item.amount_difference_with_purchase_invoice = 0
def get_gl_entries(self, warehouse_account=None, via_landed_cost_voucher=False):
from erpnext.accounts.general_ledger import process_gl_map
@@ -571,15 +579,15 @@ class PurchaseReceipt(BuyingController):
item=item,
)
def make_rate_difference_entry(item):
if item.rate_difference_with_purchase_invoice and stock_asset_rbnb:
def make_amount_difference_entry(item):
if item.amount_difference_with_purchase_invoice and stock_asset_rbnb:
account_currency = get_account_currency(stock_asset_rbnb)
self.add_gl_entry(
gl_entries=gl_entries,
account=stock_asset_rbnb,
cost_center=item.cost_center,
debit=0.0,
credit=flt(item.rate_difference_with_purchase_invoice),
credit=flt(item.amount_difference_with_purchase_invoice),
remarks=_("Adjustment based on Purchase Invoice rate"),
against_account=stock_asset_account_name,
account_currency=account_currency,
@@ -612,7 +620,7 @@ class PurchaseReceipt(BuyingController):
+ flt(item.landed_cost_voucher_amount)
+ flt(item.rm_supp_cost)
+ flt(item.item_tax_amount)
+ flt(item.rate_difference_with_purchase_invoice)
+ flt(item.amount_difference_with_purchase_invoice)
)
divisional_loss = flt(
@@ -712,7 +720,7 @@ class PurchaseReceipt(BuyingController):
make_item_asset_inward_gl_entry(d, stock_value_diff, stock_asset_account_name)
outgoing_amount = make_stock_received_but_not_billed_entry(d)
make_landed_cost_gl_entries(d)
make_rate_difference_entry(d)
make_amount_difference_entry(d)
make_sub_contracting_gl_entries(d)
make_divisional_loss_gl_entry(d, outgoing_amount)
elif (d.warehouse and d.warehouse not in warehouse_with_no_account) or (
@@ -1094,11 +1102,19 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
if adjust_incoming_rate:
adjusted_amt = 0.0
if item.billed_amt is not None and item.amount is not None:
adjusted_amt = flt(item.billed_amt) - flt(item.amount)
item_wise_billed_qty = get_billed_qty_against_purchase_receipt(pr_doc)
adjusted_amt = adjusted_amt * flt(pr_doc.conversion_rate)
item.db_set("rate_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
if (
item.billed_amt is not None
and item.amount is not None
and item_wise_billed_qty.get(item.name)
):
adjusted_amt = (
flt(item.billed_amt / item_wise_billed_qty.get(item.name)) - flt(item.rate)
) * item.qty
adjusted_amt = flt(adjusted_amt * flt(pr_doc.conversion_rate), item.precision("amount"))
item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6)
pr_doc.db_set("per_billed", percent_billed)
@@ -1111,6 +1127,21 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
adjust_incoming_rate_for_pr(pr_doc)
def get_billed_qty_against_purchase_receipt(pr_doc):
pr_names = [d.name for d in pr_doc.items]
table = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(table)
.select(table.pr_detail, fn.Sum(table.qty).as_("qty"))
.where((table.pr_detail.isin(pr_names)) & (table.docstatus == 1))
)
invoice_data = query.run(as_list=1)
if not invoice_data:
return frappe._dict()
return frappe._dict(invoice_data)
def adjust_incoming_rate_for_pr(doc):
doc.update_valuation_rate(reset_outgoing_rate=False)

View File

@@ -71,7 +71,7 @@
"item_tax_amount",
"rm_supp_cost",
"landed_cost_voucher_amount",
"rate_difference_with_purchase_invoice",
"amount_difference_with_purchase_invoice",
"billed_amt",
"warehouse_and_reference",
"warehouse",
@@ -998,14 +998,6 @@
"label": "Has Item Scanned",
"read_only": 1
},
{
"fieldname": "rate_difference_with_purchase_invoice",
"fieldtype": "Currency",
"label": "Rate Difference with Purchase Invoice",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
@@ -1135,12 +1127,20 @@
"no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1
},
{
"fieldname": "amount_difference_with_purchase_invoice",
"fieldtype": "Currency",
"label": "Amount Difference with Purchase Invoice",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-07-19 12:14:21.521466",
"modified": "2025-02-17 13:15:36.692202",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",

View File

@@ -16,6 +16,7 @@ class PurchaseReceiptItem(Document):
allow_zero_valuation_rate: DF.Check
amount: DF.Currency
amount_difference_with_purchase_invoice: DF.Currency
apply_tds: DF.Check
asset_category: DF.Link | None
asset_location: DF.Link | None
@@ -76,7 +77,6 @@ class PurchaseReceiptItem(Document):
qty: DF.Float
quality_inspection: DF.Link | None
rate: DF.Currency
rate_difference_with_purchase_invoice: DF.Currency
rate_with_margin: DF.Currency
received_qty: DF.Float
received_stock_qty: DF.Float