fix: GL entries for different exchange rate in the purchase invoice

(cherry picked from commit a953709640)
This commit is contained in:
Rohit Waghchaure
2026-04-06 14:15:03 +05:30
committed by Mergify
parent 2de51be5ae
commit 5719992cda
5 changed files with 178 additions and 24 deletions

View File

@@ -983,6 +983,10 @@ class PurchaseInvoice(BuyingController):
if provisional_accounting_for_non_stock_items:
self.get_provisional_accounts()
adjust_incoming_rate = frappe.db.get_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
)
for item in self.get("items"):
if flt(item.base_net_amount) or (self.get("update_stock") and item.valuation_rate):
if item.item_code:
@@ -1161,7 +1165,11 @@ class PurchaseInvoice(BuyingController):
)
# check if the exchange rate has changed
if item.get("purchase_receipt") and self.auto_accounting_for_stock:
if (
not adjust_incoming_rate
and item.get("purchase_receipt")
and self.auto_accounting_for_stock
):
if (
exchange_rate_map[item.purchase_receipt]
and self.conversion_rate != exchange_rate_map[item.purchase_receipt]
@@ -1198,6 +1206,7 @@ class PurchaseInvoice(BuyingController):
item=item,
)
)
if (
self.auto_accounting_for_stock
and self.is_opening == "No"

View File

@@ -350,6 +350,12 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
make_purchase_invoice as create_purchase_invoice,
)
original_value = frappe.db.get_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
)
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
pr = make_purchase_receipt(
company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1",
@@ -368,14 +374,19 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
# fetching the latest GL Entry with exchange gain and loss account account
amount = frappe.db.get_value(
"GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name}, "credit"
"GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name}, "debit"
)
discrepancy_caused_by_exchange_rate_diff = abs(
pi.items[0].base_net_amount - pr.items[0].base_net_amount
)
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
frappe.db.set_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", original_value
)
def test_purchase_invoice_with_exchange_rate_difference_for_non_stock_item(self):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as create_purchase_invoice,

View File

@@ -167,6 +167,15 @@ def create_supplier(**args):
if not args.without_supplier_group:
doc.supplier_group = args.supplier_group or "Services"
if args.get("party_account"):
doc.append(
"accounts",
{
"company": frappe.db.get_value("Account", args.get("party_account"), "company"),
"account": args.get("party_account"),
},
)
doc.insert()
return doc

View File

