Accounting Regressions

The ERPNext v15.46 Balance Sheet / P&L / Cash Flow Crash — and How to Patch Your Instance

ERPNext v15.46.0 shipped a regression that crashes Balance Sheet, Profit & Loss and Cash Flow with an AttributeError on the reporting-currency columns. This post shows what broke, how to patch, and how to make sure the next one is caught before month-end.

MManojJuly 24, 202611 min read
#erpnext#accounting-regressions#upgrades#self-hosted
Share

On ERPNext v15.46.0, opening the Balance Sheet, Profit & Loss Statement, or Cash Flow report throws an AttributeError instead of returning rows. The failing call sits in erpnext/accounts/report/financial_statements.py — the reporting-currency select fires on schemas that were never migrated to carry the column. This post shows the exact traceback, tells you whether your instance is affected, gives you three patch recipes ranked by safety, and ends with a nightly guardrail so a future regression is caught a day after it hits your bench, not on the 30th at 22:00.

Financial-statement regressions are the worst class of ERP bug to discover during audit fieldwork. I am Manoj, ERPNext and Frappe implementation lead at MithTech in Bengaluru — this playbook came out of triaging exactly that situation on a client bench.

Am I affected? Pick your version

The regression is narrower than the noise on the forum suggests. Only unpatched v15.46.0 is dead — everything below the tag is safe by absence of the offending change, and everything above (either patched v15 or any v16) is safe by construction. Tap your line.

InteractiveVersion Exposure Checkerlink

Pick your ERPNext version to learn whether you're affected by the v15.46 financial-statements regression, and what the shortest path out looks like.

Status: at risk
Affected — Balance Sheet, P&L, Cash Flow throw on run

The GL-entry select builds a reporting-currency branch unconditionally in this release, and the downstream aggregator hits an AttributeError before rows come back. Every financial statement report is dead until you patch or upgrade.

What to do: Cherry-pick the upstream fix, roll forward to v15.46.1+ once tagged, or move to v16 stable. Do NOT close month-end on this build.

The check you want to run before touching anything else is bench version --format json | grep erpnext. That returns the exact tag the bench is on — not the branch name (which lies as soon as anyone runs bench update --pull without also bumping the tag).

What actually broke — read against source

The reports all funnel through one helper: get_accounting_entries in erpnext/accounts/report/financial_statements.py. On v16.28.0 (the current mainline checkout we verified this post against), that function starts at line 505 and reaches the reporting-currency branch at line 537:

# erpnext/accounts/report/financial_statements.py:505-545 (v16.28.0)
def get_accounting_entries(
    doctype,
    from_date,
    to_date,
    ...
    ignore_reporting_currency=True,
):
    gl_entry = frappe.qb.DocType(doctype)
    query = (
        frappe.qb.from_(gl_entry)
        .select(
            gl_entry.account,
            gl_entry.debit if not group_by_account else Sum(gl_entry.debit).as_("debit"),
            gl_entry.credit if not group_by_account else Sum(gl_entry.credit).as_("credit"),
            ...
            gl_entry.account_currency,
        )
        .where(gl_entry.company == filters.company)
    )

    if not ignore_reporting_currency:
        query = query.select(
            gl_entry.debit_in_reporting_currency ...,
            gl_entry.credit_in_reporting_currency ...,
        )

Read that last block. On mainline, the reporting-currency columns are only appended to the select when the caller passes ignore_reporting_currency=False. Balance Sheet, P&L and Cash Flow all default to True — they never opt in, so the branch never runs, and legacy schemas that predate the reporting-currency columns cannot notice they exist.

On v15.46.0 the same helper unconditionally selected the reporting-currency pair. On a schema where the v16 patch erpnext/patches/v16_0/set_reporting_currency.py had not yet run — or where the equivalent v15 patch had not landed at the time of upgrade — Pypika's frappe.qb builder raised an AttributeError on the missing Field.debit_in_reporting_currency, and the whole trio of reports died on it.

InteractiveStatement Crash Simulatorlink

