* fix(bokslut): honest periodisering for enskild firma (K1) Stop mis-selling automatic periodisering to sole traders and give the auto-detect a materiality floor: - Remove the inert PeriodiseringAutoDetectToggle (write-only localStorage, no reader anywhere); the settings row is now a plain link to the periodisering wizard, with new i18n keys in sv+en. - Auto-detect tags suggestions under 5 000 kr as low confidence with the reason 'Under 5 000 kr: behöver normalt inte periodiseras', citing K1 (BFNAR 2006:1) for enskild firma and K2 for aktiebolag; the wizard only pre-ticks high-confidence rows, so under-floor posts land unticked. Personnel-cost lines (7xxx) are exempt: they must always be accrued. - The accruals GET route resolves companies.entity_type and threads it to the detector. - Per-line accrual hint in the invoice editors is entity-aware: new accruals.k1_hint (K1, förenklat årsbokslut) for EF, k2_hint stays for AB. - Periodisering wizard and year-end AccrualsStep relabel Revisionsarvode to Bokslutsarvode for EF, default the liability account to 2991 instead of 2992, and show a muted K1-floor intro line. All copy stays advisory (behöver normalt inte, never får inte): entity_type is a proxy since no förenklat-vs-full-årsbokslut flag exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(bokslut): SEK-correct materiality floor, entity-type via settings, narrower personnel exemption Review fixes on the K1 periodisering branch: - The 5 000 kr floor now compares a SEK amount: queries select currency and subtotal_sek, the floor uses the periodisation share of subtotal_sek for foreign-currency invoices, and is skipped entirely when no SEK amount is resolvable (accrual-k2-hint precedent, DECISIONS.md 2026-07-26). - The accruals route resolves entity type via getCompanyEntityType (company_settings-primary, companies fallback) instead of reading companies.entity_type directly. - The personnel-cost exemption from the floor is narrowed from startsWith('7') to /^7[0-6]/: 78xx/79xx are not personnel costs. 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>
93 lines
3.5 KiB
TypeScript
93 lines
3.5 KiB
TypeScript
/**
|
|
* K2 vasentlighetsgrans for periodisering, extracted as a pure helper so it
|
|
* can be unit-tested (this repo's Vitest runs in a node environment and does
|
|
* not render components).
|
|
*
|
|
* K2 (BFNAR 2016:10) lets a smaller company skip accruing individual
|
|
* recurring costs below 5 000 kr that do not fluctuate more than 20 % year
|
|
* over year; personnel costs must always be accrued. It is a simplification
|
|
* the company MAY use, never an obligation, so the UI copy it drives is
|
|
* advisory only. That is exactly why it must not fire on an amount we cannot
|
|
* express in kronor: an advisory nudge based on the wrong unit is worse than
|
|
* no nudge at all.
|
|
*
|
|
* The invoice editors hold line amounts in the invoice's currency. A 500 EUR
|
|
* line is roughly 5 750 kr and therefore ABOVE the threshold, but a naive
|
|
* `amount < 5000` reads it as below (and a 400 USD line reads the reverse
|
|
* way once the rate moves). Converting needs an FX rate. The supplier-invoice
|
|
* form has one (Riksbanken-fetched or hand-typed); the customer-invoice
|
|
* editor has none. When no rate is available we return null and the caller
|
|
* shows nothing, rather than guessing a branch.
|
|
*/
|
|
|
|
/** BFNAR 2016:10: the K2 accrual simplification ceiling, in SEK. */
|
|
export const K2_ACCRUAL_THRESHOLD_SEK = 5000
|
|
|
|
export interface AccrualAmountInput {
|
|
/** Net line amount, expressed in `currency`. */
|
|
amount: number
|
|
/** ISO currency code of `amount`. Missing/empty is treated as SEK. */
|
|
currency?: string | null
|
|
/** SEK per one unit of `currency`. Required to convert anything non-SEK. */
|
|
exchangeRate?: number | null
|
|
}
|
|
|
|
/**
|
|
* The line amount in SEK, or null when it cannot be determined.
|
|
*
|
|
* Deliberately NOT `resolveSekAmount()` from `@/lib/bookkeeping/currency-utils`:
|
|
* that helper falls back to returning the foreign amount untouched, which is
|
|
* the silent wrong-unit branch this helper exists to avoid.
|
|
*/
|
|
export function resolveAccrualAmountSek({
|
|
amount,
|
|
currency,
|
|
exchangeRate,
|
|
}: AccrualAmountInput): number | null {
|
|
if (typeof amount !== 'number' || !Number.isFinite(amount)) return null
|
|
|
|
const code = (currency ?? '').trim().toUpperCase()
|
|
if (code === '' || code === 'SEK') {
|
|
return Math.round(amount * 100) / 100
|
|
}
|
|
|
|
if (
|
|
typeof exchangeRate !== 'number' ||
|
|
!Number.isFinite(exchangeRate) ||
|
|
exchangeRate <= 0
|
|
) {
|
|
return null
|
|
}
|
|
|
|
return Math.round(amount * exchangeRate * 100) / 100
|
|
}
|
|
|
|
/**
|
|
* Whether to show the "below 5 000 kr need not be deferred" hint.
|
|
*
|
|
* False whenever the SEK value is unknown: no hint beats a hint measured in
|
|
* the wrong currency.
|
|
*/
|
|
export function shouldShowK2AccrualHint(input: AccrualAmountInput): boolean {
|
|
const amountSek = resolveAccrualAmountSek(input)
|
|
if (amountSek === null) return false
|
|
return amountSek > 0 && amountSek < K2_ACCRUAL_THRESHOLD_SEK
|
|
}
|
|
|
|
/**
|
|
* Which `accruals.*` message key carries the materiality hint for the given
|
|
* entity type. An enskild firma normally closes under K1 (BFNAR 2006:1,
|
|
* förenklat årsbokslut), which relieves posts below 5 000 kr; citing K2
|
|
* (BFNAR 2016:10) at a sole trader names a regelverk that does not apply to
|
|
* it. There is no stored förenklat-vs-full-årsbokslut flag, so entity_type
|
|
* is a proxy and the copy stays advisory ("behöver normalt inte").
|
|
*
|
|
* Unknown/missing entity keeps the K2 wording: it is the historical default
|
|
* and correct for every aktiebolag.
|
|
*/
|
|
export function accrualHintKey(
|
|
entityType?: 'enskild_firma' | 'aktiebolag' | null,
|
|
): 'k1_hint' | 'k2_hint' {
|
|
return entityType === 'enskild_firma' ? 'k1_hint' : 'k2_hint'
|
|
}
|