Skip to main content

Manufacturing & distribution

ERPNext Landed Cost & FX Gone Wrong: The Interactive Fix Guide (2026)

Exchange rates move between PO, GRN and invoice, and ERPNext inventory drifts if the GRN keeps the PO rate. What breaks, how to fix it, how to prevent it.

Updated 12 Oct 202612 min readBy ManojFirst published 23 Jul 2026

When you import goods on ERPNext, the exchange rates on your Purchase Order, Goods Receipt (GRN) and Purchase Invoice are three different things, and only the GRN's rate values your stock. If the GRN quietly inherits the PO's stale rate, your inventory is wrong the moment you submit. This guide walks through the behaviour against the ERPNext v16.28.0 source, the correct workflow, a live calculation of your exposure, and an automation check so it doesn't recur.

An import that lands at a different exchange rate from the PO estimate is a common cause of quiet inventory drift in ERPNext. I lead ERPNext and Frappe programmes at MithTech, a team of Frappe specialists in Bengaluru, and we use this approach with clients who import.

Try it: what is one wrong rate costing you?

Set the numbers to your last import. If the delta is bigger than your accountant's tolerance, the section on fixing historical errors further down shows how to close it.

CalculatorFX Impact Calculator

Live calculator for the base-currency delta between an FX rate you booked and the rate that should have applied on receipt day.

Booked in stock ledger

₹4,920,000

Should have been

₹5,064,000

Missing from inventory

₹144,000

Three documents, three different roles

The heart of the confusion is that Purchase Order, Purchase Receipt, and Purchase Invoice all carry a conversion_rate field, and users assume they mean the same thing. They don't. Tap through the workflow below to see what each stage does to your books.

InteractivePO → GRN → LCV → PI Walkthrough

Click each stage to see what it actually posts to your GL and stock ledger.

Freezes stock valuation

Your inventory rate for life

The Purchase Order's rate is a commitment estimate

The PO's conversion_rate has no accounting impact. No GL entries, no stock ledger entries. It exists so that base-currency totals can be shown on the PO for approval purposes, and so that downstream documents have a default to inherit.

That "default to inherit" behaviour is the root cause of most FX-related landed-cost bugs. Users assume the rate was recalculated at receipt time. It wasn't: it was copied.

The Purchase Receipt's rate is what values your stock

This is the central point of the post.

When you submit a GRN, ERPNext writes a Stock Ledger Entry for each item. The valuation stored is:

stock_value = qty × rate × conversion_rate   (in company / base currency)

That value is frozen into the ledger. Nothing downstream will retroactively touch it. The only mechanisms that change stock valuation after a GRN is submitted are a Landed Cost Voucher or a repost of the item's valuation.

Practical consequence: if the GRN carries the wrong exchange rate at submit time, your inventory is now wrong. Fixing the Purchase Invoice will not fix your inventory.

The Purchase Invoice's rate hits the payable and a P&L account

