fix(periodisering): stop overselling automatic periodization to enskild firma (#1730)
* 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>
This commit is contained in:
co-authored by
Claude Fable 5
Jakob Wennberg
parent
c402421908
commit
64fc7c783d
@@ -234,7 +234,9 @@ describe('detectPeriodisering', () => {
|
||||
id: 'sup-high',
|
||||
supplier_invoice_number: 'LF-B',
|
||||
invoice_date: '2025-12-01',
|
||||
subtotal: 3000, // smaller, but high confidence
|
||||
// Smaller, but high confidence. Kept above the 5 000 kr materiality
|
||||
// floor so the floor's low-confidence downgrade doesn't apply here.
|
||||
subtotal: 6000,
|
||||
notes: 'Mjukvara perioden 2026-01-01 till 2026-12-31',
|
||||
suppliers: { name: 'B' },
|
||||
supplier_invoice_items: [{ description: 'License', account_number: '5800' }],
|
||||
@@ -288,4 +290,281 @@ describe('detectPeriodisering', () => {
|
||||
// Suggesting it again would periodisera the same belopp twice.
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('tags a suggestion under 5 000 kr as low confidence citing K2 by default', async () => {
|
||||
mock.enqueue({
|
||||
data: { id: 'period-1', period_start: '2025-01-01', period_end: '2025-12-31' },
|
||||
error: null,
|
||||
})
|
||||
mock.enqueue({ data: [], error: null }) // accrual_schedules
|
||||
mock.enqueue({ data: [], error: null }) // invoices
|
||||
// 1 200 kr domain renewal fully in next year: below the materiality floor.
|
||||
mock.enqueue({
|
||||
data: [
|
||||
{
|
||||
id: 'sup-small',
|
||||
supplier_invoice_number: 'LF-500',
|
||||
invoice_date: '2025-12-15',
|
||||
subtotal: 1200,
|
||||
currency: 'SEK',
|
||||
subtotal_sek: 1200,
|
||||
notes: 'Domänförnyelse period 2026-01-01 till 2026-12-31',
|
||||
suppliers: { name: 'Registrar AB' },
|
||||
supplier_invoice_items: [{ description: 'Domän', account_number: '6540' }],
|
||||
},
|
||||
],
|
||||
error: null,
|
||||
})
|
||||
|
||||
const result = await detectPeriodisering(
|
||||
mock.supabase as never,
|
||||
'company-1',
|
||||
'period-1',
|
||||
)
|
||||
expect(result).toHaveLength(1)
|
||||
// Downgraded so the wizard does NOT pre-tick it (only 'high' is pre-ticked).
|
||||
expect(result[0].confidence).toBe('low')
|
||||
expect(result[0].reason).toContain('Under 5 000 kr: behöver normalt inte periodiseras (K2).')
|
||||
})
|
||||
|
||||
it('cites K1 in the under-floor reason for an enskild firma', async () => {
|
||||
mock.enqueue({
|
||||
data: { id: 'period-1', period_start: '2025-01-01', period_end: '2025-12-31' },
|
||||
error: null,
|
||||
})
|
||||
mock.enqueue({ data: [], error: null }) // accrual_schedules
|
||||
mock.enqueue({ data: [], error: null }) // invoices
|
||||
mock.enqueue({
|
||||
data: [
|
||||
{
|
||||
id: 'sup-small-ef',
|
||||
supplier_invoice_number: 'LF-501',
|
||||
invoice_date: '2025-12-15',
|
||||
subtotal: 1200,
|
||||
notes: 'Domänförnyelse period 2026-01-01 till 2026-12-31',
|
||||
suppliers: { name: 'Registrar AB' },
|
||||
supplier_invoice_items: [{ description: 'Domän', account_number: '6540' }],
|
||||
},
|
||||
],
|
||||
error: null,
|
||||
})
|
||||
|
||||
const result = await detectPeriodisering(
|
||||
mock.supabase as never,
|
||||
'company-1',
|
||||
'period-1',
|
||||
{ entityType: 'enskild_firma' },
|
||||
)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].confidence).toBe('low')
|
||||
expect(result[0].reason).toContain('Under 5 000 kr: behöver normalt inte periodiseras (K1).')
|
||||
})
|
||||
|
||||
it('keeps a suggestion at or above 5 000 kr at its original confidence', async () => {
|
||||
mock.enqueue({
|
||||
data: { id: 'period-1', period_start: '2025-01-01', period_end: '2025-12-31' },
|
||||
error: null,
|
||||
})
|
||||
mock.enqueue({ data: [], error: null }) // accrual_schedules
|
||||
mock.enqueue({ data: [], error: null }) // invoices
|
||||
mock.enqueue({
|
||||
data: [
|
||||
{
|
||||
id: 'sup-large',
|
||||
supplier_invoice_number: 'LF-502',
|
||||
invoice_date: '2025-07-01',
|
||||
subtotal: 12000,
|
||||
notes: 'Mjukvarulicens period: 2025-07-01 till 2026-06-30',
|
||||
suppliers: { name: 'Acme SaaS AB' },
|
||||
supplier_invoice_items: [{ description: 'Årslicens', account_number: '5800' }],
|
||||
},
|
||||
],
|
||||
error: null,
|
||||
})
|
||||
|
||||
const result = await detectPeriodisering(
|
||||
mock.supabase as never,
|
||||
'company-1',
|
||||
'period-1',
|
||||
{ entityType: 'enskild_firma' },
|
||||
)
|
||||
expect(result).toHaveLength(1)
|
||||
// 12000 * 181/365 = 5950.68: above the floor, stays high with no K1 note.
|
||||
expect(result[0].confidence).toBe('high')
|
||||
expect(result[0].reason).not.toContain('Under 5 000 kr')
|
||||
})
|
||||
|
||||
it('compares the floor against the SEK amount for a foreign-currency invoice', async () => {
|
||||
mock.enqueue({
|
||||
data: { id: 'period-1', period_start: '2025-01-01', period_end: '2025-12-31' },
|
||||
error: null,
|
||||
})
|
||||
mock.enqueue({ data: [], error: null }) // accrual_schedules
|
||||
mock.enqueue({ data: [], error: null }) // invoices
|
||||
// 460 EUR is numerically under 5 000, but its SEK equivalent (5 200 kr)
|
||||
// is ABOVE the floor: comparing the raw EUR number would wrongly tag it.
|
||||
mock.enqueue({
|
||||
data: [
|
||||
{
|
||||
id: 'sup-eur',
|
||||
supplier_invoice_number: 'LF-510',
|
||||
invoice_date: '2025-12-15',
|
||||
subtotal: 460,
|
||||
currency: 'EUR',
|
||||
subtotal_sek: 5200,
|
||||
notes: 'SaaS-licens period 2026-01-01 till 2026-12-31',
|
||||
suppliers: { name: 'Euro SaaS GmbH' },
|
||||
supplier_invoice_items: [{ description: 'License', account_number: '5800' }],
|
||||
},
|
||||
],
|
||||
error: null,
|
||||
})
|
||||
|
||||
const result = await detectPeriodisering(
|
||||
mock.supabase as never,
|
||||
'company-1',
|
||||
'period-1',
|
||||
)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].confidence).toBe('high')
|
||||
expect(result[0].reason).not.toContain('Under 5 000 kr')
|
||||
})
|
||||
|
||||
it('skips the floor entirely for a foreign-currency invoice without subtotal_sek', async () => {
|
||||
mock.enqueue({
|
||||
data: { id: 'period-1', period_start: '2025-01-01', period_end: '2025-12-31' },
|
||||
error: null,
|
||||
})
|
||||
mock.enqueue({ data: [], error: null }) // accrual_schedules
|
||||
mock.enqueue({ data: [], error: null }) // invoices
|
||||
// No SEK amount is resolvable, so the floor must not tag on the raw EUR
|
||||
// number (wrong currency): the suggestion keeps its confidence.
|
||||
mock.enqueue({
|
||||
data: [
|
||||
{
|
||||
id: 'sup-eur-nosek',
|
||||
supplier_invoice_number: 'LF-511',
|
||||
invoice_date: '2025-12-15',
|
||||
subtotal: 120,
|
||||
currency: 'EUR',
|
||||
subtotal_sek: null,
|
||||
notes: 'SaaS-licens period 2026-01-01 till 2026-12-31',
|
||||
suppliers: { name: 'Euro SaaS GmbH' },
|
||||
supplier_invoice_items: [{ description: 'License', account_number: '5800' }],
|
||||
},
|
||||
],
|
||||
error: null,
|
||||
})
|
||||
|
||||
const result = await detectPeriodisering(
|
||||
mock.supabase as never,
|
||||
'company-1',
|
||||
'period-1',
|
||||
)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].confidence).toBe('high')
|
||||
expect(result[0].reason).not.toContain('Under 5 000 kr')
|
||||
})
|
||||
|
||||
it('never applies the floor to personnel-cost (70xx-76xx) lines', async () => {
|
||||
mock.enqueue({
|
||||
data: { id: 'period-1', period_start: '2025-01-01', period_end: '2025-12-31' },
|
||||
error: null,
|
||||
})
|
||||
mock.enqueue({ data: [], error: null }) // accrual_schedules
|
||||
mock.enqueue({ data: [], error: null }) // invoices
|
||||
// Personnel costs must ALWAYS be accrued regardless of amount, so a
|
||||
// 1 500 kr post on a 7xxx account keeps its confidence.
|
||||
mock.enqueue({
|
||||
data: [
|
||||
{
|
||||
id: 'sup-personnel',
|
||||
supplier_invoice_number: 'LF-503',
|
||||
invoice_date: '2025-12-15',
|
||||
subtotal: 1500,
|
||||
notes: 'Utbildning personal period 2026-01-01 till 2026-03-31',
|
||||
suppliers: { name: 'Kursbolaget AB' },
|
||||
supplier_invoice_items: [{ description: 'Kurs', account_number: '7610' }],
|
||||
},
|
||||
],
|
||||
error: null,
|
||||
})
|
||||
|
||||
const result = await detectPeriodisering(
|
||||
mock.supabase as never,
|
||||
'company-1',
|
||||
'period-1',
|
||||
{ entityType: 'enskild_firma' },
|
||||
)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].confidence).toBe('high')
|
||||
expect(result[0].reason).not.toContain('Under 5 000 kr')
|
||||
})
|
||||
|
||||
it('applies the floor to a 79xx line: not a personnel cost', async () => {
|
||||
mock.enqueue({
|
||||
data: { id: 'period-1', period_start: '2025-01-01', period_end: '2025-12-31' },
|
||||
error: null,
|
||||
})
|
||||
mock.enqueue({ data: [], error: null }) // accrual_schedules
|
||||
mock.enqueue({ data: [], error: null }) // invoices
|
||||
// 7990 (övriga rörelsekostnader) is in the 7xxx class but is NOT a
|
||||
// personnel cost: the exemption is BAS 70xx-76xx only, so a small 7990
|
||||
// post gets the normal under-floor downgrade.
|
||||
mock.enqueue({
|
||||
data: [
|
||||
{
|
||||
id: 'sup-7990',
|
||||
supplier_invoice_number: 'LF-504',
|
||||
invoice_date: '2025-12-15',
|
||||
subtotal: 1500,
|
||||
notes: 'Diverse kostnad period 2026-01-01 till 2026-03-31',
|
||||
suppliers: { name: 'Diverse AB' },
|
||||
supplier_invoice_items: [{ description: 'Övrigt', account_number: '7990' }],
|
||||
},
|
||||
],
|
||||
error: null,
|
||||
})
|
||||
|
||||
const result = await detectPeriodisering(
|
||||
mock.supabase as never,
|
||||
'company-1',
|
||||
'period-1',
|
||||
)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].confidence).toBe('low')
|
||||
expect(result[0].reason).toContain('Under 5 000 kr')
|
||||
})
|
||||
|
||||
it('applies the floor to a small customer-invoice revenue deferral', async () => {
|
||||
mock.enqueue({
|
||||
data: { id: 'period-1', period_start: '2025-01-01', period_end: '2025-12-31' },
|
||||
error: null,
|
||||
})
|
||||
mock.enqueue({ data: [], error: null }) // accrual_schedules
|
||||
mock.enqueue({
|
||||
data: [
|
||||
{
|
||||
id: 'inv-small',
|
||||
invoice_number: 'F-3001',
|
||||
invoice_date: '2025-12-01',
|
||||
subtotal: 2400,
|
||||
notes: 'Supportavtal för period 2026-01-01 till 2026-12-31',
|
||||
customers: { name: 'Kund AB' },
|
||||
invoice_items: [{ description: 'Support' }],
|
||||
},
|
||||
],
|
||||
error: null,
|
||||
})
|
||||
mock.enqueue({ data: [], error: null }) // supplier_invoices
|
||||
|
||||
const result = await detectPeriodisering(
|
||||
mock.supabase as never,
|
||||
'company-1',
|
||||
'period-1',
|
||||
)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].confidence).toBe('low')
|
||||
expect(result[0].reason).toContain('Under 5 000 kr')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,6 +5,28 @@ import { parseInvoiceDateRange } from './date-range-parser'
|
||||
export type PeriodiseringSource = 'invoice' | 'supplier_invoice'
|
||||
export type PeriodiseringConfidence = 'high' | 'medium' | 'low'
|
||||
|
||||
/** The entity types the materiality wording distinguishes between. Mirrors
|
||||
* `EntityType` in `@/types` without importing app types into lib/. */
|
||||
export type PeriodiseringEntityType = 'enskild_firma' | 'aktiebolag'
|
||||
|
||||
/**
|
||||
* Materiality floor for auto-detected periodiseringar, in SEK.
|
||||
*
|
||||
* Both simplification tracks land on the same number: K1 (BFNAR 2006:1,
|
||||
* förenklat årsbokslut for enskild firma) has no requirement to accrue posts
|
||||
* below 5 000 kr, and K2 (BFNAR 2016:10) lets a company skip accruing
|
||||
* individual recurring costs below 5 000 kr. Suggestions under the floor are
|
||||
* TAGGED as low confidence rather than dropped: the relief is a MAY, never a
|
||||
* MUST, so the user can still accept them. Personnel costs (BAS 70xx-76xx)
|
||||
* must always be accrued regardless of amount, so the floor never applies
|
||||
* there. The floor is a SEK threshold: for foreign-currency invoices it is
|
||||
* compared against the subtotal_sek-derived amount, and when no SEK amount
|
||||
* can be resolved the floor is skipped entirely rather than compared against
|
||||
* a number in the wrong currency (mirrors the accrual-k2-hint decision,
|
||||
* DECISIONS.md 2026-07-26).
|
||||
*/
|
||||
export const PERIODISERING_MATERIALITY_FLOOR_SEK = 5000
|
||||
|
||||
export interface PeriodiseringSuggestion {
|
||||
/** Underlying source invoice id (invoices.id or supplier_invoices.id). */
|
||||
source_invoice_id: string
|
||||
@@ -38,6 +60,8 @@ interface InvoiceRow {
|
||||
invoice_number: string | null
|
||||
invoice_date: string
|
||||
subtotal: number
|
||||
currency: string | null
|
||||
subtotal_sek: number | null
|
||||
notes: string | null
|
||||
customers: { name: string } | null
|
||||
invoice_items: { description: string }[] | null
|
||||
@@ -48,6 +72,8 @@ interface SupplierInvoiceRow {
|
||||
supplier_invoice_number: string
|
||||
invoice_date: string
|
||||
subtotal: number
|
||||
currency: string | null
|
||||
subtotal_sek: number | null
|
||||
notes: string | null
|
||||
suppliers: { name: string } | null
|
||||
supplier_invoice_items: { description: string; account_number: string }[] | null
|
||||
@@ -84,17 +110,26 @@ function buildSuggestion(args: {
|
||||
sourceId: string
|
||||
sourceType: PeriodiseringSource
|
||||
netAmount: number
|
||||
/** Invoice currency (ISO code). Null/undefined is treated as SEK. */
|
||||
currency: string | null
|
||||
/** SEK-converted net amount (subtotal_sek). Null when the invoice predates
|
||||
* the SEK columns or no exchange rate was captured. */
|
||||
netAmountSek: number | null
|
||||
description: string | null
|
||||
itemDescriptions: string[]
|
||||
/** Default expense account from the first supplier-invoice line. Reserved
|
||||
* for a future enhancement where the wizard can pre-fill the manual-entry
|
||||
* form with the actual account rather than the 5800 fallback. Not used
|
||||
* yet but kept on the buildSuggestion args to keep the call sites stable. */
|
||||
_itemDefaultAccount: string | null
|
||||
/** Account numbers of the source lines (supplier invoices only; customer
|
||||
* invoices carry no expense accounts). The first entry doubles as the
|
||||
* default expense account for a future manual-entry pre-fill; today the
|
||||
* list only drives the personnel-cost (70xx-76xx) exemption from the
|
||||
* materiality floor. */
|
||||
itemAccounts: string[]
|
||||
sourceLabel: string
|
||||
periodEnd: string
|
||||
/** Drives the regelverk cited in the materiality wording: K1 (BFNAR
|
||||
* 2006:1) for enskild firma, K2 (BFNAR 2016:10) otherwise. */
|
||||
entityType?: PeriodiseringEntityType | null
|
||||
}): PeriodiseringSuggestion | null {
|
||||
const { sourceId, sourceType, netAmount, description, itemDescriptions, sourceLabel, periodEnd } = args
|
||||
const { sourceId, sourceType, netAmount, currency, netAmountSek, description, itemDescriptions, itemAccounts, sourceLabel, periodEnd, entityType } = args
|
||||
if (!Number.isFinite(netAmount) || netAmount <= 0) return null
|
||||
|
||||
// Try the head text first, then each item: first hit wins.
|
||||
@@ -129,15 +164,44 @@ function buildSuggestion(args: {
|
||||
|
||||
// Confidence policy: parsed from the head description wins "high"; parsed
|
||||
// from a line item lands at "medium" since the head text is the canonical
|
||||
// location. "low" is reserved for future heuristics that catch e.g. a
|
||||
// single date + interpretation rules.
|
||||
const confidence: PeriodiseringConfidence = parsedFromItem ? 'medium' : 'high'
|
||||
// location.
|
||||
let confidence: PeriodiseringConfidence = parsedFromItem ? 'medium' : 'high'
|
||||
|
||||
const isSupplier = sourceType === 'supplier_invoice'
|
||||
const reason = isSupplier
|
||||
let reason = isSupplier
|
||||
? `Leverantörsfakturan löper ${parsed.startDate}: ${parsed.endDate}. ${daysAfterPeriodEnd} av ${totalDays} dagar avser nästa räkenskapsår.`
|
||||
: `Kundfakturan löper ${parsed.startDate}: ${parsed.endDate}. ${daysAfterPeriodEnd} av ${totalDays} dagar avser nästa räkenskapsår.`
|
||||
|
||||
// Materiality floor: below 5 000 kr the K1/K2 simplifications say the post
|
||||
// normally need not be accrued, so downgrade to "low" (the wizard only
|
||||
// pre-ticks "high") and say why. Personnel costs (BAS 70xx-76xx) are exempt
|
||||
// from the relief and keep their confidence: they must always be accrued.
|
||||
// 78xx (avskrivningar) and 79xx (övriga rörelsekostnader) are NOT personnel
|
||||
// costs, so they get the relief like any other cost.
|
||||
//
|
||||
// The floor is a SEK threshold. For a foreign-currency invoice the
|
||||
// comparison uses the periodisation share of subtotal_sek; when no SEK
|
||||
// amount is available the floor is SKIPPED entirely, because tagging on an
|
||||
// amount in the wrong currency is strictly worse than not tagging
|
||||
// (accrual-k2-hint precedent, DECISIONS.md 2026-07-26).
|
||||
const touchesPersonnelCost = itemAccounts.some((a) => /^7[0-6]/.test(a ?? ''))
|
||||
const isSek = !currency || currency === 'SEK'
|
||||
let periodisationAmountSek: number | null = null
|
||||
if (isSek) {
|
||||
periodisationAmountSek = periodisationAmount
|
||||
} else if (netAmountSek != null && Number.isFinite(netAmountSek) && netAmountSek > 0) {
|
||||
periodisationAmountSek = roundOre(netAmountSek * ratio)
|
||||
}
|
||||
if (
|
||||
periodisationAmountSek !== null &&
|
||||
periodisationAmountSek < PERIODISERING_MATERIALITY_FLOOR_SEK &&
|
||||
!touchesPersonnelCost
|
||||
) {
|
||||
confidence = 'low'
|
||||
const regelverk = entityType === 'enskild_firma' ? 'K1' : 'K2'
|
||||
reason = `${reason} Under 5 000 kr: behöver normalt inte periodiseras (${regelverk}).`
|
||||
}
|
||||
|
||||
return {
|
||||
source_invoice_id: sourceId,
|
||||
source_type: sourceType,
|
||||
@@ -167,7 +231,14 @@ export async function detectPeriodisering(
|
||||
supabase: SupabaseClient,
|
||||
companyId: string,
|
||||
fiscalPeriodId: string,
|
||||
options?: {
|
||||
/** Company entity type: chooses the regelverk the materiality wording
|
||||
* cites (K1 for enskild_firma, K2 otherwise). Optional so callers that
|
||||
* cannot resolve it still get suggestions with the K2 default. */
|
||||
entityType?: PeriodiseringEntityType | null
|
||||
},
|
||||
): Promise<PeriodiseringSuggestion[]> {
|
||||
const entityType = options?.entityType ?? null
|
||||
// Resolve the fiscal period window. We scope candidate invoices to those
|
||||
// dated within the period: anything outside is either an opening-balance
|
||||
// carryover (its own concern) or a future invoice (no period to detect).
|
||||
@@ -208,7 +279,7 @@ export async function detectPeriodisering(
|
||||
// status label that overlaps with sent here.
|
||||
const { data: invoiceRows } = await supabase
|
||||
.from('invoices')
|
||||
.select('id, invoice_number, invoice_date, subtotal, notes, customers(name), invoice_items(description)')
|
||||
.select('id, invoice_number, invoice_date, subtotal, currency, subtotal_sek, notes, customers(name), invoice_items(description)')
|
||||
.eq('company_id', companyId)
|
||||
.gte('invoice_date', periodStart)
|
||||
.lte('invoice_date', periodEnd)
|
||||
@@ -218,7 +289,7 @@ export async function detectPeriodisering(
|
||||
const { data: supplierRows } = await supabase
|
||||
.from('supplier_invoices')
|
||||
.select(
|
||||
'id, supplier_invoice_number, invoice_date, subtotal, notes, suppliers(name), supplier_invoice_items(description, account_number)',
|
||||
'id, supplier_invoice_number, invoice_date, subtotal, currency, subtotal_sek, notes, suppliers(name), supplier_invoice_items(description, account_number)',
|
||||
)
|
||||
.eq('company_id', companyId)
|
||||
.gte('invoice_date', periodStart)
|
||||
@@ -238,11 +309,14 @@ export async function detectPeriodisering(
|
||||
sourceId: row.id,
|
||||
sourceType: 'invoice',
|
||||
netAmount: Number(row.subtotal ?? 0),
|
||||
currency: row.currency ?? null,
|
||||
netAmountSek: row.subtotal_sek != null ? Number(row.subtotal_sek) : null,
|
||||
description: row.notes,
|
||||
itemDescriptions: itemDescs,
|
||||
_itemDefaultAccount: null,
|
||||
itemAccounts: [],
|
||||
sourceLabel,
|
||||
periodEnd,
|
||||
entityType,
|
||||
})
|
||||
if (s) suggestions.push(s)
|
||||
}
|
||||
@@ -250,18 +324,23 @@ export async function detectPeriodisering(
|
||||
for (const row of (supplierRows ?? []) as unknown as SupplierInvoiceRow[]) {
|
||||
if (coveredSupplierInvoices.has(row.id)) continue
|
||||
const itemDescs = (row.supplier_invoice_items ?? []).map((i) => i.description).filter(Boolean)
|
||||
const firstAccount = row.supplier_invoice_items?.[0]?.account_number ?? null
|
||||
const itemAccounts = (row.supplier_invoice_items ?? [])
|
||||
.map((i) => i.account_number)
|
||||
.filter(Boolean)
|
||||
const supplierName = row.suppliers?.name ?? 'Okänd leverantör'
|
||||
const sourceLabel = `${supplierName} (lev.faktura ${row.supplier_invoice_number})`
|
||||
const s = buildSuggestion({
|
||||
sourceId: row.id,
|
||||
sourceType: 'supplier_invoice',
|
||||
netAmount: Number(row.subtotal ?? 0),
|
||||
currency: row.currency ?? null,
|
||||
netAmountSek: row.subtotal_sek != null ? Number(row.subtotal_sek) : null,
|
||||
description: row.notes,
|
||||
itemDescriptions: itemDescs,
|
||||
_itemDefaultAccount: firstAccount,
|
||||
itemAccounts,
|
||||
sourceLabel,
|
||||
periodEnd,
|
||||
entityType,
|
||||
})
|
||||
if (s) suggestions.push(s)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user