A mocked-up traceback of the AttributeError so readers who never saw the crash can recognise it in their own logs, plus a click-to-reveal of the conceptual fix.

ERPNext v15.46.0What lands in your log when a controller opens Balance Sheet
bench --site erp.yourco.in console
>>> from erpnext.accounts.report.balance_sheet.balance_sheet import execute
>>> execute({"company": "Your Co Pvt Ltd",
...          "from_date": "2026-04-01",
...          "to_date":   "2026-06-30",
...          "periodicity": "Monthly",
...          "accumulated_values": 1,
...          "presentation_currency": "INR"})
Traceback (most recent call last):
  File ".../erpnext/accounts/report/balance_sheet/balance_sheet.py", line 31, in execute
    period_list = get_period_list(...)
  File ".../erpnext/accounts/report/financial_statements.py", line 434, in set_gl_entries_by_account
    entries = get_accounting_entries(...)
  File ".../erpnext/accounts/report/financial_statements.py", line 537, in get_accounting_entries
    query = query.select(gl_entry.debit_in_reporting_currency, ...)
AttributeError: 'Field' object has no attribute 'debit_in_reporting_currency'
(Conceptual — the exact wording of the fix is in the upstream PR you cherry-pick.)

The fix in the upstream PR is conceptually simple: guard the reporting-currency select behind the caller's ignore_reporting_currency flag. Every consumer that never opted in never touches the new columns; every consumer that did opt in (nothing in core ships that way at the time of writing) is on a schema that carries them.

Financial statements aren't optional under any standards regime

Ind AS 1 Presentation of Financial Statements §54–79, IAS 1 §54–80A, ASC 205 §205-10-45, and FRS 102 §4 all require a balance sheet, an income statement and a cash-flow statement as the core financial statements a reporting entity must produce. When the reports crash on your ERP, it is not only an engineering incident — it is a delayed close, a delayed audit fieldwork window, and a delayed board pack. Fix it before month-end, not during.

Patch it — three routes, ranked by safety

The three-tab widget below is ordered from least-invasive to most-invasive. Prefer the bench update --patch path onto a fixed tag; fall back to a cherry-pick only when you are pinned to v15.46.0 by a customisation you cannot yet un-pin. The Docker rebuild path is the deployment mechanics for the same idea when your production bench lives inside frappe_docker.

Code recipePatch Command Recipelink

Three copy-paste recipes for landing the fix on your bench: cherry-pick, bench update, or Docker rebuild. All include a smoke-test step so you verify the reports actually run before anyone closes month-end.

Patching upstream is fragile. The safer play is to leap-frog to a tag that already carries the fix. Use these recipes when you are pinned to v15.46.0 by a customisation and cannot yet upgrade — not as a routine maintenance pattern.

The 'clean' path: roll the whole bench forward to a tag that includes the fix. Safer than cherry-pick because it does not leave your fork half-merged, but you inherit every other change in the release.

bench.sh
# Roll the erpnext app forward to a tag that includes the fix
cd ~/frappe-bench

# 1. Verify what tag your site is on
bench version --format json | grep erpnext

# 2. Bump the branch/tag in apps.txt if you're pinned; otherwise:
bench switch-to-branch version-15 erpnext --upgrade   # or version-16 for the v16 line

# 3. Full update — pulls, migrates, builds, restarts
bench update --patch    # skips backup step; run 'bench backup' first, always

# --- Recommended pre-flight, do not skip ---
bench --site <your-site> backup --with-files
# --- After update, verify the reports actually run ---
bench --site <your-site> console <<'PY'
from erpnext.accounts.report.balance_sheet.balance_sheet import execute
execute({"company": "<your-company>",
         "from_date": "2026-04-01", "to_date": "2026-06-30",
         "periodicity": "Monthly", "accumulated_values": 1,
         "presentation_currency": "INR"})
PY

Pre-flight the account defaults before you smoke-test

Financial statements read from GL Entry and — via set_gl_entries_by_account at erpnext/accounts/report/financial_statements.py:434 — from the account tree. Two Company-master fields need to be non-blank before any of the three reports render clean rows on a fresh install:

