From 2f49106f496d12a6e58c64822d669501c97dd1b4 Mon Sep 17 00:00:00 2001
From: Mattsson <111893710+mattssonn@users.noreply.github.com>
Date: Tue, 28 Apr 2026 14:58:27 +0200
Subject: [PATCH] =?UTF-8?q?fix(balansrapport):=20adjust=20balance=20report?=
=?UTF-8?q?=20to=20reflect=20correct=20sign=20con=E2=80=A6=20(#375)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(balansrapport): adjust balance report to reflect correct sign conventions and update PDF template
* fix(balansrapport): enforce strict negative balance convention for equity rows in balance report tests
---
app/(dashboard)/reports/page.tsx | 6 +-
lib/reports/__tests__/balansrapport.test.ts | 67 +++++++++++++++++--
lib/reports/balansrapport.ts | 29 ++++----
.../operational-report-pdf-template.tsx | 6 +-
4 files changed, 81 insertions(+), 27 deletions(-)
diff --git a/app/(dashboard)/reports/page.tsx b/app/(dashboard)/reports/page.tsx
index a11f3f95..71c621da 100644
--- a/app/(dashboard)/reports/page.tsx
+++ b/app/(dashboard)/reports/page.tsx
@@ -1073,8 +1073,8 @@ function BalansrapportView({ periodId, onNavigateToAccount }: { periodId: string
Konto |
Kontonamn |
Ingående balans |
- Utgående balans |
Förändring |
+ Utgående balans |
@@ -1096,8 +1096,8 @@ function BalansrapportView({ periodId, onNavigateToAccount }: { periodId: string
{row.account_name} |
{formatAmount(row.ib)} |
- {formatAmount(row.ub)} |
{formatAmount(row.period_change)} |
+ {formatAmount(row.ub)} |
))}
@@ -1105,10 +1105,10 @@ function BalansrapportView({ periodId, onNavigateToAccount }: { periodId: string
Summa
| {formatAmount(group.subtotal_ib)} |
- {formatAmount(group.subtotal_ub)} |
{formatAmount(group.subtotal_ub - group.subtotal_ib)}
|
+ {formatAmount(group.subtotal_ub)} |
))}
diff --git a/lib/reports/__tests__/balansrapport.test.ts b/lib/reports/__tests__/balansrapport.test.ts
index 70d0204b..a34e195e 100644
--- a/lib/reports/__tests__/balansrapport.test.ts
+++ b/lib/reports/__tests__/balansrapport.test.ts
@@ -110,19 +110,19 @@ describe('generateBalansrapport', () => {
expect(assets.subtotal_ib).toBe(60000)
expect(assets.subtotal_ub).toBe(87500)
- // Equity & liabilities — credit-positive
+ // Equity & liabilities — debit-negative (Fortnox/Visma convention)
const equity = report.groups[1]
expect(equity.rows[0]).toEqual({
account_number: '2099',
account_name: 'Årets resultat',
- ib: 52000,
- ub: 72500,
- period_change: 20500,
+ ib: -52000,
+ ub: -72500,
+ period_change: -20500,
})
- expect(equity.subtotal_ub).toBe(87500)
+ expect(equity.subtotal_ub).toBe(-87500)
expect(report.total_assets_ub).toBe(87500)
- expect(report.total_equity_liabilities_ub).toBe(87500)
+ expect(report.total_equity_liabilities_ub).toBe(-87500)
// 2099 already absorbs prior+current result, residual is 0
expect(report.beraknat_resultat).toBe(0)
expect(report.is_balanced).toBe(true)
@@ -150,12 +150,65 @@ describe('generateBalansrapport', () => {
const report = await generateBalansrapport(q.supabase as any, 'company-1', 'period-1')
expect(report.total_assets_ub).toBe(80000)
- expect(report.total_equity_liabilities_ub).toBe(30000)
+ expect(report.total_equity_liabilities_ub).toBe(-30000)
expect(report.beraknat_resultat).toBe(50000)
// Trial balance still balances — double-entry guarantees this.
expect(report.is_balanced).toBe(true)
})
+ it('renders class 2 rows with negative sign (god redovisningssed convention)', async () => {
+ // Regression test: every Swedish accounting tool (Fortnox, Visma, Bokio,
+ // Briox, BL) renders class 2 debit-negative on Balansrapport so that
+ // assets + eq_liab = beräknat resultat. Pin this convention.
+ const q = createQueuedMockSupabase()
+ q.enqueue({
+ data: { period_start: '2026-01-01', period_end: '2026-12-31' },
+ error: null,
+ })
+
+ mockTrialBalance.mockResolvedValueOnce(
+ tb([
+ makeRow({
+ account_number: '1930',
+ account_name: 'Bank',
+ account_class: 1,
+ opening_debit: 100000,
+ closing_debit: 120000,
+ }),
+ makeRow({
+ account_number: '2440',
+ account_name: 'Lev.skulder',
+ account_class: 2,
+ opening_credit: 40000,
+ closing_credit: 50000,
+ }),
+ makeRow({
+ account_number: '2350',
+ account_name: 'Banklån',
+ account_class: 2,
+ opening_credit: 30000,
+ closing_credit: 25000,
+ }),
+ ])
+ )
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const report = await generateBalansrapport(q.supabase as any, 'company-1', 'period-1')
+
+ const equity = report.groups.find((g) => g.class === 2)!
+ // Strict < 0: every fixture row has a nonzero credit balance, so the
+ // convention requires every row to be strictly negative.
+ expect(equity.rows.every((r) => r.ib < 0)).toBe(true)
+ expect(equity.rows.every((r) => r.ub < 0)).toBe(true)
+ expect(equity.subtotal_ib).toBeLessThan(0)
+ expect(equity.subtotal_ub).toBeLessThan(0)
+ expect(report.total_equity_liabilities_ub).toBeLessThan(0)
+ // Sum of both sides equals beräknat resultat (here: profit residual)
+ expect(report.total_assets_ub + report.total_equity_liabilities_ub).toBe(
+ report.beraknat_resultat
+ )
+ })
+
it('is_balanced reflects trial balance balance state', async () => {
const q = createQueuedMockSupabase()
q.enqueue({
diff --git a/lib/reports/balansrapport.ts b/lib/reports/balansrapport.ts
index 74770db8..da0d9a4d 100644
--- a/lib/reports/balansrapport.ts
+++ b/lib/reports/balansrapport.ts
@@ -18,10 +18,11 @@ const CLASS_LABELS: Record = {
* Unlike Balansräkning (formal, ÅRL Bilaga 1), this keeps account numbers
* and is meant for ongoing reconciliation, not for årsbokslut/årsredovisning.
*
- * Sign convention: assets (class 1) shown debit-positive (debit - credit),
- * equity & liabilities (class 2) shown credit-positive (credit - debit).
- * That's the normal balance for each side and matches how Fortnox/Visma
- * present a Balansrapport.
+ * Sign convention: every row is shown debit-positive (debit - credit). Class 1
+ * accounts (debit balance) render positive; class 2 accounts (credit balance)
+ * render negative. This matches Fortnox/Visma/Bokio and lets the user verify
+ * the balance by adding rows: total_assets_ub + total_equity_liabilities_ub
+ * = beraknat_resultat (the running-year P&L residual, 0 after year-end close).
*/
export async function generateBalansrapport(
supabase: SupabaseClient,
@@ -52,8 +53,8 @@ export async function generateBalansrapport(
let subtotalIb = 0
let subtotalUb = 0
for (const r of groupRows) {
- const ib = signedAmount(r.opening_debit, r.opening_credit, klass)
- const ub = signedAmount(r.closing_debit, r.closing_credit, klass)
+ const ib = signedAmount(r.opening_debit, r.opening_credit)
+ const ub = signedAmount(r.closing_debit, r.closing_credit)
const change = round2(ub - ib)
if (Math.abs(ib) < 0.005 && Math.abs(ub) < 0.005) continue
rows.push({
@@ -81,12 +82,12 @@ export async function generateBalansrapport(
const totalAssetsUb = groups.find((g) => g.class === 1)?.subtotal_ub ?? 0
const totalEquityLiabilitiesUb = groups.find((g) => g.class === 2)?.subtotal_ub ?? 0
- // Beräknat resultat (Fortnox/Visma convention): the residual on the balance
- // side. During a running year, current-year profit lives in P&L accounts
- // and 2099 still holds the prior year's accumulated result, so the residual
- // equals current-year P&L net result. After year-end closing posts result
- // into 2099, residual is 0 and total_assets == total_eq_liab.
- const beraknatResultat = round2(totalAssetsUb - totalEquityLiabilitiesUb)
+ // Beräknat resultat: the residual on the balance side. With both classes in
+ // debit-positive sign, assets are positive and eq_liab is negative; their sum
+ // equals the running-year P&L residual. Trial balance guarantees
+ // sum_all(debit - credit) = 0, so sum_balance = -sum_pl = revenues - costs.
+ // After year-end close posts the result into 2099, the residual is 0.
+ const beraknatResultat = round2(totalAssetsUb + totalEquityLiabilitiesUb)
return {
groups,
@@ -98,8 +99,8 @@ export async function generateBalansrapport(
}
}
-function signedAmount(debit: number, credit: number, klass: number): number {
- return klass === 1 ? debit - credit : credit - debit
+function signedAmount(debit: number, credit: number): number {
+ return debit - credit
}
function round2(n: number): number {
diff --git a/lib/reports/operational-report-pdf-template.tsx b/lib/reports/operational-report-pdf-template.tsx
index 0de6090b..bae10e72 100644
--- a/lib/reports/operational-report-pdf-template.tsx
+++ b/lib/reports/operational-report-pdf-template.tsx
@@ -385,8 +385,8 @@ export function BalansrapportPDF({ report, company, generatedAt }: Balansrapport
Konto
Kontonamn
Ingående
- Utgående
Förändring
+ Utgående
{report.groups.map((group) => (
@@ -399,17 +399,17 @@ export function BalansrapportPDF({ report, company, generatedAt }: Balansrapport
{row.account_number}
{row.account_name}
{formatAmount(row.ib)}
- {formatAmount(row.ub)}
{formatAmount(row.period_change)}
+ {formatAmount(row.ub)}
))}
Summa
{formatAmount(group.subtotal_ib)}
- {formatAmount(group.subtotal_ub)}
{formatAmount(group.subtotal_ub - group.subtotal_ib)}
+ {formatAmount(group.subtotal_ub)}
))}