fix(bookkeeping): book pre-FY bank transactions on the fiscal year's first day (#1828)

A newly registered company whose first rakenskapsar starts on the
Bolagsverket registration date could not book the aktiekapital deposit,
because the bank transaction is dated BEFORE the registration. Every
surface dead-ended: the manual booking dialog hard-blocked with the date
locked and only offered creating a (legally wrong) pre-registration
fiscal year, and the categorize paths either marked the row categorized
WITHOUT a verifikat ("Delvis bokforda") or silently minted a bogus
calendar-year period before the company existed.

Root cause: entry_date was hard-wired to the bank date with no clamp
against the company's first fiscal period, and the duplicated
ensureFiscalPeriod helpers upserted a calendar-year period for any
uncovered date.

Fix, per BFL (the event belongs to the first fiscal year; the real
affarshaendelse date is preserved on the verifikat):
- createTransactionJournalEntry clamps a pre-FY date into the earliest
  OPEN unlocked fiscal period with entry_date = period_start and appends
  "Affarshaendelse <date>, bokford pa rakenskapsarets forsta dag" to the
  verifikationstext. Interior gaps, future dates, and a closed/locked
  first year keep the old null return.
- Both ensureFiscalPeriod copies (categorize core + web route) skip the
  calendar-period upsert when the date predates the earliest period.
- JournalEntryForm's no_period block offers "Bokfor pa rakenskapsarets
  forsta dag (<date>)" for pre-FY dates instead of proposing a
  pre-registration year; "Skapa rakenskapsar" remains for the other
  no_period cases.

No schema, RPC, or trigger changes: the /book route and the DB triggers
already accept the clamped booking.

Fixes #1825


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

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-24 13:19:50 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5
parent 62135c0c02
commit 8e3015e541
11 changed files with 470 additions and 20 deletions
+50 -11
View File
@@ -328,6 +328,23 @@ export default function JournalEntryForm({
}
}, [entryDate, periods])
// Earliest fiscal period, for the pre-FY affordance in the no_period block:
// a date before the company's first rakenskapsar (e.g. the aktiekapital
// deposit paid in before the Bolagsverket registration) must not lead to
// creating a pre-registration year. The correct remedy per BFL is to book
// the event on the first fiscal year's first day, so we offer exactly that
// when the earliest period is open and unlocked (issue #1825).
const preFyClampTarget = useMemo(() => {
const earliest = periods.reduce<FiscalPeriod | null>(
(min, p) => (min === null || p.period_start < min.period_start ? p : min),
null,
)
if (!earliest) return null
if (entryDate >= earliest.period_start) return null
if (earliest.is_closed || earliest.locked_at) return null
return earliest
}, [periods, entryDate])
// Preview the upcoming voucher number for the selected period + series.
// Read-only hint; the actual number is reserved atomically at commit time,
// so this may shift by one if another entry lands first.
@@ -1514,17 +1531,37 @@ export default function JournalEntryForm({
<AlertTriangle className="h-5 w-5 text-attn mt-0.5 shrink-0" />
<div className="flex-1 text-sm text-attn">
<p className="font-medium">{t('no_period_warning', { date: entryDate })}</p>
<p className="mt-0.5">{t('no_period_help')}</p>
<p className="mt-0.5">
{preFyClampTarget ? t('pre_fy_help') : t('no_period_help')}
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => setShowCreatePeriod(true)}
className="shrink-0"
>
<CalendarPlus className="h-3.5 w-3.5 mr-1.5" />
{t('create_period')}
</Button>
{/* Pre-FY: the date predates the first rakenskapsar. Offering
"Skapa räkenskapsår" here would propose a pre-registration year
(legally wrong); the correct remedy is booking on the first
fiscal year's first day, so offer that instead. Setting the
entry date state drives the /book payload even in embedded mode
where the date input is hidden. */}
{preFyClampTarget ? (
<Button
variant="outline"
size="sm"
onClick={() => setEntryDate(preFyClampTarget.period_start)}
className="shrink-0"
>
<CalendarPlus className="h-3.5 w-3.5 mr-1.5" />
{t('pre_fy_book_on_first_day', { date: preFyClampTarget.period_start })}
</Button>
) : (
<Button
variant="outline"
size="sm"
onClick={() => setShowCreatePeriod(true)}
className="shrink-0"
>
<CalendarPlus className="h-3.5 w-3.5 mr-1.5" />
{t('create_period')}
</Button>
)}
</div>
)}
</div>
@@ -1951,7 +1988,9 @@ export default function JournalEntryForm({
<div className="text-xs text-destructive space-y-0.5 text-right">
{!description && <p>{t('validation_description')}</p>}
{!selectedPeriod && <p>{t('validation_period')}</p>}
{periodMismatch === 'no_period' && <p>{t('validation_no_matching_period')}</p>}
{periodMismatch === 'no_period' && (
<p>{preFyClampTarget ? t('validation_pre_fy') : t('validation_no_matching_period')}</p>
)}
{isUploading && <p>{t('validation_uploading')}</p>}
{incompleteLineCount > 0 && (
<p>{t('validation_incomplete_lines')}</p>