feat(arsredovisning): manual override for medelantal anställda (Not 2) (#2420)
* feat(arsredovisning): manual override for medelantal anstallda (Not 2) Why the problem occurred: the ÅRL 5:20 § note was derived only from the employees table, and most aktiebolag that book salary never create a Löner employee record (hand-booked salary, SIE import, migrated history). In prod 148 of 195 aktiebolag with posted 70xx-73xx lines have no employees rows, so their note reads "inga anställda". The reporting company had one row created the same day with a start date halfway through a July-June year: 181/365 = 0.5 FTE rounds to 0. What was removed or simplified: nothing removed. One resolver (resolveMedelantalAnstallda: override, else FTE average) now feeds the K2 note, the K3 note and the iXBRL fact, so no reader can pick a different number. The override sits on arsredovisning_narratives next to the other ÅRL 5 kap. disclosures and rides the existing narrative GET/POST route, service and page save. Why this and not the proposed one: the request asked support to "enable override of Not 2" as free text. A whole number keeps the statutory sentence intact and the iXBRL MedelantaletAnstallda fact taggable; free text would allow a non-compliant note. Rounding 0.5 up globally was rejected (changes every company's note silently, does nothing for companies with no employees rows), as was backdating the hire date (fixes one company, misstates the fact). The iXBRL input also reads the previous period's override so the jämförelseår column shows what last year's document showed. Migration 20260908130127 is additive (nullable INTEGER with a CHECK) and is applied and tracked on staging. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GxiKQke7sY6mrAK9KX4hbD * fix(arsredovisning): size-threshold metrics use the medelantal override too Skeptic refutation on 442fa1bc5: reportMetrics in model.ts still read the FTE average from the employees table, so the ÅRL 1:3 § större- företag test and the K2 relief thresholds could disagree with the figure Not 2 and the iXBRL fact disclose. A SIE-migrated aktiebolag with no employees rows and an override of 60 both years, balansomslutning over 40 MSEK, would have validated as K2-eligible while its own document said 60 employees. Fix: the metrics resolve the employee figure the same way the note does (current period override from report.disclosures, previous period via getMedelantalOverride on that period's narrative row). previous_period on ArsredovisningData now carries the period id so the lookup needs no extra fiscal_periods read. The iXBRL employees-error fallback keeps the previous period's override instead of blanking the jämförelseår. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GxiKQke7sY6mrAK9KX4hbD --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
59d5b7b366
commit
e85eac317c
@@ -12,7 +12,8 @@ import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
import { generateTrialBalance } from '@/lib/reports/trial-balance'
|
||||
import { buildArsredovisningData } from '@/lib/bokslut/arsredovisning/build-data'
|
||||
import { listSignatureRequests } from '@/lib/bokslut/arsredovisning/signature-service'
|
||||
import { computeMedelantalAnstallda } from '@/lib/salary/medelantal'
|
||||
import { resolveMedelantalAnstallda } from '@/lib/salary/medelantal'
|
||||
import { getMedelantalOverride } from '@/lib/bokslut/arsredovisning/narrative-service'
|
||||
import { mapTrialBalancesToK2, type TrialBalancePair } from './k2-mapper'
|
||||
import { resolveEntryPoint } from './taxonomy/entry-points'
|
||||
import type {
|
||||
@@ -89,6 +90,7 @@ export async function buildIxbrlInput(
|
||||
// Previous period: trial balances for jämförelsesiffror (same full/
|
||||
// pre-closing split as the current year).
|
||||
let previousPeriod: { start: string; end: string } | null = null
|
||||
let previousPeriodId: string | null = null
|
||||
let previousTb: TrialBalancePair | null = null
|
||||
if (period.previous_period_id) {
|
||||
const { data: prev } = await supabase
|
||||
@@ -99,6 +101,7 @@ export async function buildIxbrlInput(
|
||||
.maybeSingle()
|
||||
if (prev) {
|
||||
previousPeriod = { start: prev.period_start, end: prev.period_end }
|
||||
previousPeriodId = prev.id
|
||||
try {
|
||||
const [prevFull, prevPreClosing] = await Promise.all([
|
||||
generateTrialBalance(supabase, companyId, prev.id, { closingEntry: 'include' }),
|
||||
@@ -326,16 +329,23 @@ export async function buildIxbrlInput(
|
||||
}
|
||||
|
||||
// ---- medelantal anställda ---------------------------------------------------
|
||||
// Compute BOTH years with the real FTE helper (the same one the PDF note
|
||||
// uses) over the employees table. The note-prose regex stays only as a
|
||||
// last-resort fallback when the employees query fails.
|
||||
// Compute BOTH years with the same resolver the PDF note uses: a manual
|
||||
// override on arsredovisning_narratives for that period wins, otherwise
|
||||
// the FTE average over the employees table. The note-prose regex stays
|
||||
// only as a last-resort fallback when the employees query fails.
|
||||
let medelantalAnstallda: { current: number; previous: number | null }
|
||||
const { data: employeeRows, error: employeesError } = await supabase
|
||||
.from('employees')
|
||||
.select('employment_start, employment_end, employment_degree')
|
||||
.eq('company_id', companyId)
|
||||
const [{ data: employeeRows, error: employeesError }, previousOverride] = await Promise.all([
|
||||
supabase
|
||||
.from('employees')
|
||||
.select('employment_start, employment_end, employment_degree')
|
||||
.eq('company_id', companyId),
|
||||
previousPeriodId ? getMedelantalOverride(supabase, companyId, previousPeriodId) : null,
|
||||
])
|
||||
if (employeesError) {
|
||||
medelantalAnstallda = extractMedelantal(pdfData.noter, null)
|
||||
// The note already embeds the current year's override; last year's
|
||||
// manual figure is still worth showing when only the employees read
|
||||
// failed.
|
||||
medelantalAnstallda = extractMedelantal(pdfData.noter, previousOverride)
|
||||
} else {
|
||||
const employees = (employeeRows ?? []) as Array<{
|
||||
employment_start: string
|
||||
@@ -343,9 +353,19 @@ export async function buildIxbrlInput(
|
||||
employment_degree: number
|
||||
}>
|
||||
medelantalAnstallda = {
|
||||
current: computeMedelantalAnstallda(employees, period.period_start, period.period_end),
|
||||
current: resolveMedelantalAnstallda(
|
||||
pdfData.disclosures.medelantal_anstallda_override,
|
||||
employees,
|
||||
period.period_start,
|
||||
period.period_end,
|
||||
),
|
||||
previous: previousPeriod
|
||||
? computeMedelantalAnstallda(employees, previousPeriod.start, previousPeriod.end)
|
||||
? resolveMedelantalAnstallda(
|
||||
previousOverride,
|
||||
employees,
|
||||
previousPeriod.start,
|
||||
previousPeriod.end,
|
||||
)
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user