Files
accounted/lib/bookkeeping/fiscal-year-gaps.ts
T
c62321988b feat(reconciliation,bokslut): underlag on a balansdag + persisted closing checklist (Reko bilagor, PR 2 + PR 3) (#1873)
* feat(reconciliation): underlag on a balansdag, the files behind a sign-off (Reko bilagor, PR 2)

A konsult attaches the kontoutdrag, engagemangsbesked or reskontralista an
account was reconciled against to (account_key, through_date), before or
after the sign-off, from every account body on the Avstämning page. Rows
live in account_reconciliation_attachments (append-only, removal stamp by
trigger, RLS like account_reconciliations), bytes in the documents bucket
under the company prefix so its RLS applies unchanged, and the full
archive copies them into bilagor/ with a hash manifest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz

* fix(reconciliation): literal selects and payload in the attachments store so the phantom-column scanner can read them

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz

* feat(bokslut): persisted closing checklist and missing-fiscal-year warning (Reko bilagor, PR 3) (#1867)

* feat(bokslut): persisted closing checklist and missing-fiscal-year warning (Reko bilagor, PR 3)

The bokslut checklist is a catalogue in code with one state row per period
(bokslut_checklist_items): the steps the system can judge (sign-offs through
balansdagen, reskontra tie-outs, drafts, voucher gaps, trial balance) are
computed live and a stored row only overrides them; the manual steps are
the konsult's ticks, with who and when. It sits on the wizard's Kontroll
step and is dumped into the full archive.

A hole between fiscal years (one-file SIE migrations) is now named on the
bokslut readiness screen and on the import result screen, where the next
file is one click away. Non-adjacent period links are #1849's fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz

* fix(bokslut): count unexplained voucher gaps, literal select and payload for the checklist store

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
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>
2026-08-25 09:34:05 +02:00

57 lines
2.0 KiB
TypeScript

/**
* Missing räkenskapsår between the ones a company has. SIE exports carry one
* year per file, so a migration that imported 2024 and let the app create
* 2026 leaves 2025 absent: balances stop rolling, the 2026 IB is empty, and
* nothing said so until a bokslut failed on continuity. Pure and UTC-only,
* shared by the readiness warnings, the import result screen and the
* checklist.
*/
export interface PeriodLike {
id: string
name: string
period_start: string
period_end: string
}
export interface FiscalYearGap {
/** The period before the hole. */
after: PeriodLike
/** The period after the hole. */
before: PeriodLike
/** First and last missing day (inclusive). */
missing_from: string
missing_to: string
}
function shiftIsoDate(isoDate: string, days: number): string {
const d = new Date(isoDate + 'T00:00:00Z')
d.setUTCDate(d.getUTCDate() + days)
return d.toISOString().slice(0, 10)
}
/** Every hole between consecutive periods, oldest first. Overlaps are not gaps and are ignored. */
export function findFiscalYearGaps(periods: readonly PeriodLike[]): FiscalYearGap[] {
const sorted = [...periods].sort((a, b) => a.period_start.localeCompare(b.period_start))
const gaps: FiscalYearGap[] = []
for (let i = 0; i + 1 < sorted.length; i += 1) {
const after = sorted[i]
const before = sorted[i + 1]
const expectedStart = shiftIsoDate(after.period_end, 1)
if (before.period_start > expectedStart) {
gaps.push({
after,
before,
missing_from: expectedStart,
missing_to: shiftIsoDate(before.period_start, -1),
})
}
}
return gaps
}
/** Swedish, user-facing: "Räkenskapsår saknas: 2025-01-01 till 2025-12-31 (mellan Räkenskapsår 2024 och Räkenskapsår 2026)." */
export function describeFiscalYearGap(gap: FiscalYearGap): string {
return `Räkenskapsår saknas: ${gap.missing_from} till ${gap.missing_to} (mellan ${gap.after.name} och ${gap.before.name}). Importera eller skapa det innan bokslutet, annars rullar inga balanser fram.`
}