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.
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.
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.
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.
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.
>>> 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'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.
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.
# 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"})
PYPre-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.
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.
Well before anyone opens the desk. Finance sees a red email at breakfast, not a broken report at 22:00 on the 30th.
The scheduler owns it. No maintenance window; no dependency on a human remembering to smoke-test after every bench update.
Writes a timestamp when all three pass so you can prove the check ran on audit day. Optional but free.
# 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.
Related issues you may also hit
- ERPNext bank reconciliation: the interactive fix guide — another accounting-adjacent workflow with silent-drift failure modes.
- ERPNext Landed Cost & FX Gone Wrong — the sister post on stock-valuation drift from stale exchange rates on the GRN.
- ERPNext BOM cost on Item master out of sync — how the BOM-to-Item cost linkage silently rots on manufacturing benches.
- Tuning MariaDB for a real ERPNext workload — because a report that crashes is often masking a report that also times out.
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.