@@ -1259,11 +1259,11 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
total_amount, total_billed_amount, pi_landed_cost_amount = 0, 0, 0
item_wise_returned_qty = get_item_wise_returned_qty(pr_doc)
billed_qty_amt = frappe._dict()
if adjust_incoming_rate:
item_wise_billed_qty = get_billed_qty_against_purchase_receipt(pr_doc)
billed_qty_based_on_po = get_billed_qty_against_purchase_order(pr_doc)
billed_qty_amt = get_billed_qty_amount_against_purchase_receipt(pr_doc)
billed_qty_amt_based_on_po = get_billed_qty_amount_against_purchase_order(pr_doc)
for item in pr_doc.items:
returned_qty = flt(item_wise_returned_qty.get(item.name))
@@ -1293,22 +1293,46 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
item.billed_amt is not None
and item.amount is not None
and (
item_wise_billed_qty.get(item.name)
or billed_qty_based_on_po.get(item.purchase_order_item)
billed_qty_amt.get(item.name) or billed_qty_amt_based_on_po.get(item.purchase_order_item)
)
):
qty = item_wise_billed_qty.get(item.name)
if not qty:
if item.qty < billed_qty_based_on_po.get(item.purchase_order_item):
qty = None
if billed_qty_amt.get(item.name):
qty = billed_qty_amt.get(item.name).get("qty")
if not qty and billed_qty_amt_based_on_po.get(item.purchase_order_item):
if item.qty < billed_qty_amt_based_on_po.get(item.purchase_order_item)["qty"]:
qty = item.qty
else:
qty = billed_qty_based_on_po.get(item.purchase_order_item)
qty = billed_qty_amt_based_on_po.get(item.purchase_order_item)["qty"]
billed_qty_based_on_po[item.purchase_order_item] -= qty
billed_qty_amt_based_on_po[item.purchase_order_item]["qty"] -= qty
adjusted_amt = (flt(item.billed_amt / qty) - flt(item.rate)) * item.qty
billed_amt = item.billed_amt
if billed_qty_amt.get(item.name):
billed_amt = flt(billed_qty_amt.get(item.name).get("amount"))
elif billed_qty_amt_based_on_po.get(item.purchase_order_item):
total_billed_qty = (
billed_qty_amt_based_on_po.get(item.purchase_order_item).get("qty") + qty
)
adjusted_amt = flt(adjusted_amt * flt(pr_doc.conversion_rate), item.precision("amount"))
if total_billed_qty:
billed_amt = flt(
flt(billed_qty_amt_based_on_po.get(item.purchase_order_item).get("amount"))
* (qty / total_billed_qty)
)
else:
billed_amt = 0.0
# Reduce billed amount based on PO for next iterations
billed_qty_amt_based_on_po[item.purchase_order_item]["amount"] -= billed_amt
if qty:
adjusted_amt = (
flt(billed_amt / qty) - (flt(item.rate) * flt(pr_doc.conversion_rate))
) * item.qty
adjusted_amt = flt(adjusted_amt, item.precision("amount"))
pi_landed_cost_amount += adjusted_amt
item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
elif amount and item.billed_amt > amount:
@@ -1337,23 +1361,40 @@ 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):
def get_billed_qty_amount_against_purchase_receipt(pr_doc):
pr_names = [d.name for d in pr_doc.items]
parent_table = frappe.qb.DocType("Purchase Invoice")
table = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(table)
.select(table.pr_detail, fn.Sum(table.qty).as_("qty"))
frappe.qb.from_(parent_table)
.inner_join(table)
.on(parent_table.name == table.parent)
.select(
table.pr_detail,
fn.Sum(table.amount * parent_table.conversion_rate).as_("amount"),
fn.Sum(table.qty).as_("qty"),
)
.where((table.pr_detail.isin(pr_names)) & (table.docstatus == 1))
.groupby(table.pr_detail)
)
invoice_data = query.run(as_list=1)
invoice_data = query.run(as_dict=1)
if not invoice_data:
return frappe._dict()
return frappe._dict(invoice_data)
billed_qty_amt = frappe._dict()
for row in invoice_data:
if row.pr_detail not in billed_qty_amt:
billed_qty_amt[row.pr_detail] = {"amount": 0, "qty": 0}
billed_qty_amt[row.pr_detail]["amount"] += flt(row.amount)
billed_qty_amt[row.pr_detail]["qty"] += flt(row.qty)
return billed_qty_amt
def get_billed_qty_against_purchase_order(pr_doc):
def get_billed_qty_amount_against_purchase_order(pr_doc):
po_names = list(
set(
[
@@ -1366,15 +1407,32 @@ def get_billed_qty_against_purchase_order(pr_doc):
invoice_data_po_based = frappe._dict()
if po_names:
parent_table = frappe.qb.DocType("Purchase Invoice")
table = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(table)
.select(table.po_detail, fn.Sum(table.qty).as_("qty"))
frappe.qb.from_(parent_table)
.inner_join(table)
.on(parent_table.name == table.parent)
.select(
table.po_detail,
fn.Sum(table.qty).as_("qty"),
fn.Sum(table.amount * parent_table.conversion_rate).as_("amount"),
)
.where((table.po_detail.isin(po_names)) & (table.docstatus == 1) & (table.pr_detail.isnull()))
.groupby(table.po_detail)
)
invoice_data_po_based = query.run(as_list=1)
invoice_data_po_based = frappe._dict(invoice_data_po_based)
invoice_data = query.run(as_dict=1)
if not invoice_data:
return frappe._dict()
for row in invoice_data:
if row.po_detail not in invoice_data_po_based:
invoice_data_po_based[row.po_detail] = {"amount": 0, "qty": 0}
invoice_data_po_based[row.po_detail]["amount"] += flt(row.amount)
invoice_data_po_based[row.po_detail]["qty"] += flt(row.qty)
return invoice_data_po_based

View File

@@ -5450,6 +5450,70 @@ class TestPurchaseReceipt(ERPNextTestSuite):
self.assertEqual(pr.total_qty, 12)
self.assertEqual(pr.total, 120)
def test_different_exchange_rate_in_pr_and_pi(self):
from erpnext.accounts.doctype.account.test_account import create_account
original_value = frappe.db.get_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
)
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
party_account = create_account(
account_name="USD Party Account Creditors",
parent_account="Accounts Payable - TCP1",
account_type="Payable",
company="_Test Company with perpetual inventory",
account_currency="USD",
)
supplier = create_supplier(
supplier_name="_Test USD Supplier New 1", default_currency="USD", party_account=party_account
).name
item_code = make_item("Test Item for Different Exchange Rate", {"is_stock_item": 1}).name
pr = make_purchase_receipt(
item_code=item_code,
qty=1,
currency="USD",
conversion_rate=80,
rate=100,
company="_Test Company with perpetual inventory",
warehouse=frappe.get_value(
"Warehouse", {"company": "_Test Company with perpetual inventory"}, "name"
),
supplier=supplier,
)
self.assertEqual(pr.currency, "USD")
self.assertEqual(pr.conversion_rate, 80)
gl_entries = get_gl_entries(pr.doctype, pr.name)
self.assertTrue(len(gl_entries) == 2)
for row in gl_entries:
amount = row.credit or row.debit
self.assertEqual(amount, 8000.0)
pi = make_purchase_invoice(pr.name)
pi.conversion_rate = 90
pi.currency = "USD"
pi.save()
pi.submit()
gl_entries = get_gl_entries(pi.doctype, pi.name)
self.assertTrue(len(gl_entries) == 2)
accounts = ["USD Party Account Creditors - TCP1", "Stock Received But Not Billed - TCP1"]
for row in gl_entries:
amount = row.credit or row.debit
self.assertEqual(amount, 9000.0)
self.assertTrue(row.account in accounts)
frappe.db.set_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", original_value
)
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
@@ -5620,6 +5684,9 @@ def make_purchase_receipt(**args):
pr.return_against = args.return_against
pr.apply_putaway_rule = args.apply_putaway_rule
if args.get("conversion_rate") is not None:
pr.conversion_rate = args.conversion_rate
qty = args.qty if args.qty is not None else 5
rejected_qty = args.rejected_qty or 0
received_qty = args.received_qty or flt(rejected_qty) + flt(qty)