Company master fields the reports rely on

Open Company master and confirm default_currency is set (drives the presentation-currency default) and, if you post in a foreign currency, default_receivable_account / default_payable_account are populated. On a fresh install these are sometimes blank, and the reports render with mysteriously empty rows rather than an outright error. The regression above is separate; this is the mistake people conflate with it when they finally roll the patch and still see an empty Balance Sheet.

Fix, verified — the six-step drill

Confirm the diagnosis

bench version --format json | grep erpnext tells you the exact tag. If it reads v15.46.0, and Balance Sheet / P&L / Cash Flow all raise an AttributeError mentioning debit_in_reporting_currency, you are in the exposure window. If the tag is anything else, the fault is elsewhere — check permissions, fiscal year setup, or the account tree first.

Back up before you touch

bench --site your-site backup --with-files. Non-negotiable at month-end. bench update --patch skips the automatic backup step — do it yourself, verify the SQL dump and files archive are actually on disk, then proceed.

Roll forward or cherry-pick

Prefer bench switch-to-branch version-15 erpnext --upgrade onto a tag ≥ v15.46.1, or straight to version-16. Fall back to git cherry-pick <FIX_COMMIT_SHA> inside apps/erpnext only if you cannot roll the whole app forward. Docker deployments: bump the image tag in your compose / values override, docker compose pull, then migrate.

Migrate and rebuild

bench migrate runs any v16 patches that landed between your old tag and the new one — including erpnext/patches/v16_0/set_reporting_currency.py if you crossed into v16. bench build --app erpnext refreshes the desk bundle. bench restart cycles workers so background jobs pick up the new code.

Smoke-test the three reports from the console

bench --site your-site execute \
  erpnext.accounts.report.balance_sheet.balance_sheet.execute \
  --kwargs "{'filters': {'company': 'Your Co', \
             'from_date': '2026-04-01', 'to_date': '2026-06-30', \
             'periodicity': 'Monthly', 'accumulated_values': 1, \
             'presentation_currency': 'INR'}}"

Repeat for profit_and_loss_statement.profit_and_loss_statement.execute and cash_flow.cash_flow.execute. All three must return without raising before you sign off on the patch.

Install the nightly guardrail

Deploy the RegressionHardening server script below. It runs the three reports every night, emails finance and ops on failure, and writes a heartbeat on green. Cost: one server script; benefit: no ever again "found on audit day" report crash.

Who owns what — a split for benches with a real ops team

The workflow above assumes one person owns bench maintenance, the CFO's month-end close, and the patch decision. That is fine for a 20-user SME. For anything bigger, the responsibilities split, and the split matters:

Ops / DevOps owns the patch

Version selection, backup, bench update --patch or cherry-pick, image rebuild, migration. Ops treats a broken financial statement as a P0 incident and does not defer it to "next maintenance window" — because there isn't one that lands before month-end.

Finance owns the smoke test sign-off

Ops does not close the ticket until finance opens Balance Sheet, P&L and Cash Flow from the desk (not the console), against the current fiscal year, and confirms the numbers match yesterday's expectation. A green console execution proves the crash is gone; it does not prove the numbers are right.

Both own the nightly guardrail's inbox

Route the alert email to a shared distribution list — not to one person's inbox. A regression the alert catches at 03:30 on a Saturday should reach an on-call ops engineer and whoever is closing month-end. In practice we route to a Slack channel via a webhook step and email as a fallback, so nobody has to see it if the room is watched.

The reason to write this split down — even for a five-person team — is that "who runs the patch" and "who decides it landed correctly" are different roles in every audit framework. Pretending they are one role for convenience is exactly how a broken bench closes month-end.

Harden against the next one

The v15.46 regression is not going to be the last statement-crashing change to land in ERPNext. The stack is under active development; refactors happen; occasionally one ships before all the consumer call sites are audited. The correct posture is not "audit every commit" — nobody has time — but "run each statement report once per night and yell if any of them raises." Forty lines of server script, no external dependency.

