fix(accruals): carry origin dimensions onto dissolution entries (#1419)

* fix(accruals): carry origin dimensions onto dissolution entries

A project-tagged deferred invoice line lost its dimensions bag on every
monthly dissolution: the schedule never stored the bag, so dissolution
lines booked untagged and the per-project P&L drifted from the origin.

Persist the merged bag (invoice default_dimensions with the item bag on
top, same merge the origin generators use) on accrual_schedules and
attach it to BOTH dissolution lines: both origin generators tag the
interim 17xx/29xx line too, so per-dimension views of the interim
account keep netting to zero. Pre-existing schedules stay at '{}' and
keep today's untagged behavior; backfill is a separate follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(accruals): let dissolutions post when the tagged dimension value is archived

Carrying the origin's dimensions bag onto dissolution lines pulled every
monthly periodisering into validateEntryDimensions. With dimensions_enabled,
a project archived after the origin invoice was booked made the validator
reject the dissolution: the accrual service caught the rejection, wrote
last_error and left the installment pending forever. The remaining months of
prepaid cost would never reach 5xxx/6xxx, the interim 17xx/29xx account would
stay overstated, and the trial balance still balances, so no year-end check
fires and the arsredovisning is filed with understated cost.

Add a second, narrow exempt set (DIMENSION_VALIDATION_EXEMPT_SOURCE_TYPES =
{'accrual'}) with an isDimensionValidationExemptSource() helper, and skip the
soft registry validation for it in createDraftEntry. A dissolution is the
mechanical continuation of an already-approved, already-posted decision: the
same category as a storno, which the engine already bypasses.

The tag is kept, never stripped: an archived value still exists in the
registry and the cost genuinely belongs to that project. The rules set
(required/default/fixed) stays untouched, and the other two
validateEntryDimensions call sites keep validating: updateDraftEntry is a
user edit of an editable draft, and replaceOpeningBalanceEntry rejects any
source_type other than opening_balance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-06 10:04:20 +02:00
committed by GitHub
co-authored by Claude Fable 5 Jakob Wennberg
parent d7320a6c87
commit 31fba5e8f9
11 changed files with 707 additions and 33 deletions
+9 -1
View File
@@ -23,6 +23,7 @@ import {
assertMandatoryDimensions,
fetchActiveDimensionRules,
isDimensionRuleExemptSource,
isDimensionValidationExemptSource,
} from '@/lib/bookkeeping/dimension-rules'
import { fetchEntryLines, type EntryLinesQuery } from '@/lib/bookkeeping/entry-lines'
import { backfillStandardBASAccounts } from '@/lib/bookkeeping/account-backfill'
@@ -264,7 +265,14 @@ export async function createDraftEntry(
// enabled companies get registry validation with a typed Swedish rejection.
// Runs before any insert so a rejection leaves no orphan rows. Reversal/
// storno/correction paths bypass this: they copy posted data verbatim.
await validateEntryDimensions(supabase, companyId, lines)
// Accrual dissolutions bypass it for exactly that reason too: they replay
// the origin entry's bag, so a value archived after the origin was posted
// must not be able to strand the remaining months as pending and leave the
// interim 17xx/29xx account overstated. See
// DIMENSION_VALIDATION_EXEMPT_SOURCE_TYPES.
if (!isDimensionValidationExemptSource(input.source_type)) {
await validateEntryDimensions(supabase, companyId, lines)
}
// Validate that entry_date falls within the selected fiscal period
const { data: period, error: periodError } = await supabase