The PI's conversion_rate determines the rupee value of the supplier's payable (correct: you owe them at that day's rate) and posts an Exchange Gain/Loss GL entry for the delta between the PR's rate and the PI's rate.

You can see this in the ERPNext source itself. In erpnext/stock/doctype/purchase_receipt/purchase_receipt.py around line 588, when GL entries are being built for a PR that has a linked PI:

Python
if (
    exchange_rate_map[item.purchase_invoice]
    and self.conversion_rate != exchange_rate_map[item.purchase_invoice]
    and item.net_rate == net_rate_map[item.purchase_invoice_item]
):
    discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * (
        exchange_rate_map[item.purchase_invoice] - self.conversion_rate
    )
    # ... posts one leg to Stock Received But Not Billed
    # ... posts the other leg to Exchange Gain/Loss

The delta is booked to P&L against the company's default Exchange Gain/Loss account. It is not added to inventory.

InteractiveDefault vs Correct Habit Comparator

Side-by-side of the silent-drift default and the receipt-day-override habit.

The default (bad)
GRN inherits PO rate silently
  • Stock ledger frozen at stale rate
  • Landed cost silently understated / overstated
  • Auditor discovers it months later
  • Correction requires reposting
The habit (good)
Override GRN rate to receipt-day spot
  • Inventory valued at receipt-date FX (Ind AS 21)
  • PI books FX delta to P&L automatically
  • No LCV cleanup needed later
  • Gate script prevents regression

The Landed Cost Voucher and the "double stock" myth

Two half-truths come up again and again in forum threads about landed cost on imports. Here is what the source says about each.

Why the LCV button is hidden on a plain PI

Look at erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js — the button is registered conditionally:

JavaScript
if (frm.doc.docstatus === 1 && frm.doc.update_stock) {
    frm.add_custom_button(
        __("Landed Cost Voucher"),
        // ...
    );
}

The condition is deliberate. An LCV allocates cost across items that actually moved into stock. A PI without Update Stock posted no Stock Ledger Entries, so there is nothing to allocate against.

The correct answer is not to force the button onto the PI. It is to create the LCV against the Purchase Receipt, because that is the document that posted stock.

The "duplicate SLEs" claim, checked against source

The claim: "tick Update Stock on a PI while a GRN already exists and you double your stock." That is not what happens in a properly-linked flow. From erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:

Python
def validate_purchase_receipt_if_update_stock(self):
    if self.update_stock:
        for item in self.get("items"):
            if item.purchase_receipt:
                frappe.throw(
                    _("Stock cannot be updated for Purchase Invoice {0} "
                      "because a Purchase Receipt {1} has already been created "
                      "for this transaction. Please disable the 'Update Stock' "
                      "checkbox in the Purchase Invoice and save the invoice."),
                    title=_("Stock Update Not Allowed"),
                )

If any PI item has a purchase_receipt linked, update_stock=1 throws. If you are seeing duplicate SLEs, one of these is true:

  • The PI was created standalone (from the PO, or by hand) with Update Stock ticked, and a separate GRN was raised against the same PO. item.purchase_receipt was empty, so the guard never fired.
  • A customisation in your instance has overridden the validator.
  • The GRN and PI reference different rows and the link didn't propagate.

The fix is process, not code: always create the PI from the GRN, not from the PO, when a GRN exists.

What is the correct workflow, end to end?

1

Purchase Order: an estimate, not a valuation

Enter your best guess of the FX rate. It has no downstream accounting weight. Approvals happen against a base-currency number computed from this rate; nothing else uses it.

2

Purchase Receipt (GRN): override the rate

The most important habit. Before you submit, replace whatever ERPNext fetched from the PO with the actual spot rate on the receipt date: the FBIL reference rate, your bank's TT rate, or whatever your accounting policy sets. This is the number that lands in your Stock Ledger for life.

3

Landed Cost Voucher (against the GRN)

Add freight, customs duty, CHA, insurance and port handling: anything that should be part of inventory cost. Distribute by amount or by quantity per policy. The LCV re-writes SLEs and revalues stock.

4

Purchase Invoice: pull from the GRN

Use Get Items From → Purchase Receipt. This wires purchase_receipt on every line so the validator can protect you. Leave Update Stock off. Enter the actual FX rate on the invoice date. The difference between the GRN rate and the PI rate goes to Exchange Gain/Loss automatically, where it belongs.

Inventory ends up valued at receipt-date FX. Payable ends up valued at invoice-date FX. The difference sits in P&L instead of in stock cost.

What if stores does GRN and accounts owns FX?

The workflow above assumes the person receiving the goods can also set an authoritative exchange rate. In many companies that isn't how it works: the person who opens the container shouldn't be deciding which reference rate applies. That is the accounts team's call. Here is a split that works.

1

Stores books the GRN as-is

Stores submits the Purchase Receipt at whatever conversion_rate propagated from the PO. No FX judgement on their side. Their job is to check quantity and quality against the PO, not to set exchange rates. The GRN posts stock at a rate that everyone acknowledges is provisional.

2

Accounts works from a saved 'GRNs Awaiting FX Correction' report

Build a saved Report Builder view, or a small custom Query Report, filtered to Purchase Receipt where docstatus=1 and currency != 'INR' and posting_date in current period, and no linked FX-correction LCV yet. This is the accounts team's daily worklist. One view, one filter, no chasing.

3

Accounts opens each GRN, checks the true receipt-date rate

Against the company's stated policy (the FBIL reference rate on the receipt date, the bank's TT buying rate, or whatever the accounting manual says). If the delta between the GRN's rate and the policy rate is material by the company's threshold (see When is any of this actually material? below), accounts creates a Landed Cost Voucher against the GRN with a single "FX Rate Correction" charge line. If it isn't material, they mark the GRN reviewed and move on.