Code recipeNightly Regression Guardraillink

Server script (Scheduler Event, Daily) that runs Balance Sheet, P&L and Cash Flow against the current fiscal year and emails finance + ops on any exception.

Runs at 03:15 IST

Well before anyone opens the desk. Finance sees a red email at breakfast, not a broken report at 22:00 on the 30th.

Zero clicks per day

The scheduler owns it. No maintenance window; no dependency on a human remembering to smoke-test after every bench update.

Heartbeat on green

Writes a timestamp when all three pass so you can prove the check ran on audit day. Optional but free.

fs_nightly_smoke_test.py
# Server Script → Type: Scheduler Event → Frequency: Daily (03:15 IST is fine)
# Runs each of the three financial statements against the current fiscal year and
# emails finance + ops if any of them raises. A future regression is then caught
# a day after it hits your bench, not on the 30th at 22:00.

import frappe
from frappe.utils import getdate, get_first_day, get_last_day
from erpnext.accounts.report.balance_sheet.balance_sheet import execute as bs_exec
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import execute as pl_exec
from erpnext.accounts.report.cash_flow.cash_flow import execute as cf_exec

COMPANY = frappe.db.get_single_value("Global Defaults", "default_company")
today = getdate()
frm = get_first_day(today).isoformat()
to  = get_last_day(today).isoformat()
filters = {
    "company": COMPANY,
    "from_date": frm,
    "to_date": to,
    "periodicity": "Monthly",
    "accumulated_values": 1,
    "presentation_currency": frappe.db.get_value("Company", COMPANY, "default_currency"),
}

failures = []
for label, fn in [("Balance Sheet", bs_exec),
                  ("Profit & Loss", pl_exec),
                  ("Cash Flow",     cf_exec)]:
    try:
        fn(filters.copy())
    except Exception as e:
        failures.append((label, type(e).__name__, str(e)[:400]))

if failures:
    rows = "".join(
        f"<tr><td>{r[0]}</td><td>{r[1]}</td><td><pre>{frappe.utils.escape_html(r[2])}</pre></td></tr>"
        for r in failures
    )
    frappe.sendmail(
        recipients=["finance@yourco.in", "ops-lead@yourco.in"],
        subject=f"[ERPNext] {len(failures)} financial statement(s) crashed on nightly check",
        message=(
            "<p>Nightly smoke-test of Balance Sheet / P&L / Cash Flow reports failed. "
            "Do not close month-end until this clears.</p>"
            f"<table border=1 cellpadding=6><tr><th>Report</th><th>Exception</th>"
            f"<th>Message</th></tr>{rows}</table>"
        ),
    )
else:
    # Optional: write a heartbeat so you can prove the check ran on audit day
    frappe.db.set_single_value("System Settings", "reports_last_verified_at", frappe.utils.now())

The script runs from the built-in Frappe scheduler — not GitHub Actions, not an external cron on your Vercel deploy, not a paid CI job. Everything stays inside the bench, so the check works even if your outbound network is firewalled.

A defensible policy for when to hold a close

If the guardrail sends the red email at 03:30 on the 29th and you are supposed to close on the 30th, what is the right response? This threshold is a starting point — treat it as a template your CFO and auditor ratify, not gospel:

Statement-Regression Response Policy (draft — for auditor / CFO review): If any of Balance Sheet, Profit & Loss Statement, or Cash Flow raises an exception on the nightly smoke test, mark the current month-end on hold until Ops confirms a fix has landed and Finance has re-verified all three reports render cleanly from the desk. The hold applies whether or not the affected reports are on the current close's checklist — a corrupted helper function that breaks one report is signal that the shared code path is untrusted, and every downstream number derived from it (management dashboards, board pack, GST reconciliation drafts) must be re-verified. Escalation contact: on-call ERPNext ops lead. Target time-to-fix: < 24 hours for a regression with a known upstream patch; < 72 hours otherwise.

