fix(reports): count reversed originals in the KPI monthly breakdown (#2201) (#2243)

The year total (tb / tb_ex_year_end) aggregates posted AND reversed
entries, so a same-year storno nets to 0. The monthly section, both the
get_kpi_report_aggregates RPC and the dimension-filtered JS fallback in
lib/reports/monthly-breakdown.ts, was posted-only: it dropped the reversed
original but kept the storno (itself posted). 10 000 kr on 3041 in March,
reversed in April, gave March 0 kr, April -10 000 kr, year 0 kr, and PR
#2198 made the per-month figures visible enough to add up.

Migration 20260903160000 replaces the RPC with the monthly join on
tb_ex_year_end's entry set verbatim (no extra status predicate); the JS
fallback filters status in ('posted','reversed') the same way. The pg-real
pin ("in tb, not in monthly") is flipped and a storno case asserts
sum(months) = net result. Everything else in the function is byte-identical
to 20260730090000.

Verified: pg-real suite against a rebuilt local supabase/postgres with every
migration applied (9 tests), unit suite, lint.

Closes #2201


Claude-Session: https://claude.ai/code/session_01VnConrmMCxJRQ5kfiPPWyy

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-09-03 18:21:00 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5.1
parent 3918ff6620
commit d69de86b71
4 changed files with 225 additions and 13 deletions
+11 -4
View File
@@ -93,10 +93,16 @@ export function assembleMonthlyBreakdown(
/**
* Generate monthly income vs expenses breakdown for a fiscal period.
*
* Groups posted journal entry lines by month and account class:
* Groups posted AND reversed journal entry lines by month and account class:
* - Class 3 (30xx) = revenue (credit side)
* - Class 4-7 (40xx-79xx) = expenses (debit side)
*
* Reversed originals are included on purpose (issue #2201): the year total
* (tb_ex_year_end / the income statement) counts posted + reversed, so a
* same-year storno nets to 0 there. Dropping the reversed original here but
* keeping the storno (itself posted) made the months stop summing to
* Nettoresultat. The same entry set on both paths keeps sum(months) = year.
*
* Year-end entries are excluded, including the storno/correction chain of a
* REVERSED year-end entry (an undone bokslut). Without that the resultatavslut,
* which posts the mirror image of every P&L account, showed the whole year's
@@ -145,8 +151,8 @@ export async function generateMonthlyBreakdown(
)
).map((r) => r.id)
// Get all posted journal entry lines for this period with their entry dates,
// via the two-step entry-lines fetch (see lib/bookkeeping/entry-lines.ts).
// Get all posted and reversed journal entry lines for this period with their
// entry dates, via the two-step entry-lines fetch (lib/bookkeeping/entry-lines.ts).
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let lines: any[]
try {
@@ -158,7 +164,8 @@ export async function generateMonthlyBreakdown(
let query = q
.eq('fiscal_period_id', fiscalPeriodId)
.eq('company_id', companyId)
.eq('status', 'posted')
// posted + reversed: the same set as tb_ex_ye_entries in the RPC.
.in('status', ['posted', 'reversed'])
.neq('source_type', 'year_end')
if (reversedYearEndIds.length > 0) {
const idList = `(${reversedYearEndIds.join(',')})`