4

Accounts books the Purchase Invoice separately

Pulled from the GRN via Get Items From, Update Stock OFF, at the actual invoice-date FX rate. Any residual FX difference between receipt-date and invoice-date auto-posts to Exchange Gain/Loss, and that residual is a real P&L item that the standards want in P&L (see the standards box above). Accounts doesn't need to touch it.

At high import volumes you can partially automate step 3: a server script pre-fills a draft LCV from a rate source (the FBIL reference rate, a rate API, or your bank's daily rate email), and accounts only reviews and submits. See the automation section below; the Server Script (Auditor) recipe is close to what you'd extend for this.

Two things this split gets right that a naive "everyone always overrides on the GRN" model doesn't:

  • Segregation of duties is preserved. Stores can't move accounting numbers around; accounts can't book stock without a GRN. Both keep their scope.
  • The audit trail is clean. Every FX correction is its own dated LCV with a clear description and, ideally, a reference-rate source attached — instead of a hand-edit of the GRN's conversion_rate by whoever was on shift.

How do you fix historical errors?

If you are here because the books are already wrong, pick your scenario; the widget below shows the fix.

InteractiveScenario Decider

Pick your situation (Scenario A, B, C, or D) — get the specific ERPNext fix path.

Choose a scenario above to reveal the exact fix path.

Scenario B, worked live

If your GRN is submitted with a stale rate and the stock is still on hand (or partially consumed), you can top up the valuation with a second LCV against the GRN. Set the inputs to your invoice and the numbers update.

CalculatorLCV Top-Up Calculator

Enter qty, unit price, stale GRN rate and correct spot rate — get the exact 'FX Rate Correction' amount to post as an LCV.

Per-unit adjustment

$10 × (84 − 82) = ₹20

Total LCV charge

100 × ₹20 = ₹2,000

Post this as a Landed Cost Voucher
  • Description: FX Rate Correction
  • Amount: ₹2,000 (top-up)
  • Account: Exchange Gain/Loss
  • Distribute: By Amount
  • Attach against the affected GRN

ERPNext will then:

  • Increase each item's valuation by its share of the top-up.
  • Repost outgoing SLEs so any units already sold or consumed pick up the corrected valuation, flowing the difference to COGS.
  • Skip already-closed accounting periods if your policy blocks reposting into them (see Scenario C).

Which account do I post the LCV to, and why?

This trips up accountants the first time they run the procedure, and it matters more than it looks.

Recommended default: use the company's existing Default Exchange Gain / Loss Account, the one you already confirmed was set in the pre-flight check above (Company master → Exchange Gain / Loss Section → Exchange Gain / Loss Account, typically an Indirect Expenses head named "Exchange Gain/Loss" or "Foreign Exchange Gain/Loss"). No new account head required.

Why this account and not another FX expense account: ERPNext already auto-posts an FX difference entry to this account when the PI's conversion_rate differs from the GRN's. That is the purchase_receipt.py:588 code path cited earlier, whose debit leg is account=self.get_company_default("exchange_gain_loss_account"). If your manual LCV correction uses the same account, the two entries offset each other. Net P&L is clean, stock ends up at the true receipt-date rate, and the residual balance in Exchange Gain/Loss reflects only the real movement between receipt date and invoice date, which is what Ind AS 21, IAS 21 and ASC 830 want in P&L.

The offsetting mechanics, walked through numerically

Take the worked example above: 100 units, USD 10 unit price, GRN submitted at ₹82, correct receipt-date rate ₹84. Assume the invoice arrives later at ₹84 as well (so the invoice-date and receipt-date rates match; we're isolating just the GRN-was-stale problem). Three postings happen, in order:

1. GRN (submitted with the stale ₹82 rate)

Dr Stock in Hand                             82,000
    Cr Stock Received But Not Billed (SRBNB)  82,000

2. Purchase Invoice at ₹84 — ERPNext auto-posts the FX discrepancy

Dr Stock Received But Not Billed (SRBNB)     82,000
Dr Exchange Gain/Loss                         2,000
    Cr Supplier (Payable)                     84,000

The Exchange Gain/Loss debit here is the entry made by the purchase_receipt.py:588 code path: ERPNext books the FX difference itself. It hits P&L, not stock, as the standards want.

3. LCV correction (this post's fix, posted to the same account)

Dr Stock in Hand                              2,000
    Cr Exchange Gain/Loss                     2,000

Net effect across all three entries:

AccountDrCrNet
Stock in Hand84,000—84,000 Dr ✓
Stock Received But Not Billed82,00082,0000 ✓
Supplier / Payable—84,00084,000 Cr ✓
Exchange Gain/Loss2,0002,0000 ✓

That is identical to what a correctly booked GRN at ₹84 would have produced. That is what makes the procedure defensible to an auditor: the correction plus ERPNext's own auto-post reach the same ledger state as a flow done right first time. The Exchange Gain/Loss account carries only the real movement between receipt and invoice dates (in this example none, so it nets to zero).

If the invoice-date rate had been ₹84.60 instead of ₹84, the result would be almost the same, except that a real residual of USD 1,000 × ₹0.60 = ₹600 would sit in Exchange Gain/Loss at the end, representing real remeasurement of the monetary payable. That's a real P&L item and belongs there.

When to create a dedicated account instead

Optional, not required. Consider a separate account (for example "Exchange Rate Adjustment, Imports" under Indirect Expenses) if:

  • Your auditor explicitly wants GRN-rate corrections separated from real remeasurement gain/loss (some do, especially in first-year Ind AS conversions).
  • Your import volume is high enough that noise from these corrections would swamp the Exchange Gain/Loss account and obscure real FX movement.
  • Management wants a figure for the "cost of stale PO rates": a running P&L number that quantifies process slippage in the buying team.

Same accounting treatment, kept apart for reporting. If you go this route, you'll no longer get the automatic self-cancellation with ERPNext's PI auto-post; you accept that in exchange for the visibility.

Do not post the LCV correction to any of these

These are the usual wrong choices:

  • Not COGS. The correction is a valuation adjustment on stock still on hand, not a consumption event. COGS is only touched later, if ERPNext reposts outgoing SLEs for units that have already been sold.
  • Not Stock Adjustment. The LCV's debit already goes to stock; crediting Stock Adjustment mixes an exchange-rate correction with count differences and hides its cause.
  • Not the supplier / creditor account. The supplier's payable was correctly booked at the invoice-date rate on the PI; touching it retrospectively breaks the payables sub-ledger.
  • Not bank charges. Real FX remeasurement is not a bank charge, and neither is a GRN correction. Bank charges are the fee your bank actually took, which is a separate PI or Journal Entry.

The rest of the FX story: payment, advances, period-end

Once the GRN, LCV and PI behave, three more FX events matter for an import.

Payment to the supplier weeks later: the Payment Entry posts more FX

You pay the supplier the USD 1,000 on a day when USD/INR is ₹84.90, so ₹84,900 leaves your bank. ERPNext's Payment Entry compares your bank's actual outflow to the payable it's settling (which sits at the PI's ₹84 rate) and posts the difference to the Default Exchange Gain/Loss account automatically. Nothing to do, but know that this is the third FX posting on the same shipment:

Dr Supplier (Payable) at PI rate      84,000
Dr Exchange Gain/Loss                    900  # USD 1000 × (84.90 − 84.00)
    Cr Bank Account                   84,900

If your books show a growing balance in Exchange Gain/Loss over the year, this is usually where it comes from: settling liabilities at rates different from when they were recognised. That is real remeasurement gain or loss, and the standards above want it in P&L.

Supplier advance in FX: the one people miss

If you pre-paid part of the invoice as an advance in USD (say USD 3,000 wired at ₹83), the Payment Entry booked that portion at ₹83. When the PI arrives later at ₹84 and you allocate the advance against it, ERPNext posts an FX difference only on the advance portion (USD 3,000 × (₹84 − ₹83) = ₹3,000) to Exchange Gain/Loss, and settles the rest at ₹84. Don't override the advance's rate by hand to match the PI: that breaks the payable sub-ledger and hides real FX movement. Let ERPNext handle it.

Period-end: Exchange Rate Revaluation on open FX payables

Ind AS 21, IAS 21, ASC 830 and FRS 102 all require you to remeasure monetary items denominated in a foreign currency at the closing rate on the balance sheet date, with the movement to P&L. In ERPNext that job is a dedicated DocType: Exchange Rate Revaluation (erpnext/accounts/doctype/exchange_rate_revaluation). At period end you enter the revaluation date, pick the accounts to revalue (typically your foreign-currency Creditors and Debtors accounts), ERPNext computes the delta against today's Currency Exchange rate for each open balance, and generates a Journal Entry booking the movement to Exchange Gain/Loss.

Two things to know:

  • It's a period-end job, not per-transaction. Run it as part of your month-end / quarter-end close checklist.
  • Unrealized vs realised. The revaluation posts to the unrealised Exchange Gain/Loss account (Company master → Unrealized Exchange Gain / Loss Account) if that field is populated; otherwise it falls back to the realised one. Set it if your auditor wants the split.

Landed-cost distribution: by amount, by qty, or by weight?

The workflow above says "distribute by amount or by quantity per policy". Which is right depends on the charge. A rule of thumb worth writing down:

Ocean / air freightNet weight (or qty if items are similar)Freight is priced by weight, not value
Insurance premiumAmount (item value)Insurance premium scales with declared value
Customs BCD (Basic Customs Duty)Amount, applied per HSN groupRate is per HSN; distribute inside each HSN cluster
CHA / clearing agentAmountFee tracks value moved, close enough
Inland transportWeight or qtyTrucker charges by weight / space
Bank charges on LC / TTAmount (invoice value)Bank fee tracks payment value, not qty

ERPNext v16 supports Distribute Charges Based On = Amount, Qty or Distribute Manually. For weight-based distribution you need a custom script on the LCV or a server script; talk to us if you want one. Getting freight distributed by amount instead of weight is a common source of per-unit landed cost being wrong on high-weight low-value SKUs (steel, chemicals, packaging).

Related but separate: IGST on imports

So nobody mixes this up with the FX story: on imports of goods you pay IGST at customs (section 3(7) of the Customs Tariff Act), and you claim it as input tax on the basis of the Bill of Entry, separately from the supplier invoice. That is a compliance flow, not an FX flow, and neither the LCV nor Exchange Gain/Loss touches it. If IGST amounts are landing in an LCV correction, the wires are crossed: book them in a Purchase Invoice or Journal Entry against your import IGST account instead, and check the treatment with your CA.

Scenario C: closed period? Book a manual JV

If the period is closed and your policy forbids reposting, don't reopen the period. Book a Journal Entry in the current period instead:

  • Dr. Cost of Goods Sold (or Stock-in-Hand for units still on the shelf)
  • Cr. Exchange Gain/Loss (or the account you'd have used on the LCV)

This preserves the closed period's integrity and puts the correction in the current period where an auditor expects it. Yes, ERPNext's inventory still shows the old per-unit valuation; that trade-off exists in every ERP.

How do gates and automation make the fix permanent?

You now know the workflow, but people forget. The answer is a check on the GRN so a stale rate can't be submitted. Pick the form that suits you: client script, server script, an app hook, or an n8n workflow. All four give the same guarantee.

Code recipeAutomation Gate Recipes

Four automations to copy that prevent a stale-rate GRN from ever being submitted again: client script, server script, hooks.py, and an n8n workflow.

Warn the user when the PO→GRN gap is > 7 days or the fetched rate is > 0.5% stale, before they submit the GRN.

client_script__gate_.js
// Custom Script → DocType: Purchase Receipt
frappe.ui.form.on('Purchase Receipt', {
  before_submit: async function(frm) {
    if (!frm.doc.conversion_rate || frm.doc.currency === 'INR') return;

    // 1. Reject if any linked PO is older than 7 days
    const po_names = [...new Set(frm.doc.items.map(i => i.purchase_order).filter(Boolean))];
    for (const po of po_names) {
      const r = await frappe.db.get_value('Purchase Order', po, 'transaction_date');
      const days = frappe.datetime.get_day_diff(frm.doc.posting_date, r.message.transaction_date);
      if (days > 7) {
        const ok = await new Promise(res =>
          frappe.confirm(
            `PO ${po} is ${days} days old. Re-check today's FX rate before submitting?`,
            () => res(true), () => res(false)
          )
        );
        if (!ok) frappe.throw('Submission cancelled — update FX rate first.');
      }
    }

    // 2. Fetch today's reference rate and compare
    const today = await frappe.db.get_value('Currency Exchange', {
      from_currency: frm.doc.currency, to_currency: 'INR',
      date: frm.doc.posting_date
    }, 'exchange_rate');

    const spot = today.message && today.message.exchange_rate;
    if (spot) {
      const drift = Math.abs(spot - frm.doc.conversion_rate) / spot * 100;
      if (drift > 0.5) {
        frappe.throw(`GRN rate ${frm.doc.conversion_rate} differs from today's ${spot} by ${drift.toFixed(2)}% — update before submit.`);
      }
    }
  }
});

What each gate actually enforces

Freshness check

Reject a GRN whose linked PO is older than 7 days without an FX-rate refresh confirmation. Removes the "just accept the default" reflex.

Drift threshold

Compare the GRN's conversion_rate against today's Currency Exchange entry. Reject on > 0.5% drift (or your policy threshold).

Reference rate pull

Nightly scheduler job that fetches a reference rate (FBIL, or a rate API) into Currency Exchange. Without this, your drift check has nothing to compare against.

Audit trail

on_submit snapshot of (doc.name, conversion_rate, reference_rate, drift_pct, user) into a custom FX Rate Snapshot DocType. Finance can review it weekly.

When is any of this material?

Fix immediatelyFix process, backlog the history
1% FX move on a ₹50L consignment = ₹50,000 in the wrong accountHigh-margin imports where FX ≪ contribution margin
Low-margin imports (chemicals, commodities, electronics)PO→GRN gap < 5 days on average
PO→GRN gap consistently > 2 weeksYou're mid-year-end and can't repost the SLEs anyway
Any FSA / audit engagement in the next 6 monthsThe delta rounds to noise on your P&L

Either way, the process fix costs a few seconds per GRN.

A concrete threshold you can lift into your SOP

If your accounts team is going to run the "GRNs Awaiting FX Correction" report daily (see the org-workflow section above), they need a written policy on when to raise an LCV and when to let a small delta ride. Below is a starting point; treat these numbers as a template for your CA or auditor to sign off, not as a rule:

FX Correction Policy (draft — for auditor review): Raise a "FX Rate Correction" Landed Cost Voucher against a submitted Purchase Receipt when either (a) the FX rate has moved more than 0.5% between the PO date and the GRN posting date, or (b) the absolute FX-related delta on the consignment exceeds ₹5,000 (base-currency, computed as qty × rate × |policy_rate − grn_conversion_rate|, summed across items). Below both thresholds, do not raise a correction — the residual sits in Exchange Gain/Loss via the PI's auto-post, and its P&L noise is immaterial by the company's materiality benchmark. All corrections are booked against the company's Default Exchange Gain / Loss Account under Indirect Expenses. Rate source: FBIL reference rate on the GRN posting date; fall back to the company's bank TT buying rate if it is not yet published.

Three things to notice about this being written down rather than left to individual judgement:

  • Auditors prefer a written policy to case-by-case calls. A written threshold with a documented rate source is far easier to defend at year-end than "we corrected it when it looked material".
  • The numbers should match your company's materiality. ₹5,000 is a placeholder; a much larger company should run a much higher threshold. Anchor it to the materiality benchmark your auditor already uses, not a number from a blog post.
  • A 0.5% move is a low threshold. USD/INR can move that much in a week, so expect corrections on a fair share of import GRNs at first, falling as the buying team times POs closer to shipment.

Common questions from the buying team

+Does editing the exchange rate on a submitted Purchase Receipt fix the stock ledger?

No. You can't edit a submitted PR's exchange rate at all, and even if you could via a customisation, the Stock Ledger Entries were written at submit time and won't recalculate. The only supported mechanisms are a Landed Cost Voucher against the GRN or a repost of the item's valuation. Use the LCV calculator in this post to compute the top-up amount.

+Why can't I see the 'Create Landed Cost Voucher' button on my Purchase Invoice?

Because the invoice didn't move stock. ERPNext hides the button unless docstatus === 1 && update_stock === 1. This is deliberate: LCVs allocate cost across items that actually posted Stock Ledger Entries, and a PI without Update Stock posted none. Create the LCV against the linked Purchase Receipt instead.

+If I tick Update Stock on the PI, will I get duplicate stock entries?

Only if the PI wasn't pulled from an existing GRN. ERPNext's validate_purchase_receipt_if_update_stock throws whenever any PI item has a purchase_receipt link and update_stock=1. Duplicates happen when someone bypasses that flow by creating the PI from the PO instead of the GRN. Always use Get Items From → Purchase Receipt when a GRN exists.

+Which rate should I use on the GRN: a reference rate, the bank's TT rate, or something else?

Whatever your accounting policy defines as the spot rate on the receipt date. Many companies use the FBIL reference rate (published since 2018 in place of RBI's) for consistency; companies with an active treasury desk often use their bank's TT buying rate. The critical part is the date (receipt day, not PO day, not invoice day), more than the source. Agree the source with your auditor.

+How does the LCV FX top-up interact with a closed accounting period?

It depends on whether your closing setup (a closed Accounting Period, or the frozen-till date in Accounts Settings) blocks posting into the closed period. If it doesn't, an LCV reposts outgoing SLEs and changes COGS in the closed period; talk to your auditor before that happens. If they do block it, follow Scenario C above: book a Journal Entry in the current period against Exchange Gain/Loss and accept that ERPNext's inventory rate stays uncorrected.

+Will the gate script slow down GRN submission?

The check does two frappe.db.get_value calls per GRN, a negligible cost on any reasonable server (see the Hetzner vs AWS guide for hosting). The scheduler job that pulls the reference rate runs once per day and is entirely off the submit path.

+Does this apply to ERPNext v14 and v15, or just v16?

The code paths cited above are from v16.28.0, but the same logic (the GRN freezes stock at conversion_rate, the PI posts the FX difference to Exchange Gain/Loss, the LCV is the supported correction) is in v14 and v15 with small differences. Test the check script on your version before you rely on it.

+What if the supplier invoices in INR and there's no FX at all?

Then none of this applies: the conversion_rate is 1 and nothing drifts. This post is for imports invoiced in a foreign currency (USD, EUR, CNY, AED and so on).

What is the bottom line?

ERPNext's landed-cost and FX handling is closer to correct than forum threads suggest. The stock ledger honours receipt-date FX; the exchange gain/loss on the invoice is booked separately; the LCV button is hidden on non-stock-moving documents on purpose; and the "double SLE" scenario is explicitly gated in code.

What breaks the flow is usually the PO's rate carrying through to the GRN because nobody overrode it. Fix that one habit, put the check in, and most of the landed-cost trouble goes. The ERPNext platform page covers how we work with ERPNext more widely, and the distribution page shows the buying and stock flows running live.

Your next step

Books already tangled with historical FX drift?

Next step · Manufacturing & distribution

Now see how ERPNext runs a plant like yours

How we set up ERPNext for manufacturers, from bills of materials and job work to costing and the shop floor.

Comments & ratings

No comments yet. Start a new discussion.

0

Manoj · Published on 23 July 2026

Worked with us? Leave a Google review

Questions about this? Talk to the team.

Book a 30-minute Google Meet, WhatsApp us, or send a message. The engineer who works on this replies.

Send us a message
Your details go to our team's CRM and are used only to reply.