Two things about writing this down rather than leaving it to individual judgement:

  • Auditors love a policy, hate ad-hoc. A written response protocol — even a conservative one — is dramatically easier to defend at year-end than "we noticed and fixed it before close."
  • The 24 / 72 hour targets are placeholders. A firm running month-end on a 24-hour cycle with a live trading business needs tighter numbers; a smaller SME with a five-day close window can loosen them. Anchor to your existing incident-response SLA rather than an internet blog post's number.

Fix immediately vs backlog

Either way, the process fix — the nightly guardrail — costs a single deploy and is silent on green days. There is no reason not to install it.

FAQ

+What ERPNext versions are affected by this financial statements crash?

Only v15.46.0 (unpatched). Everything below the tag pre-dates the reporting-currency select branch; every v16.x release and every patched v15.46.x carries the guard that fixes it. Run bench version --format json | grep erpnext to confirm your exact tag.

+How do I fix the Balance Sheet AttributeError in ERPNext v15.46 without upgrading to v16?

Two options — cherry-pick the fix commit into your v15 checkout, or run bench switch-to-branch version-15 erpnext --upgrade onto a v15.46.1+ tag once it lands. Both are covered in the Patch Command Recipe above. Docker deployments should bump the image tag rather than patching a running container.

+Why does this bug only affect Balance Sheet, P&L and Cash Flow — and not, say, the General Ledger report?

Because those three share a single helper — get_accounting_entries in erpnext/accounts/report/financial_statements.py — and the regression sat inside that helper. The General Ledger report has its own query path that never touches the reporting-currency columns, so it kept running.

+What happens if we skip the patch and just avoid opening the reports?

Your month-end close cannot be signed off, because Ind AS 1, IAS 1, ASC 205 and FRS 102 all require the balance sheet, income statement and cash-flow statement as core statements. Statutory audit fieldwork stalls at the first ask. Board packs go out incomplete. The workaround is not "don't open the reports" — it's "patch or upgrade so the reports run."

+Can I edit financial_statements.py directly on the running bench and skip the git dance?

Technically yes; practically no. Files under apps/erpnext are tracked by git, and the next bench update --pull will overwrite your hand-edit without warning. Cherry-pick or roll forward — those survive the update. Hand-edits do not.

+Will the nightly guardrail work on a Docker deployment?

Yes — the guardrail is a Frappe Server Script scheduled via the built-in scheduler, which runs inside the scheduler container. It has no external dependencies. Same script, same behaviour on a bench install and a Docker install.

+How do I know the fix landed correctly — what should I re-check after patching?

Run all three reports from bench console against the current fiscal year (the smoke-test snippet in Step 5). Then have a human open each report from the desk and eyeball the totals against yesterday's expected numbers. A green console execution proves the crash is gone; a human comparison proves the numbers are right. Both are load-bearing.

Closing

Statement-crashing regressions happen — the stack moves fast, and occasionally a refactor ships ahead of every downstream call site being audited. The lesson from v15.46 is not that ERPNext is unstable; it is that a self-hosted ERP with month-end obligations needs a nightly smoke test the way a production API needs a health check. Install the guardrail once, and the next regression stops being an audit-day incident and becomes an ops-day one.

Bench stuck on v15.46 with month-end days away?

We patch, upgrade and harden ERPNext benches for Indian SMEs — including the nightly regression guardrail deployed live to your instance so the next one doesn't touch your close.

Free · By email

Get practical ERPNext & automation guides

New implementation guides, cost breakdowns and open-source tips for Indian businesses — occasionally, straight to your inbox. No spam.

M

Written by

Manoj

Founder of Mith Tech, an open-source ERP & automation studio. Hands-on ERPNext/Frappe implementation across multi-branch, multi-warehouse Indian operations — GST/TDS/PT compliance, branch-level permissions, and custom Frappe apps that give management real-time visibility.

Keep reading

See what this looks like for your business

Book a free 30-minute audit. We'll map your workflows, find where time and money leak, and design an open-source stack you actually own — no per-user licence fees.

Book a consultation
0 0
Published on 24 July 2026

Manoj

Comments

No comments yet. Start a new discussion.

Ctrl+Enter to add comment