feat: BAS data overhaul, currency revaluation, expenses, UI polish, and cleanup

- Update BAS account catalog with comprehensive SRU codes and K2 flags
- Add currency revaluation service with tests and API route
- Add expenses page and account deletion API
- Enhance booking templates with new patterns and improved tests
- Improve transaction categorization with template picker and description matching
- Polish dashboard, onboarding, import, and transaction UIs
- Refactor year-end service for multi-step closing
- Move SRU generator to ne-bilaga, remove standalone SRU export
- Remove unused dev docs, mock data, and extension hooks
- Add invoice delivery note sequences migration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-03-04 14:19:56 +01:00
co-authored by Claude Opus 4.6
parent a9bf24ce0b
commit 66a4027f1e
104 changed files with 4964 additions and 5501 deletions
+1
View File
@@ -102,6 +102,7 @@ export const JournalEntrySourceTypeSchema = z.enum([
'supplier_invoice_paid',
'supplier_invoice_cash_payment',
'supplier_credit_note',
'currency_revaluation',
])
export const AccountTypeSchema = z.enum([
@@ -9,6 +9,9 @@ import {
searchTemplates,
findMatchingTemplates,
buildMappingResultFromTemplate,
getCommonTemplates,
getAdvancedTemplates,
validateTemplateForEntity,
type BookingTemplate,
} from '../booking-templates'
@@ -17,8 +20,8 @@ import {
// ============================================================
describe('BOOKING_TEMPLATES data integrity', () => {
it('has exactly 48 templates', () => {
expect(BOOKING_TEMPLATES).toHaveLength(48)
it('has exactly 51 templates', () => {
expect(BOOKING_TEMPLATES).toHaveLength(51)
})
it('all template IDs are unique', () => {
@@ -46,6 +49,7 @@ describe('BOOKING_TEMPLATES data integrity', () => {
expect(typeof t.default_private).toBe('boolean')
expect(t.fallback_category).toBeTruthy()
expect(t.description_sv).toBeTruthy()
expect(typeof t.common).toBe('boolean')
expect(Array.isArray(t.mcc_codes)).toBe(true)
expect(Array.isArray(t.keywords)).toBe(true)
expect(t.keywords.length).toBeGreaterThan(0)
@@ -136,7 +140,7 @@ describe('getTemplateGroups', () => {
it('every template is in exactly one group', () => {
const groups = getTemplateGroups()
const allTemplates = groups.flatMap((g) => g.templates)
expect(allTemplates).toHaveLength(48)
expect(allTemplates).toHaveLength(51)
})
})
@@ -175,8 +179,8 @@ describe('searchTemplates', () => {
})
it('supports multi-token search', () => {
const results = searchTemplates('annonsering marknadsföring')
expect(results.some((t) => t.id === 'marketing_online_ads')).toBe(true)
const results = searchTemplates('annonsering EU')
expect(results.some((t) => t.id === 'marketing_online_ads_eu')).toBe(true)
})
})
@@ -205,7 +209,7 @@ describe('findMatchingTemplates', () => {
merchant_name: 'Google',
})
const matches = findMatchingTemplates(tx)
expect(matches.some((m) => m.template.id === 'marketing_online_ads')).toBe(true)
expect(matches.some((m) => m.template.id === 'marketing_online_ads_eu')).toBe(true)
})
it('returns empty for a transaction with no signals', () => {
@@ -404,3 +408,141 @@ describe('buildMappingResultFromTemplate', () => {
expect(result.description).toBe('Drivmedel & Laddning: OKQ8 tankstation')
})
})
// ============================================================
// Template Curation Helpers
// ============================================================
describe('getCommonTemplates', () => {
it('returns only templates with common: true', () => {
const common = getCommonTemplates()
expect(common.length).toBeGreaterThan(0)
for (const t of common) {
expect(t.common).toBe(true)
}
})
it('filters by entity type', () => {
const efCommon = getCommonTemplates('enskild_firma')
for (const t of efCommon) {
expect(t.entity_applicability).not.toBe('aktiebolag')
}
})
it('filters by direction', () => {
const expenses = getCommonTemplates(undefined, 'expense')
for (const t of expenses) {
expect(t.direction).toBe('expense')
}
})
})
describe('getAdvancedTemplates', () => {
it('returns only templates with common: false', () => {
const advanced = getAdvancedTemplates()
expect(advanced.length).toBeGreaterThan(0)
for (const t of advanced) {
expect(t.common).toBe(false)
}
})
it('common + advanced = all templates (for a given entity/direction)', () => {
const common = getCommonTemplates()
const advanced = getAdvancedTemplates()
expect(common.length + advanced.length).toBe(BOOKING_TEMPLATES.length)
})
})
describe('validateTemplateForEntity', () => {
it('accepts template with entity_applicability "all"', () => {
const template = getTemplateById('premises_rent')!
const result = validateTemplateForEntity(template, 'aktiebolag')
expect(result.valid).toBe(true)
})
it('accepts EF template for EF entity', () => {
const template = getTemplateById('private_withdrawal_ef')!
const result = validateTemplateForEntity(template, 'enskild_firma')
expect(result.valid).toBe(true)
})
it('rejects EF template for AB entity', () => {
const template = getTemplateById('private_withdrawal_ef')!
const result = validateTemplateForEntity(template, 'aktiebolag')
expect(result.valid).toBe(false)
expect(result.error).toContain('enskild_firma')
})
it('rejects AB template for EF entity', () => {
const template = getTemplateById('personnel_salary')!
const result = validateTemplateForEntity(template, 'enskild_firma')
expect(result.valid).toBe(false)
expect(result.error).toContain('aktiebolag')
})
})
// ============================================================
// New/Split Templates
// ============================================================
describe('new and split templates', () => {
it('has marketing_online_ads_eu with reverse_charge', () => {
const t = getTemplateById('marketing_online_ads_eu')
expect(t).toBeDefined()
expect(t!.vat_treatment).toBe('reverse_charge')
expect(t!.common).toBe(true)
expect(t!.requires_vat_registration_data).toBe(true)
})
it('has marketing_online_ads_domestic with standard_25', () => {
const t = getTemplateById('marketing_online_ads_domestic')
expect(t).toBeDefined()
expect(t!.vat_treatment).toBe('standard_25')
expect(t!.common).toBe(false)
})
it('has representation_internal with account 7622', () => {
const t = getTemplateById('representation_internal')
expect(t).toBeDefined()
expect(t!.debit_account).toBe('7622')
expect(t!.vat_treatment).toBeNull()
expect(t!.common).toBe(true)
})
it('has shareholder_loan_received (AB, D:1930 K:2393)', () => {
const t = getTemplateById('shareholder_loan_received')
expect(t).toBeDefined()
expect(t!.debit_account).toBe('1930')
expect(t!.credit_account).toBe('2393')
expect(t!.entity_applicability).toBe('aktiebolag')
expect(t!.common).toBe(true)
})
it('has shareholder_loan_disbursed (AB, D:1680 K:1930)', () => {
const t = getTemplateById('shareholder_loan_disbursed')
expect(t).toBeDefined()
expect(t!.debit_account).toBe('1680')
expect(t!.credit_account).toBe('1930')
expect(t!.entity_applicability).toBe('aktiebolag')
expect(t!.common).toBe(false)
})
it('personnel_employer_tax uses debit account 2731 (liability clearing)', () => {
const t = getTemplateById('personnel_employer_tax')
expect(t).toBeDefined()
expect(t!.debit_account).toBe('2731')
})
it('personnel_salary has special_rules_sv warning', () => {
const t = getTemplateById('personnel_salary')
expect(t).toBeDefined()
expect(t!.special_rules_sv).toContain('nettolön')
expect(t!.requires_review).toBe(true)
})
it('representation_external has updated deductibility note with VAT cap', () => {
const t = getTemplateById('representation_external')
expect(t).toBeDefined()
expect(t!.deductibility_note_sv).toContain('46 kr/person')
})
})
@@ -0,0 +1,666 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import type { CreateJournalEntryInput, Currency } from '@/types'
import { makeInvoice, makeSupplierInvoice } from '@/tests/helpers'
// Mock riksbanken
vi.mock('@/lib/currency/riksbanken', () => ({
fetchMultipleRates: vi.fn(),
}))
// Mock engine
vi.mock('../engine', () => ({
createJournalEntry: vi.fn().mockImplementation(
async (_supabase: unknown, _userId: string, input: CreateJournalEntryInput) => ({
id: 'entry-1',
...input,
lines: input.lines,
status: 'posted',
voucher_number: 1,
voucher_series: 'A',
user_id: _userId,
committed_at: '2024-12-31T00:00:00Z',
reversed_by_id: null,
reverses_id: null,
correction_of_id: null,
attachment_urls: null,
created_at: '2024-12-31T00:00:00Z',
updated_at: '2024-12-31T00:00:00Z',
})
),
}))
// Mock supabase server
vi.mock('@/lib/supabase/server', () => ({
createClient: vi.fn(),
}))
const { fetchMultipleRates } = await import('@/lib/currency/riksbanken')
const mockedFetchRates = vi.mocked(fetchMultipleRates)
const { createJournalEntry } = await import('../engine')
const mockedCreateEntry = vi.mocked(createJournalEntry)
const {
getOpenForeignCurrencyReceivables,
getOpenForeignCurrencyPayables,
previewCurrencyRevaluation,
executeCurrencyRevaluation,
} = await import('../currency-revaluation')
// Helper to build mock supabase
function createMockSupabase(config: {
invoices?: ReturnType<typeof makeInvoice>[]
supplierInvoices?: ReturnType<typeof makeSupplierInvoice>[]
existingRevaluation?: boolean
}) {
const fromMap: Record<string, unknown[]> = {
invoices: config.invoices || [],
supplier_invoices: config.supplierInvoices || [],
}
const supabase = {
from: vi.fn().mockImplementation((table: string) => {
if (table === 'journal_entries') {
// For idempotency check
return {
select: vi.fn().mockReturnValue({
eq: vi.fn().mockReturnThis(),
then: undefined,
count: undefined,
// Build chain that resolves with count
...((() => {
const chain: Record<string, unknown> = {}
chain.eq = vi.fn().mockReturnValue(chain)
chain.select = vi.fn().mockReturnValue(chain)
// Terminal — return count
Object.defineProperty(chain, 'then', {
value: (resolve: (val: unknown) => void) => {
resolve({
count: config.existingRevaluation ? 1 : 0,
error: null,
})
},
})
return chain
})()),
}),
}
}
const data = fromMap[table] || []
const chain = buildFilterChain(data)
return chain
}),
}
return supabase
}
function buildFilterChain(data: unknown[]) {
let filtered = [...data]
const chain: Record<string, unknown> = {}
chain.select = vi.fn().mockImplementation(() => {
return chain
})
chain.eq = vi.fn().mockImplementation((col: string, val: unknown) => {
filtered = filtered.filter((row: Record<string, unknown>) => row[col] === val)
return chain
})
chain.neq = vi.fn().mockImplementation((col: string, val: unknown) => {
filtered = filtered.filter((row: Record<string, unknown>) => row[col] !== val)
return chain
})
chain.in = vi.fn().mockImplementation((col: string, vals: unknown[]) => {
filtered = filtered.filter((row: Record<string, unknown>) => vals.includes(row[col]))
return chain
})
chain.not = vi.fn().mockImplementation((col: string, op: string, _val: unknown) => {
if (op === 'is') {
filtered = filtered.filter((row: Record<string, unknown>) => row[col] != null)
}
return chain
})
// Make it thenable for await
chain.then = (resolve: (val: unknown) => void) => {
resolve({ data: filtered, error: null })
}
return chain
}
// Better mock for supabase that supports journal_entries idempotency check
function createFullMockSupabase(config: {
invoices?: ReturnType<typeof makeInvoice>[]
supplierInvoices?: ReturnType<typeof makeSupplierInvoice>[]
existingRevaluation?: boolean
}) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const supabase: any = {
from: vi.fn().mockImplementation((table: string) => {
if (table === 'journal_entries') {
const countResult = {
count: config.existingRevaluation ? 1 : 0,
error: null,
}
const journalChain: Record<string, unknown> = {}
journalChain.select = vi.fn().mockReturnValue(journalChain)
journalChain.eq = vi.fn().mockReturnValue(journalChain)
journalChain.then = (resolve: (val: unknown) => void) => {
resolve(countResult)
}
return journalChain
}
const fromMap: Record<string, unknown[]> = {
invoices: config.invoices || [],
supplier_invoices: config.supplierInvoices || [],
}
return buildFilterChain(fromMap[table] || [])
}),
}
return supabase
}
describe('currency-revaluation', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('getOpenForeignCurrencyReceivables', () => {
it('returns non-SEK invoices with sent/overdue status', async () => {
const eurInvoice = makeInvoice({
status: 'sent',
currency: 'EUR',
exchange_rate: 11.5,
total: 1000,
})
const sekInvoice = makeInvoice({
status: 'sent',
currency: 'SEK',
total: 5000,
})
const supabase = createMockSupabase({ invoices: [eurInvoice, sekInvoice] })
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await getOpenForeignCurrencyReceivables(supabase as any, 'user-1')
expect(result).toHaveLength(1)
expect(result[0].currency).toBe('EUR')
})
it('excludes paid invoices', async () => {
const paidEurInvoice = makeInvoice({
status: 'paid',
currency: 'EUR',
exchange_rate: 11.5,
total: 1000,
})
const supabase = createMockSupabase({ invoices: [paidEurInvoice] })
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await getOpenForeignCurrencyReceivables(supabase as any, 'user-1')
expect(result).toHaveLength(0)
})
it('excludes invoices without exchange_rate', async () => {
const noRateInvoice = makeInvoice({
status: 'sent',
currency: 'EUR',
exchange_rate: null,
total: 1000,
})
const supabase = createMockSupabase({ invoices: [noRateInvoice] })
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await getOpenForeignCurrencyReceivables(supabase as any, 'user-1')
expect(result).toHaveLength(0)
})
})
describe('getOpenForeignCurrencyPayables', () => {
it('returns non-SEK supplier invoices with open status', async () => {
const eurSI = makeSupplierInvoice({
status: 'registered',
currency: 'EUR',
exchange_rate: 11.5,
remaining_amount: 5000,
})
const sekSI = makeSupplierInvoice({
status: 'registered',
currency: 'SEK',
remaining_amount: 3000,
})
const supabase = createMockSupabase({ supplierInvoices: [eurSI, sekSI] })
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await getOpenForeignCurrencyPayables(supabase as any, 'user-1')
expect(result).toHaveLength(1)
expect(result[0].currency).toBe('EUR')
})
it('includes partially_paid supplier invoices', async () => {
const partialSI = makeSupplierInvoice({
status: 'partially_paid',
currency: 'USD',
exchange_rate: 10.5,
remaining_amount: 2000,
})
const supabase = createMockSupabase({ supplierInvoices: [partialSI] })
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await getOpenForeignCurrencyPayables(supabase as any, 'user-1')
expect(result).toHaveLength(1)
expect(result[0].remaining_amount).toBe(2000)
})
it('excludes paid supplier invoices', async () => {
const paidSI = makeSupplierInvoice({
status: 'paid',
currency: 'EUR',
exchange_rate: 11.5,
})
const supabase = createMockSupabase({ supplierInvoices: [paidSI] })
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await getOpenForeignCurrencyPayables(supabase as any, 'user-1')
expect(result).toHaveLength(0)
})
})
describe('previewCurrencyRevaluation', () => {
function mockRates(rates: Record<Currency, number>) {
mockedFetchRates.mockResolvedValue(
new Map(
Object.entries(rates).map(([currency, rate]) => [
currency as Currency,
{ currency: currency as Currency, rate, date: '2024-12-31' },
])
)
)
}
it('returns empty preview when no foreign currency items', async () => {
const supabase = createMockSupabase({
invoices: [],
supplierInvoices: [],
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const preview = await previewCurrencyRevaluation(supabase as any, 'user-1', '2024-12-31')
expect(preview.items).toHaveLength(0)
expect(preview.lines).toHaveLength(0)
expect(preview.netEffect).toBe(0)
})
it('computes receivable gain (closing rate > original rate)', async () => {
const eurInvoice = makeInvoice({
id: 'inv-1',
status: 'sent',
currency: 'EUR',
exchange_rate: 11.0,
total: 1000,
invoice_number: 'F-001',
})
mockRates({ EUR: 11.5 } as Record<Currency, number>)
const supabase = createMockSupabase({ invoices: [eurInvoice] })
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const preview = await previewCurrencyRevaluation(supabase as any, 'user-1', '2024-12-31')
expect(preview.items).toHaveLength(1)
expect(preview.items[0].type).toBe('receivable')
expect(preview.items[0].difference_sek).toBe(500) // 1000 * (11.5 - 11.0)
// Should debit 1510 (receivable up), credit 3960 (gain)
const debit1510 = preview.lines.find(l => l.account_number === '1510' && l.debit_amount > 0)
const credit3960 = preview.lines.find(l => l.account_number === '3960' && l.credit_amount > 0)
expect(debit1510).toBeDefined()
expect(debit1510!.debit_amount).toBe(500)
expect(credit3960).toBeDefined()
expect(credit3960!.credit_amount).toBe(500)
expect(preview.totalGain).toBe(500)
expect(preview.totalLoss).toBe(0)
expect(preview.netEffect).toBe(500)
})
it('computes receivable loss (closing rate < original rate)', async () => {
const eurInvoice = makeInvoice({
id: 'inv-2',
status: 'overdue',
currency: 'EUR',
exchange_rate: 12.0,
total: 1000,
invoice_number: 'F-002',
})
mockRates({ EUR: 11.5 } as Record<Currency, number>)
const supabase = createMockSupabase({ invoices: [eurInvoice] })
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const preview = await previewCurrencyRevaluation(supabase as any, 'user-1', '2024-12-31')
expect(preview.items[0].difference_sek).toBe(-500) // 1000 * (11.5 - 12.0)
// Should credit 1510 (receivable down), debit 7960 (loss)
const credit1510 = preview.lines.find(l => l.account_number === '1510' && l.credit_amount > 0)
const debit7960 = preview.lines.find(l => l.account_number === '7960' && l.debit_amount > 0)
expect(credit1510).toBeDefined()
expect(credit1510!.credit_amount).toBe(500)
expect(debit7960).toBeDefined()
expect(debit7960!.debit_amount).toBe(500)
expect(preview.totalLoss).toBe(500)
expect(preview.totalGain).toBe(0)
expect(preview.netEffect).toBe(-500)
})
it('computes payable loss (closing rate > original rate — liability grew)', async () => {
const eurSI = makeSupplierInvoice({
id: 'si-1',
status: 'registered',
currency: 'EUR',
exchange_rate: 11.0,
remaining_amount: 2000,
supplier_invoice_number: 'LF-001',
})
mockRates({ EUR: 11.5 } as Record<Currency, number>)
const supabase = createMockSupabase({ supplierInvoices: [eurSI] })
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const preview = await previewCurrencyRevaluation(supabase as any, 'user-1', '2024-12-31')
expect(preview.items[0].type).toBe('payable')
expect(preview.items[0].difference_sek).toBe(1000) // 2000 * (11.5 - 11.0)
// Should debit 7960 (loss), credit 2440 (liability up)
const debit7960 = preview.lines.find(l => l.account_number === '7960' && l.debit_amount > 0)
const credit2440 = preview.lines.find(l => l.account_number === '2440' && l.credit_amount > 0)
expect(debit7960).toBeDefined()
expect(debit7960!.debit_amount).toBe(1000)
expect(credit2440).toBeDefined()
expect(credit2440!.credit_amount).toBe(1000)
})
it('computes payable gain (closing rate < original rate — liability shrank)', async () => {
const eurSI = makeSupplierInvoice({
id: 'si-2',
status: 'approved',
currency: 'EUR',
exchange_rate: 12.0,
remaining_amount: 2000,
supplier_invoice_number: 'LF-002',
})
mockRates({ EUR: 11.5 } as Record<Currency, number>)
const supabase = createMockSupabase({ supplierInvoices: [eurSI] })
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const preview = await previewCurrencyRevaluation(supabase as any, 'user-1', '2024-12-31')
expect(preview.items[0].difference_sek).toBe(-1000) // 2000 * (11.5 - 12.0)
// Should debit 2440 (liability down), credit 3960 (gain)
const debit2440 = preview.lines.find(l => l.account_number === '2440' && l.debit_amount > 0)
const credit3960 = preview.lines.find(l => l.account_number === '3960' && l.credit_amount > 0)
expect(debit2440).toBeDefined()
expect(debit2440!.debit_amount).toBe(1000)
expect(credit3960).toBeDefined()
expect(credit3960!.credit_amount).toBe(1000)
})
it('handles mixed currencies correctly', async () => {
const eurInvoice = makeInvoice({
id: 'inv-eur',
status: 'sent',
currency: 'EUR',
exchange_rate: 11.0,
total: 1000,
invoice_number: 'F-EUR',
})
const usdInvoice = makeInvoice({
id: 'inv-usd',
status: 'sent',
currency: 'USD',
exchange_rate: 10.0,
total: 500,
invoice_number: 'F-USD',
})
mockRates({ EUR: 11.5, USD: 10.5 } as Record<Currency, number>)
const supabase = createMockSupabase({ invoices: [eurInvoice, usdInvoice] })
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const preview = await previewCurrencyRevaluation(supabase as any, 'user-1', '2024-12-31')
expect(preview.items).toHaveLength(2)
// EUR: 1000 * (11.5 - 11.0) = 500
// USD: 500 * (10.5 - 10.0) = 250
expect(preview.totalGain).toBe(750)
})
it('aggregates journal lines correctly with mixed gains and losses', async () => {
const gainInvoice = makeInvoice({
id: 'inv-gain',
status: 'sent',
currency: 'EUR',
exchange_rate: 11.0,
total: 1000,
invoice_number: 'F-GAIN',
})
const lossSI = makeSupplierInvoice({
id: 'si-loss',
status: 'registered',
currency: 'EUR',
exchange_rate: 11.0,
remaining_amount: 2000,
supplier_invoice_number: 'LF-LOSS',
})
// EUR went up to 11.5
mockRates({ EUR: 11.5 } as Record<Currency, number>)
const supabase = createMockSupabase({
invoices: [gainInvoice],
supplierInvoices: [lossSI],
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const preview = await previewCurrencyRevaluation(supabase as any, 'user-1', '2024-12-31')
// Receivable gain: 1000 * 0.5 = 500 → Debit 1510, Credit 3960
// Payable loss: 2000 * 0.5 = 1000 → Debit 7960, Credit 2440
expect(preview.totalGain).toBe(500)
expect(preview.totalLoss).toBe(1000)
expect(preview.netEffect).toBe(-500)
// Verify all entries balance
const totalDebit = preview.lines.reduce((sum, l) => sum + l.debit_amount, 0)
const totalCredit = preview.lines.reduce((sum, l) => sum + l.credit_amount, 0)
expect(Math.round(totalDebit * 100) / 100).toBe(Math.round(totalCredit * 100) / 100)
})
it('uses remaining_amount for partially paid supplier invoices', async () => {
const partialSI = makeSupplierInvoice({
id: 'si-partial',
status: 'partially_paid',
currency: 'EUR',
exchange_rate: 11.0,
total: 10000,
remaining_amount: 5000, // Half paid
supplier_invoice_number: 'LF-PARTIAL',
})
mockRates({ EUR: 11.5 } as Record<Currency, number>)
const supabase = createMockSupabase({ supplierInvoices: [partialSI] })
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const preview = await previewCurrencyRevaluation(supabase as any, 'user-1', '2024-12-31')
// Only remaining 5000 EUR is revalued, not full 10000
expect(preview.items[0].amount_in_currency).toBe(5000)
expect(preview.items[0].difference_sek).toBe(2500) // 5000 * (11.5 - 11.0)
})
it('skips items with zero difference', async () => {
const eurInvoice = makeInvoice({
id: 'inv-same',
status: 'sent',
currency: 'EUR',
exchange_rate: 11.5,
total: 1000,
invoice_number: 'F-SAME',
})
// Closing rate equals original rate
mockRates({ EUR: 11.5 } as Record<Currency, number>)
const supabase = createMockSupabase({ invoices: [eurInvoice] })
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const preview = await previewCurrencyRevaluation(supabase as any, 'user-1', '2024-12-31')
expect(preview.items).toHaveLength(0)
expect(preview.lines).toHaveLength(0)
})
it('all generated journal lines balance (debits === credits)', async () => {
const eurInvoice = makeInvoice({
id: 'inv-bal',
status: 'sent',
currency: 'EUR',
exchange_rate: 11.0,
total: 1234.56,
invoice_number: 'F-BAL',
})
const gbpSI = makeSupplierInvoice({
id: 'si-bal',
status: 'overdue',
currency: 'GBP',
exchange_rate: 14.0,
remaining_amount: 789.12,
supplier_invoice_number: 'LF-BAL',
})
mockRates({ EUR: 11.8, GBP: 13.5 } as Record<Currency, number>)
const supabase = createMockSupabase({
invoices: [eurInvoice],
supplierInvoices: [gbpSI],
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const preview = await previewCurrencyRevaluation(supabase as any, 'user-1', '2024-12-31')
const totalDebit = preview.lines.reduce((sum, l) => sum + l.debit_amount, 0)
const totalCredit = preview.lines.reduce((sum, l) => sum + l.credit_amount, 0)
expect(Math.round(totalDebit * 100)).toBe(Math.round(totalCredit * 100))
})
})
describe('executeCurrencyRevaluation', () => {
function mockRates(rates: Record<Currency, number>) {
mockedFetchRates.mockResolvedValue(
new Map(
Object.entries(rates).map(([currency, rate]) => [
currency as Currency,
{ currency: currency as Currency, rate, date: '2024-12-31' },
])
)
)
}
it('returns null when no foreign currency items exist', async () => {
const supabase = createFullMockSupabase({
invoices: [],
supplierInvoices: [],
existingRevaluation: false,
})
mockRates({} as Record<Currency, number>)
const result = await executeCurrencyRevaluation(supabase, 'user-1', '2024-12-31', 'period-1')
expect(result).toBeNull()
expect(mockedCreateEntry).not.toHaveBeenCalled()
})
it('creates journal entry with correct source_type', async () => {
const eurInvoice = makeInvoice({
status: 'sent',
currency: 'EUR',
exchange_rate: 11.0,
total: 1000,
invoice_number: 'F-001',
})
mockRates({ EUR: 11.5 } as Record<Currency, number>)
const supabase = createFullMockSupabase({
invoices: [eurInvoice],
existingRevaluation: false,
})
const result = await executeCurrencyRevaluation(supabase, 'user-1', '2024-12-31', 'period-1')
expect(result).not.toBeNull()
expect(mockedCreateEntry).toHaveBeenCalledOnce()
const callArgs = mockedCreateEntry.mock.calls[0]
expect(callArgs[2].source_type).toBe('currency_revaluation')
expect(callArgs[2].fiscal_period_id).toBe('period-1')
expect(callArgs[2].entry_date).toBe('2024-12-31')
expect(callArgs[2].description).toContain('Omvärdering utländsk valuta')
})
it('throws when revaluation already exists for period (idempotency)', async () => {
const supabase = createFullMockSupabase({
existingRevaluation: true,
})
await expect(
executeCurrencyRevaluation(supabase, 'user-1', '2024-12-31', 'period-1')
).rejects.toThrow('Currency revaluation already exists for this period')
expect(mockedCreateEntry).not.toHaveBeenCalled()
})
it('returns entry and preview in result', async () => {
const eurInvoice = makeInvoice({
status: 'sent',
currency: 'EUR',
exchange_rate: 11.0,
total: 1000,
invoice_number: 'F-001',
})
mockRates({ EUR: 12.0 } as Record<Currency, number>)
const supabase = createFullMockSupabase({
invoices: [eurInvoice],
existingRevaluation: false,
})
const result = await executeCurrencyRevaluation(supabase, 'user-1', '2024-12-31', 'period-1')
expect(result).not.toBeNull()
expect(result!.entry).toBeDefined()
expect(result!.preview).toBeDefined()
expect(result!.preview.items).toHaveLength(1)
expect(result!.preview.totalGain).toBe(1000) // 1000 * (12 - 11)
})
})
})
+26 -26
View File
@@ -8,7 +8,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '10',
account_type: 'asset',
normal_balance: 'debit',
description: 'Aktiverade utgifter for utvecklingsarbete, t.ex. mjukvaruutveckling eller produktutveckling.',
description: 'Aktiverade utgifter för utvecklingsarbete, t.ex. mjukvaruutveckling eller produktutveckling.',
sru_code: '7201',
k2_excluded: true,
},
@@ -96,7 +96,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '10',
account_type: 'asset',
normal_balance: 'debit',
description: 'Aktiverade kostnader for patent och liknande rattigheter.',
description: 'Aktiverade kostnader för patent och liknande rättigheter.',
sru_code: '7201',
k2_excluded: false,
},
@@ -162,7 +162,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '10',
account_type: 'asset',
normal_balance: 'debit',
description: 'Goodwill som uppkommer vid forvrav av rorelse eller inkramsforvrav.',
description: 'Goodwill som uppkommer vid forvrav av rörelse eller inkråmsförvärv.',
sru_code: '7201',
k2_excluded: false,
},
@@ -338,7 +338,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '11',
account_type: 'asset',
normal_balance: 'debit',
description: 'Anskaffningsvarde for byggnader som ags av foretaget.',
description: 'Anskaffningsvärde för byggnader som ägs av företaget.',
sru_code: '7202',
k2_excluded: false,
},
@@ -382,7 +382,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '11',
account_type: 'asset',
normal_balance: 'credit',
description: 'Ackumulerad vardeminskning pa byggnader sedan anskaffningstidpunkten.',
description: 'Ackumulerad värdeminskning på byggnader sedan anskaffningstidpunkten.',
sru_code: '7202',
k2_excluded: false,
},
@@ -415,7 +415,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '11',
account_type: 'asset',
normal_balance: 'debit',
description: 'Anskaffningsvarde for mark som ags av foretaget. Mark skrivs inte av.',
description: 'Anskaffningsvärde för mark som ägs av företaget. Mark skrivs inte av.',
sru_code: '7202',
k2_excluded: false,
},
@@ -437,7 +437,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '11',
account_type: 'asset',
normal_balance: 'debit',
description: 'Aktiverade utgifter for markanlaggningar som parkering, dranering och brunnar.',
description: 'Aktiverade utgifter för markanläggningar som parkering, dränering och brunnar.',
sru_code: '7202',
k2_excluded: false,
},
@@ -668,7 +668,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '12',
account_type: 'asset',
normal_balance: 'credit',
description: 'Ackumulerad vardeminskning pa inventarier och verktyg sedan anskaffning.',
description: 'Ackumulerad värdeminskning på inventarier och verktyg sedan anskaffning.',
sru_code: '7202',
k2_excluded: false,
},
@@ -756,7 +756,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '12',
account_type: 'asset',
normal_balance: 'debit',
description: 'Materiella anlaggningstillgangar som inte passar i ovriga underkategorier.',
description: 'Materiella anläggningstillgångar som inte passar i övriga underkategorier.',
sru_code: '7202',
k2_excluded: false,
},
@@ -811,7 +811,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '13',
account_type: 'asset',
normal_balance: 'debit',
description: 'Aktier och andelar i dotterbolag och koncernforetag.',
description: 'Aktier och andelar i dotterbolag och koncernföretag.',
sru_code: '7203',
k2_excluded: false,
},
@@ -1229,7 +1229,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '13',
account_type: 'asset',
normal_balance: 'debit',
description: 'Langfristiga fordringar som inte faller under andra kategorier, t.ex. deposition.',
description: 'Långfristiga fordringar som inte faller under andra kategorier, t.ex. deposition.',
sru_code: '7203',
k2_excluded: false,
},
@@ -1328,7 +1328,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '14',
account_type: 'asset',
normal_balance: 'debit',
description: 'Ravaror och material som anvands i produktion men inte ar fardiga produkter.',
description: 'Ravaror och material som används i produktion men inte är färdiga produkter.',
sru_code: '7210',
k2_excluded: false,
},
@@ -1471,7 +1471,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '14',
account_type: 'asset',
normal_balance: 'debit',
description: 'Halvfabrikat och arbeten under tillverkning som annu inte slutforts.',
description: 'Halvfabrikat och arbeten under tillverkning som ännu inte slutförts.',
sru_code: '7210',
k2_excluded: false,
},
@@ -1592,7 +1592,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '15',
account_type: 'asset',
normal_balance: 'debit',
description: 'Pengar som kunder ar skyldiga foretaget for skickade fakturor som inte betalats annu.',
description: 'Pengar som kunder är skyldiga företaget för skickade fakturor som inte betalats ännu.',
sru_code: '7211',
k2_excluded: false,
},
@@ -1658,7 +1658,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '15',
account_type: 'asset',
normal_balance: 'credit',
description: 'Vardering av befarade kundforluster, minskar kundfordringsbalansen.',
description: 'Värdering av befarade kundförluster, minskar kundfordringsbalansen.',
sru_code: '7211',
k2_excluded: false,
},
@@ -1878,7 +1878,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '16',
account_type: 'asset',
normal_balance: 'debit',
description: 'Utlagg eller forskott till anstallda som ska aterbetalas.',
description: 'Utlägg eller förskott till anställda som ska återbetalas.',
sru_code: '7212',
k2_excluded: false,
},
@@ -1955,7 +1955,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '16',
account_type: 'asset',
normal_balance: 'debit',
description: 'Foretagets skattekonto hos Skatteverket. Visar saldo for inbetalda skatter och avgifter.',
description: 'Företagets skattekonto hos Skatteverket. Visar saldo för inbetalda skatter och avgifter.',
sru_code: '7212',
k2_excluded: false,
},
@@ -1977,7 +1977,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '16',
account_type: 'asset',
normal_balance: 'debit',
description: 'Fordran pa Skatteverket nar ingaende moms overstiger utgaende moms.',
description: 'Fordran på Skatteverket nar ingående moms överstiger utgående moms.',
sru_code: '7212',
k2_excluded: false,
},
@@ -2197,7 +2197,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '17',
account_type: 'asset',
normal_balance: 'debit',
description: 'Hyra som betalats i forskott men avser kommande perioder.',
description: 'Hyra som betalats i förskott men avser kommande perioder.',
sru_code: '7212',
k2_excluded: false,
},
@@ -2208,7 +2208,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '17',
account_type: 'asset',
normal_balance: 'debit',
description: 'Leasingavgifter betalade i forskott som avser framtida perioder.',
description: 'Leasingavgifter betalade i förskott som avser framtida perioder.',
sru_code: '7212',
k2_excluded: false,
},
@@ -2285,7 +2285,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '17',
account_type: 'asset',
normal_balance: 'debit',
description: 'Forutbetalda kostnader och upplupna intakter som inte ryms i andra underkonton.',
description: 'Förutbetalda kostnader och upplupna intäkter som inte ryms i andra underkonton.',
sru_code: '7212',
k2_excluded: false,
},
@@ -2296,7 +2296,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '18',
account_type: 'asset',
normal_balance: 'debit',
description: 'Kortfristiga aktieinnehav i borsnoterade foretag avsedda att saljas inom 12 manader.',
description: 'Kortfristiga aktieinnehav i börsnoterade företag avsedda att säljas inom 12 månader.',
sru_code: '7212',
k2_excluded: false,
},
@@ -2384,7 +2384,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '19',
account_type: 'asset',
normal_balance: 'debit',
description: 'Kontanta pengar i foretagets kassa.',
description: 'Kontanta pengar i företagets kassa.',
sru_code: '7212',
k2_excluded: false,
},
@@ -2428,7 +2428,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '19',
account_type: 'asset',
normal_balance: 'debit',
description: 'Pengar pa foretagets PlusGiro-konto.',
description: 'Pengar på företagets PlusGiro-konto.',
sru_code: '7212',
k2_excluded: false,
},
@@ -2439,7 +2439,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '19',
account_type: 'asset',
normal_balance: 'debit',
description: 'Foretagets huvudsakliga bankkonto for dagliga in- och utbetalningar.',
description: 'Företagets huvudsakliga bankkonto för dagliga in- och utbetalningar.',
sru_code: '7212',
k2_excluded: false,
},
@@ -2450,7 +2450,7 @@ export const CLASS_1_ACCOUNTS: BASReferenceAccount[] = [
account_group: '19',
account_type: 'asset',
normal_balance: 'debit',
description: 'Ytterligare bankkonton utover huvudkontot, t.ex. sparkonto.',
description: 'Ytterligare bankkonton utöver huvudkontot, t.ex. sparkonto.',
sru_code: '7212',
k2_excluded: false,
},
@@ -8,7 +8,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '20',
account_type: 'equity',
normal_balance: 'credit',
description: 'Agarens insatta kapital i enskild firma. Visar vad agaren har investerat.',
description: 'Ägarens insatta kapital i enskild firma. Visar vad ägaren har investerat.',
sru_code: '7221',
k2_excluded: false,
},
@@ -30,7 +30,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '20',
account_type: 'equity',
normal_balance: 'debit',
description: 'Pengar som agaren av en enskild firma tar ut privat ur foretaget.',
description: 'Pengar som ägaren av en enskild firma tar ut privat ur företaget.',
sru_code: '7221',
k2_excluded: false,
},
@@ -41,7 +41,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '20',
account_type: 'equity',
normal_balance: 'credit',
description: 'Tillskott fran agaren under lopande rakenskapsar i enskild firma.',
description: 'Tillskott från ägaren under löpande räkenskapsår i enskild firma.',
sru_code: '7221',
k2_excluded: false,
},
@@ -52,7 +52,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '20',
account_type: 'equity',
normal_balance: 'credit',
description: 'Pengar som agaren satter in privat i foretaget (enskild firma).',
description: 'Pengar som ägaren sätter in privat i företaget (enskild firma).',
sru_code: '7221',
k2_excluded: false,
},
@@ -63,7 +63,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '20',
account_type: 'equity',
normal_balance: 'credit',
description: 'Arets vinst eller forlust i enskild firma.',
description: 'Årets vinst eller förlust i enskild firma.',
sru_code: '7221',
k2_excluded: false,
},
@@ -459,7 +459,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '20',
account_type: 'equity',
normal_balance: 'credit',
description: 'Fond for uppskrivning av anlaggningstillgangar i aktiebolag.',
description: 'Fond för uppskrivning av anläggningstillgångar i aktiebolag.',
sru_code: '7221',
k2_excluded: false,
},
@@ -525,7 +525,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '20',
account_type: 'equity',
normal_balance: 'debit',
description: 'Ackumulerade vinster eller forluster fran tidigare ar som inte delats ut.',
description: 'Ackumulerade vinster eller förluster från tidigare är som inte delats ut.',
sru_code: '7221',
k2_excluded: false,
},
@@ -547,7 +547,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '20',
account_type: 'equity',
normal_balance: 'credit',
description: 'Tillskott fran aktieagare som inte ar lan, okar fritt eget kapital.',
description: 'Tillskott från aktieägare som inte är lån, ökar fritt eget kapital.',
sru_code: '7221',
k2_excluded: false,
},
@@ -613,7 +613,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '20',
account_type: 'equity',
normal_balance: 'credit',
description: 'Vinst eller forlust for innevarande rakenskapsar (aktiebolag).',
description: 'Vinst eller förlust för innevarande räkenskapsår (aktiebolag).',
sru_code: '7222',
k2_excluded: false,
},
@@ -767,7 +767,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '21',
account_type: 'untaxed_reserves',
normal_balance: 'credit',
description: 'Skattemassiga overavskrivningar pa inventarier utover plan (periodiseringsfond).',
description: 'Skattemässiga överavskrivningar på inventarier utöver plan (periodiseringsfond).',
sru_code: '7230',
k2_excluded: false,
},
@@ -976,7 +976,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '23',
account_type: 'liability',
normal_balance: 'credit',
description: 'Langfristiga lan fran banker och kreditinstitut.',
description: 'Långfristiga lån från banker och kreditinstitut.',
sru_code: '7230',
k2_excluded: false,
},
@@ -1064,7 +1064,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '23',
account_type: 'liability',
normal_balance: 'credit',
description: 'Langfristiga skulder utover banklan, t.ex. lan fran privatpersoner.',
description: 'Långfristiga skulder utöver banklån, t.ex. lån från privatpersoner.',
sru_code: '7230',
k2_excluded: false,
},
@@ -1295,7 +1295,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '24',
account_type: 'liability',
normal_balance: 'credit',
description: 'Banklan och checkkrediter med aterbetalningstid under 12 manader.',
description: 'Banklån och checkkrediter med återbetalningstid under 12 månader.',
sru_code: '7230',
k2_excluded: false,
},
@@ -1427,7 +1427,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '24',
account_type: 'liability',
normal_balance: 'credit',
description: 'Pengar som foretaget ar skyldigt leverantorer for mottagna fakturor.',
description: 'Pengar som företaget är skyldigt leverantörer för mottagna fakturor.',
sru_code: '7230',
k2_excluded: false,
},
@@ -1636,7 +1636,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '25',
account_type: 'liability',
normal_balance: 'credit',
description: 'Skulder till Skatteverket for preliminar skatt och andra skattebetalningar.',
description: 'Skulder till Skatteverket för preliminär skatt och andra skattebetalningar.',
sru_code: '7231',
k2_excluded: false,
},
@@ -1724,7 +1724,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '26',
account_type: 'liability',
normal_balance: 'credit',
description: 'Moms du tar ut pa forsaljning med 25% momssats. Ska betalas in till Skatteverket.',
description: 'Moms du tar ut på försäljning med 25% momssats. Ska betalas in till Skatteverket.',
sru_code: '7231',
k2_excluded: false,
},
@@ -1757,7 +1757,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '26',
account_type: 'liability',
normal_balance: 'credit',
description: 'Utgaende moms vid omvand skattskyldighet (reverse charge) med 25% momssats.',
description: 'Utgående moms vid omvänd skattskyldighet (reverse charge) med 25% momssats.',
sru_code: '7231',
k2_excluded: false,
},
@@ -1812,7 +1812,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '26',
account_type: 'liability',
normal_balance: 'credit',
description: 'Moms pa forsaljning med 12% momssats, t.ex. livsmedel och hotell.',
description: 'Moms på försäljning med 12% momssats, t.ex. livsmedel och hotell.',
sru_code: '7231',
k2_excluded: false,
},
@@ -1845,7 +1845,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '26',
account_type: 'liability',
normal_balance: 'credit',
description: 'Utgaende moms vid omvand skattskyldighet (reverse charge) med 12% momssats.',
description: 'Utgående moms vid omvänd skattskyldighet (reverse charge) med 12% momssats.',
sru_code: '7231',
k2_excluded: false,
},
@@ -1900,7 +1900,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '26',
account_type: 'liability',
normal_balance: 'credit',
description: 'Moms pa forsaljning med 6% momssats, t.ex. bocker och tidningar.',
description: 'Moms på försäljning med 6% momssats, t.ex. böcker och tidningar.',
sru_code: '7231',
k2_excluded: false,
},
@@ -1933,7 +1933,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '26',
account_type: 'liability',
normal_balance: 'credit',
description: 'Utgaende moms vid omvand skattskyldighet (reverse charge) med 6% momssats.',
description: 'Utgående moms vid omvänd skattskyldighet (reverse charge) med 6% momssats.',
sru_code: '7231',
k2_excluded: false,
},
@@ -1988,7 +1988,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '26',
account_type: 'liability',
normal_balance: 'credit',
description: 'Moms pa inkop som foretaget har ratt att dra av. Minskar momsskulden.',
description: 'Moms på inköp som företaget har ratt att dra av. Minskar momsskulden.',
sru_code: '7231',
k2_excluded: false,
},
@@ -2010,7 +2010,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '26',
account_type: 'liability',
normal_balance: 'credit',
description: 'Ingaende moms som beraknas sjalv vid inkop fran andra EU-lander (omvand skattskyldighet).',
description: 'Ingående moms som beräknas själv vid inköp från andra EU-länder (omvänd skattskyldighet).',
sru_code: '7231',
k2_excluded: false,
},
@@ -2098,7 +2098,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '27',
account_type: 'liability',
normal_balance: 'credit',
description: 'Innehallen preliminarskatt pa anstallda loner som ska betalas till Skatteverket.',
description: 'Innehållen preliminärskatt på anställda löner som ska betalas till Skatteverket.',
sru_code: '7231',
k2_excluded: false,
},
@@ -2120,7 +2120,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '27',
account_type: 'liability',
normal_balance: 'credit',
description: 'Arbetsgivaravgifter redovisade men annu inte inbetalda till Skatteverket.',
description: 'Arbetsgivaravgifter redovisade men ännu inte inbetalda till Skatteverket.',
sru_code: '7231',
k2_excluded: false,
},
@@ -2307,7 +2307,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '28',
account_type: 'liability',
normal_balance: 'credit',
description: 'Skulder till anstallda for t.ex. reseforskott eller utlagg.',
description: 'Skulder till anställda för t.ex. reseförskott eller utlägg.',
sru_code: '7231',
k2_excluded: false,
},
@@ -2560,7 +2560,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '28',
account_type: 'liability',
normal_balance: 'credit',
description: 'Pengar som aktiebolaget lanat av sina agare. Vanligt i mindre AB.',
description: 'Pengar som aktiebolaget lånat av sina ägare. Vanligt i mindre AB.',
sru_code: '7231',
k2_excluded: false,
},
@@ -2593,7 +2593,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '28',
account_type: 'liability',
normal_balance: 'credit',
description: 'Beslutad men annu ej utbetald aktieutdelning.',
description: 'Beslutad men ännu ej utbetald aktieutdelning.',
sru_code: '7231',
k2_excluded: false,
},
@@ -2615,7 +2615,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '29',
account_type: 'liability',
normal_balance: 'credit',
description: 'Loner som intjanats men annu inte utbetalats vid periodens slut.',
description: 'Löner som intjänats men ännu inte utbetalats vid periodens slut.',
sru_code: '7231',
k2_excluded: false,
},
@@ -2659,7 +2659,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '29',
account_type: 'liability',
normal_balance: 'credit',
description: 'Skuld for intjanade men inte uttagna semesterdagar.',
description: 'Skuld för intjänade men inte uttagna semesterdagar.',
sru_code: '7231',
k2_excluded: false,
},
@@ -2692,7 +2692,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '29',
account_type: 'liability',
normal_balance: 'credit',
description: 'Arbetsgivaravgifter som hanfor sig till redovisade loner men annu inte betalats.',
description: 'Arbetsgivaravgifter som hänför sig till redovisade löner men ännu inte betalats.',
sru_code: '7231',
k2_excluded: false,
},
@@ -2780,7 +2780,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '29',
account_type: 'liability',
normal_balance: 'credit',
description: 'Rantekostnader som upplupit men inte fakturerats eller betalats annu.',
description: 'Räntekostnader som upplupit men inte fakturerats eller betalats ännu.',
sru_code: '7231',
k2_excluded: false,
},
@@ -2846,7 +2846,7 @@ export const CLASS_2_ACCOUNTS: BASReferenceAccount[] = [
account_group: '29',
account_type: 'liability',
normal_balance: 'credit',
description: 'Upplupna kostnader och forutbetalda intakter som inte ryms i andra underkonton.',
description: 'Upplupna kostnader och förutbetalda intäkter som inte ryms i andra underkonton.',
sru_code: '7231',
k2_excluded: false,
},
+17 -17
View File
@@ -19,7 +19,7 @@ export const CLASS_3_ACCOUNTS: BASReferenceAccount[] = [
account_group: '30',
account_type: 'revenue',
normal_balance: 'credit',
description: 'Intakter fran forsaljning med 25% moms - den vanligaste intaktsraden for svenska foretag.',
description: 'Intäkter från försäljning med 25% moms - den vanligaste intäktsraden för svenska företag.',
sru_code: '7310',
k2_excluded: false,
},
@@ -30,7 +30,7 @@ export const CLASS_3_ACCOUNTS: BASReferenceAccount[] = [
account_group: '30',
account_type: 'revenue',
normal_balance: 'credit',
description: 'Intakter fran forsaljning med 12% moms, t.ex. livsmedel och restaurang.',
description: 'Intäkter från försäljning med 12% moms, t.ex. livsmedel och restaurang.',
sru_code: '7310',
k2_excluded: false,
},
@@ -41,7 +41,7 @@ export const CLASS_3_ACCOUNTS: BASReferenceAccount[] = [
account_group: '30',
account_type: 'revenue',
normal_balance: 'credit',
description: 'Intakter fran forsaljning med 6% moms, t.ex. bocker, tidningar och kollektivtrafik.',
description: 'Intäkter från försäljning med 6% moms, t.ex. böcker, tidningar och kollektivtrafik.',
sru_code: '7310',
k2_excluded: false,
},
@@ -52,7 +52,7 @@ export const CLASS_3_ACCOUNTS: BASReferenceAccount[] = [
account_group: '30',
account_type: 'revenue',
normal_balance: 'credit',
description: 'Intakter fran forsaljning som ar undantagen fran moms, t.ex. sjukvard och utbildning.',
description: 'Intäkter från försäljning som är undantagen från moms, t.ex. sjukvård och utbildning.',
sru_code: '7310',
k2_excluded: false,
},
@@ -63,7 +63,7 @@ export const CLASS_3_ACCOUNTS: BASReferenceAccount[] = [
account_group: '31',
account_type: 'revenue',
normal_balance: 'credit',
description: 'Forsaljning av varor utanfor Sverige, gruppkonto.',
description: 'Försäljning av varor utanfor Sverige, gruppkonto.',
sru_code: '7311',
k2_excluded: false,
},
@@ -74,7 +74,7 @@ export const CLASS_3_ACCOUNTS: BASReferenceAccount[] = [
account_group: '31',
account_type: 'revenue',
normal_balance: 'credit',
description: 'Intakter fran forsaljning av varor till kunder utanfor EU. Momsfritt.',
description: 'Intäkter från försäljning av varor till kunder utanfor EU. Momsfritt.',
sru_code: '7310',
k2_excluded: false,
},
@@ -96,7 +96,7 @@ export const CLASS_3_ACCOUNTS: BASReferenceAccount[] = [
account_group: '31',
account_type: 'revenue',
normal_balance: 'credit',
description: 'Intakter fran forsaljning av varor till momsregistrerade foretag i andra EU-lander.',
description: 'Intäkter från försäljning av varor till momsregistrerade företag i andra EU-länder.',
sru_code: '7310',
k2_excluded: false,
},
@@ -162,7 +162,7 @@ export const CLASS_3_ACCOUNTS: BASReferenceAccount[] = [
account_group: '33',
account_type: 'revenue',
normal_balance: 'credit',
description: 'Intakter fran forsaljning av tjanster till kunder utanfor EU. Momsfritt.',
description: 'Intäkter från försäljning av tjänster till kunder utanfor EU. Momsfritt.',
sru_code: '7310',
k2_excluded: false,
},
@@ -173,7 +173,7 @@ export const CLASS_3_ACCOUNTS: BASReferenceAccount[] = [
account_group: '33',
account_type: 'revenue',
normal_balance: 'credit',
description: 'Intakter fran forsaljning av tjanster till foretag i andra EU-lander. Omvand skattskyldighet.',
description: 'Intäkter från försäljning av tjänster till företag i andra EU-länder. Omvänd skattskyldighet.',
sru_code: '7310',
k2_excluded: false,
},
@@ -250,7 +250,7 @@ export const CLASS_3_ACCOUNTS: BASReferenceAccount[] = [
account_group: '35',
account_type: 'revenue',
normal_balance: 'credit',
description: 'Kostnader for emballage som vidarefaktureras till kunder.',
description: 'Kostnader för emballage som vidarefaktureras till kunder.',
sru_code: '7310',
k2_excluded: false,
},
@@ -294,7 +294,7 @@ export const CLASS_3_ACCOUNTS: BASReferenceAccount[] = [
account_group: '35',
account_type: 'revenue',
normal_balance: 'credit',
description: 'Fraktkostnader som vidarefaktureras till kunder i andra EU-lander.',
description: 'Fraktkostnader som vidarefaktureras till kunder i andra EU-länder.',
sru_code: '7310',
k2_excluded: false,
},
@@ -646,7 +646,7 @@ export const CLASS_3_ACCOUNTS: BASReferenceAccount[] = [
account_group: '37',
account_type: 'revenue',
normal_balance: 'debit',
description: 'Oreskillnad som uppstar vid avrundning av betalningar (oret).',
description: 'Oreskillnad som uppstår vid avrundning av betalningar (oret).',
sru_code: '7310',
k2_excluded: false,
},
@@ -745,7 +745,7 @@ export const CLASS_3_ACCOUNTS: BASReferenceAccount[] = [
account_group: '39',
account_type: 'revenue',
normal_balance: 'credit',
description: 'Andra intakter som inte hor till karnverksamheten, t.ex. uthyrning av lokal.',
description: 'Andra intäkter som inte hör till kärnverksamheten, t.ex. uthyrning av lokal.',
sru_code: '7311',
k2_excluded: false,
},
@@ -756,7 +756,7 @@ export const CLASS_3_ACCOUNTS: BASReferenceAccount[] = [
account_group: '39',
account_type: 'revenue',
normal_balance: 'credit',
description: 'Intakter fran uthyrning av lokaler, mark eller annan egendom.',
description: 'Intäkter från uthyrning av lokaler, mark eller annan egendom.',
sru_code: '7310',
k2_excluded: false,
},
@@ -877,7 +877,7 @@ export const CLASS_3_ACCOUNTS: BASReferenceAccount[] = [
account_group: '39',
account_type: 'revenue',
normal_balance: 'credit',
description: 'Vinster som uppstar vid valutavaxling eller betalningar i utlandsk valuta.',
description: 'Vinster som uppstår vid valutaväxling eller betalningar i utländsk valuta.',
sru_code: '7310',
k2_excluded: false,
},
@@ -888,7 +888,7 @@ export const CLASS_3_ACCOUNTS: BASReferenceAccount[] = [
account_group: '39',
account_type: 'revenue',
normal_balance: 'credit',
description: 'Vinst vid forsaljning av anlaggningstillgangar, t.ex. maskiner eller inventarier.',
description: 'Vinst vid försäljning av anläggningstillgångar, t.ex. maskiner eller inventarier.',
sru_code: '7311',
k2_excluded: false,
},
@@ -998,7 +998,7 @@ export const CLASS_3_ACCOUNTS: BASReferenceAccount[] = [
account_group: '39',
account_type: 'revenue',
normal_balance: 'credit',
description: 'Diverse andra rorelseintakter som inte passar i ovriga kategorier.',
description: 'Diverse andra rörelseintäkter som inte passar i övriga kategorier.',
sru_code: '7310',
k2_excluded: false,
},
@@ -19,7 +19,7 @@ export const CLASS_4_ACCOUNTS: BASReferenceAccount[] = [
account_group: '40',
account_type: 'expense',
normal_balance: 'debit',
description: 'Kostnader for inkop av varor avsedda for vidareforssaljning.',
description: 'Kostnader för inköp av varor avsedda för vidareförsäljning.',
sru_code: '7320',
k2_excluded: false,
},
@@ -382,7 +382,7 @@ export const CLASS_4_ACCOUNTS: BASReferenceAccount[] = [
account_group: '45',
account_type: 'expense',
normal_balance: 'debit',
description: 'Varuinkop fran utlandet (ravaror och fornodenheter).',
description: 'Varuinköp från utlandet (råvaror och förnödenheter).',
sru_code: '7320',
k2_excluded: false,
},
@@ -580,7 +580,7 @@ export const CLASS_4_ACCOUNTS: BASReferenceAccount[] = [
account_group: '46',
account_type: 'expense',
normal_balance: 'debit',
description: 'Kostnader for arbete utfort av underleverantorer som del av leverans till kund.',
description: 'Kostnader för arbete utfört av underleverantörer som del av leverans till kund.',
sru_code: '7320',
k2_excluded: false,
},
@@ -19,7 +19,7 @@ export const CLASS_5_ACCOUNTS: BASReferenceAccount[] = [
account_group: '50',
account_type: 'expense',
normal_balance: 'debit',
description: 'Manadshyra for kontorslokal, lager eller annan arbetsplats.',
description: 'Månadshyra för kontorslokal, lager eller annan arbetsplats.',
sru_code: '7321',
k2_excluded: false,
},
@@ -74,7 +74,7 @@ export const CLASS_5_ACCOUNTS: BASReferenceAccount[] = [
account_group: '50',
account_type: 'expense',
normal_balance: 'debit',
description: 'Elkostnader for foretagets lokaler.',
description: 'Elkostnader för företagets lokaler.',
sru_code: '7321',
k2_excluded: false,
},
@@ -118,7 +118,7 @@ export const CLASS_5_ACCOUNTS: BASReferenceAccount[] = [
account_group: '50',
account_type: 'expense',
normal_balance: 'debit',
description: 'Kostnader for stadning och rengoring av foretagets lokaler.',
description: 'Kostnader för städning och rengöring av företagets lokaler.',
sru_code: '7321',
k2_excluded: false,
},
@@ -426,7 +426,7 @@ export const CLASS_5_ACCOUNTS: BASReferenceAccount[] = [
account_group: '52',
account_type: 'expense',
normal_balance: 'debit',
description: 'Kostnader for leasing eller hyra av maskiner, bilar och annan utrustning.',
description: 'Kostnader för leasing eller hyra av maskiner, bilar och annan utrustning.',
sru_code: '7321',
k2_excluded: false,
},
@@ -492,7 +492,7 @@ export const CLASS_5_ACCOUNTS: BASReferenceAccount[] = [
account_group: '53',
account_type: 'expense',
normal_balance: 'debit',
description: 'Energikostnader for drift, t.ex. el for produktion.',
description: 'Energikostnader för drift, t.ex. el för produktion.',
sru_code: '7321',
k2_excluded: false,
},
@@ -635,7 +635,7 @@ export const CLASS_5_ACCOUNTS: BASReferenceAccount[] = [
account_group: '54',
account_type: 'expense',
normal_balance: 'debit',
description: 'Kostnader for mjukvara, prenumerationer och licenser.',
description: 'Kostnader för mjukvara, prenumerationer och licenser.',
sru_code: '7321',
k2_excluded: false,
},
@@ -668,7 +668,7 @@ export const CLASS_5_ACCOUNTS: BASReferenceAccount[] = [
account_group: '54',
account_type: 'expense',
normal_balance: 'debit',
description: 'Forbrukningsmaterial som inte ar kontorsmaterial, t.ex. forpackningsmaterial.',
description: 'Förbrukningsmaterial som inte är kontorsmaterial, t.ex. förpackningsmaterial.',
sru_code: '7321',
k2_excluded: false,
},
@@ -690,7 +690,7 @@ export const CLASS_5_ACCOUNTS: BASReferenceAccount[] = [
account_group: '55',
account_type: 'expense',
normal_balance: 'debit',
description: 'Kostnader for reparation och underhall av maskiner, inventarier och lokaler.',
description: 'Kostnader för reparation och underhåll av maskiner, inventarier och lokaler.',
sru_code: '7321',
k2_excluded: false,
},
@@ -778,7 +778,7 @@ export const CLASS_5_ACCOUNTS: BASReferenceAccount[] = [
account_group: '56',
account_type: 'expense',
normal_balance: 'debit',
description: 'Drivmedel, forsakring, reparation och ovriga kostnader for foretagsbilar.',
description: 'Drivmedel, försäkring, reparation och övriga kostnader för företagsbilar.',
sru_code: '7321',
k2_excluded: false,
},
@@ -1372,7 +1372,7 @@ export const CLASS_5_ACCOUNTS: BASReferenceAccount[] = [
account_group: '57',
account_type: 'expense',
normal_balance: 'debit',
description: 'Kostnader for frakt och transport av varor till och fran foretaget.',
description: 'Kostnader för frakt och transport av varor till och från företaget.',
sru_code: '7321',
k2_excluded: false,
},
@@ -1471,7 +1471,7 @@ export const CLASS_5_ACCOUNTS: BASReferenceAccount[] = [
account_group: '58',
account_type: 'expense',
normal_balance: 'debit',
description: 'Tjansteresor: tag, flyg, taxi och ovriga resekostnader.',
description: 'Tjänsteresor: tåg, flyg, taxi och övriga resekostnader.',
sru_code: '7321',
k2_excluded: false,
},
@@ -1482,7 +1482,7 @@ export const CLASS_5_ACCOUNTS: BASReferenceAccount[] = [
account_group: '58',
account_type: 'expense',
normal_balance: 'debit',
description: 'Rese- och transportbiljetter for tjansteresor (tag, flyg, buss).',
description: 'Rese- och transportbiljetter för tjänsteresor (tåg, flyg, buss).',
sru_code: '7321',
k2_excluded: false,
},
@@ -1493,7 +1493,7 @@ export const CLASS_5_ACCOUNTS: BASReferenceAccount[] = [
account_group: '58',
account_type: 'expense',
normal_balance: 'debit',
description: 'Hyrbilskostnader vid tjansteresor.',
description: 'Hyrbilskostnader vid tjänsteresor.',
sru_code: '7321',
k2_excluded: false,
},
@@ -1559,7 +1559,7 @@ export const CLASS_5_ACCOUNTS: BASReferenceAccount[] = [
account_group: '59',
account_type: 'expense',
normal_balance: 'debit',
description: 'Kostnader for marknadsforing, annonser, Google Ads, sociala medier och reklamkampanjer.',
description: 'Kostnader för marknadsföring, annonser, Google Ads, sociala medier och reklamkampanjer.',
sru_code: '7321',
k2_excluded: false,
},
@@ -1581,7 +1581,7 @@ export const CLASS_5_ACCOUNTS: BASReferenceAccount[] = [
account_group: '59',
account_type: 'expense',
normal_balance: 'debit',
description: 'Trycksaker for marknadsforingsandamal, t.ex. broschyrer och visitkort.',
description: 'Trycksaker för marknadsföringsändamål, t.ex. broschyrer och visitkort.',
sru_code: '7321',
k2_excluded: false,
},
@@ -184,7 +184,7 @@ export const CLASS_6_ACCOUNTS: BASReferenceAccount[] = [
account_group: '60',
account_type: 'expense',
normal_balance: 'debit',
description: 'Representation som overstiger avdragsgilla beloppet. Inte skattemassigt avdragsgill.',
description: 'Representation som överstiger avdragsgilla beloppet. Inte skattemässigt avdragsgill.',
sru_code: '7321',
k2_excluded: false,
},
@@ -228,7 +228,7 @@ export const CLASS_6_ACCOUNTS: BASReferenceAccount[] = [
account_group: '61',
account_type: 'expense',
normal_balance: 'debit',
description: 'Pennor, papper, toner, USB-minnen och ovriga kontorsfornodenheter.',
description: 'Pennor, papper, toner, USB-minnen och övriga kontorsförnödenheter.',
sru_code: '7321',
k2_excluded: false,
},
@@ -239,7 +239,7 @@ export const CLASS_6_ACCOUNTS: BASReferenceAccount[] = [
account_group: '61',
account_type: 'expense',
normal_balance: 'debit',
description: 'Trycksaker for internt bruk, t.ex. blanketter och formular.',
description: 'Trycksaker för internt bruk, t.ex. blanketter och formulär.',
sru_code: '7321',
k2_excluded: false,
},
@@ -272,7 +272,7 @@ export const CLASS_6_ACCOUNTS: BASReferenceAccount[] = [
account_group: '62',
account_type: 'expense',
normal_balance: 'debit',
description: 'Kostnad for fast telefoni och telefonabonnemang.',
description: 'Kostnad för fast telefoni och telefonabonnemang.',
sru_code: '7321',
k2_excluded: false,
},
@@ -283,7 +283,7 @@ export const CLASS_6_ACCOUNTS: BASReferenceAccount[] = [
account_group: '62',
account_type: 'expense',
normal_balance: 'debit',
description: 'Mobilabonnemang och samtalskostnader for foretagets mobiler.',
description: 'Mobilabonnemang och samtalskostnader för företagets mobiler.',
sru_code: '7321',
k2_excluded: false,
},
@@ -305,7 +305,7 @@ export const CLASS_6_ACCOUNTS: BASReferenceAccount[] = [
account_group: '62',
account_type: 'expense',
normal_balance: 'debit',
description: 'Bredband, internetabonnemang, domaner, hosting och molntjanster.',
description: 'Bredband, internetabonnemang, domäner, hosting och molntjänster.',
sru_code: '7321',
k2_excluded: false,
},
@@ -316,7 +316,7 @@ export const CLASS_6_ACCOUNTS: BASReferenceAccount[] = [
account_group: '62',
account_type: 'expense',
normal_balance: 'debit',
description: 'Porto, frimarken och kostnader for postutskick.',
description: 'Porto, frimärken och kostnader för postutskick.',
sru_code: '7321',
k2_excluded: false,
},
@@ -349,7 +349,7 @@ export const CLASS_6_ACCOUNTS: BASReferenceAccount[] = [
account_group: '63',
account_type: 'expense',
normal_balance: 'debit',
description: 'Premiekostnader for foretagets forsakringar, t.ex. ansvars- och egendomsforsakring.',
description: 'Premiekostnader för företagets försäkringar, t.ex. ansvars- och egendomsförsäkring.',
sru_code: '7321',
k2_excluded: false,
},
@@ -448,7 +448,7 @@ export const CLASS_6_ACCOUNTS: BASReferenceAccount[] = [
account_group: '63',
account_type: 'expense',
normal_balance: 'debit',
description: 'Kostnader for garantier och garantiavsattningar.',
description: 'Kostnader för garantier och garantiavsättningar.',
sru_code: '7321',
k2_excluded: false,
},
@@ -657,7 +657,7 @@ export const CLASS_6_ACCOUNTS: BASReferenceAccount[] = [
account_group: '65',
account_type: 'expense',
normal_balance: 'debit',
description: 'Kostnader for matning och besiktning.',
description: 'Kostnader för mätning och besiktning.',
sru_code: '7321',
k2_excluded: false,
},
@@ -679,7 +679,7 @@ export const CLASS_6_ACCOUNTS: BASReferenceAccount[] = [
account_group: '65',
account_type: 'expense',
normal_balance: 'debit',
description: 'Avgifter till bokforingsbyrta eller redovisningskonsult.',
description: 'Avgifter till bokföringsbyrå eller redovisningskonsult.',
sru_code: '7321',
k2_excluded: false,
},
@@ -690,7 +690,7 @@ export const CLASS_6_ACCOUNTS: BASReferenceAccount[] = [
account_group: '65',
account_type: 'expense',
normal_balance: 'debit',
description: 'Kostnader for extern IT-support, konsultation och drifttjanster.',
description: 'Kostnader för extern IT-support, konsultation och drifttjänster.',
sru_code: '7321',
k2_excluded: false,
},
@@ -701,7 +701,7 @@ export const CLASS_6_ACCOUNTS: BASReferenceAccount[] = [
account_group: '65',
account_type: 'expense',
normal_balance: 'debit',
description: 'Arvode till externa konsulter for radgivning och specialisttjanster.',
description: 'Arvode till externa konsulter för rådgivning och specialisttjänster.',
sru_code: '7321',
k2_excluded: false,
},
@@ -789,7 +789,7 @@ export const CLASS_6_ACCOUNTS: BASReferenceAccount[] = [
account_group: '65',
account_type: 'expense',
normal_balance: 'debit',
description: 'Serviceavtal och underhalskostnader for utrustning och system.',
description: 'Serviceavtal och underhållskostnader för utrustning och system.',
sru_code: '7321',
k2_excluded: false,
},
@@ -800,7 +800,7 @@ export const CLASS_6_ACCOUNTS: BASReferenceAccount[] = [
account_group: '65',
account_type: 'expense',
normal_balance: 'debit',
description: 'Avgifter for banktjanster, betalformedling, Swish och kortinlosen.',
description: 'Avgifter för banktjänster, betalförmedling, Swish och kortinlösen.',
sru_code: '7321',
k2_excluded: false,
},
@@ -1031,7 +1031,7 @@ export const CLASS_6_ACCOUNTS: BASReferenceAccount[] = [
account_group: '69',
account_type: 'expense',
normal_balance: 'debit',
description: 'Prenumerationer pa tidningar, tidskrifter och branschpublikationer.',
description: 'Prenumerationer på tidningar, tidskrifter och branschpublikationer.',
sru_code: '7321',
k2_excluded: false,
},
@@ -1086,7 +1086,7 @@ export const CLASS_6_ACCOUNTS: BASReferenceAccount[] = [
account_group: '69',
account_type: 'expense',
normal_balance: 'debit',
description: 'Diverse externa kostnader som inte passar in under andra konton men ar avdragsgilla.',
description: 'Diverse externa kostnader som inte passar in under andra konton men är avdragsgilla.',
sru_code: '7330',
k2_excluded: false,
},
@@ -1097,7 +1097,7 @@ export const CLASS_6_ACCOUNTS: BASReferenceAccount[] = [
account_group: '69',
account_type: 'expense',
normal_balance: 'debit',
description: 'Externa kostnader som inte ar skattemassigt avdragsgilla, t.ex. boter och forseningsavgifter.',
description: 'Externa kostnader som inte är skattemässigt avdragsgilla, t.ex. böter och förseningsavgifter.',
sru_code: '7330',
k2_excluded: false,
},
+15 -15
View File
@@ -19,7 +19,7 @@ export const CLASS_7_ACCOUNTS: BASReferenceAccount[] = [
account_group: '70',
account_type: 'expense',
normal_balance: 'debit',
description: 'Bruttoloner (fore skatt) till kollektivanstallda.',
description: 'Bruttolöner (före skatt) till kollektivanställda.',
sru_code: '7322',
k2_excluded: false,
},
@@ -184,7 +184,7 @@ export const CLASS_7_ACCOUNTS: BASReferenceAccount[] = [
account_group: '70',
account_type: 'expense',
normal_balance: 'debit',
description: 'Semesterloner till kollektivanstallda.',
description: 'Semesterlöner till kollektivanställda.',
sru_code: '7322',
k2_excluded: false,
},
@@ -217,7 +217,7 @@ export const CLASS_7_ACCOUNTS: BASReferenceAccount[] = [
account_group: '70',
account_type: 'expense',
normal_balance: 'debit',
description: 'Justering av skuld for intjanade semesterdagar som annu inte tagits ut.',
description: 'Justering av skuld för intjänade semesterdagar som ännu inte tagits ut.',
sru_code: '7322',
k2_excluded: false,
},
@@ -239,7 +239,7 @@ export const CLASS_7_ACCOUNTS: BASReferenceAccount[] = [
account_group: '72',
account_type: 'expense',
normal_balance: 'debit',
description: 'Bruttoloner till tjansteman.',
description: 'Bruttolöner till tjänstemän.',
sru_code: '7322',
k2_excluded: false,
},
@@ -558,7 +558,7 @@ export const CLASS_7_ACCOUNTS: BASReferenceAccount[] = [
account_group: '72',
account_type: 'expense',
normal_balance: 'debit',
description: 'Justering av semesterlonskuld for tjansteman.',
description: 'Justering av semesterlöneskuld för tjänstemän.',
sru_code: '7322',
k2_excluded: false,
},
@@ -602,7 +602,7 @@ export const CLASS_7_ACCOUNTS: BASReferenceAccount[] = [
account_group: '73',
account_type: 'expense',
normal_balance: 'debit',
description: 'Kostnadsersattningar till anstallda, t.ex. milersattning och traktamente.',
description: 'Kostnadsersättningar till anställda, t.ex. milersättning och traktamente.',
sru_code: '7322',
k2_excluded: false,
},
@@ -987,7 +987,7 @@ export const CLASS_7_ACCOUNTS: BASReferenceAccount[] = [
account_group: '74',
account_type: 'expense',
normal_balance: 'debit',
description: 'Premiebetalningar for tjanstepension till anstallda.',
description: 'Premiebetalningar för tjänstepension till anställda.',
sru_code: '7322',
k2_excluded: false,
},
@@ -1218,7 +1218,7 @@ export const CLASS_7_ACCOUNTS: BASReferenceAccount[] = [
account_group: '75',
account_type: 'expense',
normal_balance: 'debit',
description: 'Beraknade sociala avgifter pa upplupna semesterloner och andra loneskulder.',
description: 'Beräknade sociala avgifter på upplupna semesterlöner och andra löneskulder.',
sru_code: '7322',
k2_excluded: false,
},
@@ -1262,7 +1262,7 @@ export const CLASS_7_ACCOUNTS: BASReferenceAccount[] = [
account_group: '75',
account_type: 'expense',
normal_balance: 'debit',
description: 'Sarskild loneskatt pa pensionskostnader (24,26%).',
description: 'Särskild löneskatt på pensionskostnader (24,26%).',
sru_code: '7322',
k2_excluded: false,
},
@@ -1438,7 +1438,7 @@ export const CLASS_7_ACCOUNTS: BASReferenceAccount[] = [
account_group: '76',
account_type: 'expense',
normal_balance: 'debit',
description: 'Kostnader for utbildning, kurser och konferenser for anstallda.',
description: 'Kostnader för utbildning, kurser och konferenser för anställda.',
sru_code: '7322',
k2_excluded: false,
},
@@ -1779,7 +1779,7 @@ export const CLASS_7_ACCOUNTS: BASReferenceAccount[] = [
account_group: '78',
account_type: 'expense',
normal_balance: 'debit',
description: 'Planmassig avskrivning av goodwill, patent och andra immateriella tillgangar.',
description: 'Planmässig avskrivning av goodwill, patent och andra immateriella tillgangar.',
sru_code: '7325',
k2_excluded: false,
},
@@ -1878,7 +1878,7 @@ export const CLASS_7_ACCOUNTS: BASReferenceAccount[] = [
account_group: '78',
account_type: 'expense',
normal_balance: 'debit',
description: 'Arlig vardeminskning pa byggnader. Typiskt 2-5% per ar beroende pa byggnadstyp.',
description: 'Årlig värdeminskning på byggnader. Typiskt 2-5% per är beroende på byggnadstyp.',
sru_code: '7324',
k2_excluded: false,
},
@@ -1944,7 +1944,7 @@ export const CLASS_7_ACCOUNTS: BASReferenceAccount[] = [
account_group: '78',
account_type: 'expense',
normal_balance: 'debit',
description: 'Arlig vardeminskning pa inventarier, maskiner och verktyg.',
description: 'Årlig värdeminskning på inventarier, maskiner och verktyg.',
sru_code: '7325',
k2_excluded: false,
},
@@ -1999,7 +1999,7 @@ export const CLASS_7_ACCOUNTS: BASReferenceAccount[] = [
account_group: '79',
account_type: 'expense',
normal_balance: 'debit',
description: 'Forluster som uppstar vid valutavaxling eller betalningar i utlandsk valuta.',
description: 'Förluster som uppstår vid valutaväxling eller betalningar i utländsk valuta.',
sru_code: '7360',
k2_excluded: false,
},
@@ -2010,7 +2010,7 @@ export const CLASS_7_ACCOUNTS: BASReferenceAccount[] = [
account_group: '79',
account_type: 'expense',
normal_balance: 'debit',
description: 'Forlust vid avyttring av immateriella och materiella anlaggningstillgangar.',
description: 'Förlust vid avyttring av immateriella och materiella anläggningstillgångar.',
sru_code: '7321',
k2_excluded: false,
},
@@ -811,7 +811,7 @@ export const CLASS_8_ACCOUNTS: BASReferenceAccount[] = [
account_group: '83',
account_type: 'revenue',
normal_balance: 'credit',
description: 'Ranta pa bankkontosaldo, sparkonton och utlanade pengar.',
description: 'Ränta på bankkontosaldo, sparkonton och utlånade pengar.',
sru_code: '7313',
k2_excluded: false,
},
@@ -1064,7 +1064,7 @@ export const CLASS_8_ACCOUNTS: BASReferenceAccount[] = [
account_group: '84',
account_type: 'expense',
normal_balance: 'debit',
description: 'Ranta pa lan, krediter och ovriga skulder till kreditgivare.',
description: 'Ränta på lån, krediter och övriga skulder till kreditgivare.',
sru_code: '7323',
k2_excluded: false,
},
@@ -1372,7 +1372,7 @@ export const CLASS_8_ACCOUNTS: BASReferenceAccount[] = [
account_group: '88',
account_type: 'revenue',
normal_balance: 'credit',
description: 'Forandring av periodiseringsfond.',
description: 'Förändring av periodiseringsfond.',
sru_code: '7380',
k2_excluded: false,
},
@@ -1438,7 +1438,7 @@ export const CLASS_8_ACCOUNTS: BASReferenceAccount[] = [
account_group: '88',
account_type: 'revenue',
normal_balance: 'credit',
description: 'Forandring av overavskrivningar.',
description: 'Förändring av överavskrivningar.',
sru_code: '7380',
k2_excluded: false,
},
@@ -1603,7 +1603,7 @@ export const CLASS_8_ACCOUNTS: BASReferenceAccount[] = [
account_group: '89',
account_type: 'expense',
normal_balance: 'debit',
description: 'Beraknad inkomstskatt pa det skattepliktiga resultatet for rakenskapsaret.',
description: 'Beräknad inkomstskatt på det skattepliktiga resultatet för räkenskapsåret.',
sru_code: '7380',
k2_excluded: false,
},
@@ -1669,7 +1669,7 @@ export const CLASS_8_ACCOUNTS: BASReferenceAccount[] = [
account_group: '89',
account_type: 'expense',
normal_balance: 'debit',
description: 'Slutresultatkonto som visar vinst eller forlust efter alla intakter och kostnader.',
description: 'Slutresultatkonto som visar vinst eller förlust efter alla intäkter och kostnader.',
sru_code: '7380',
k2_excluded: false,
},
+61 -61
View File
@@ -36,12 +36,12 @@ export { BAS_REFERENCE } from './bas-data'
/** Swedish labels for each BAS account class (1-8) */
export const ACCOUNT_CLASS_LABELS: Record<number, string> = {
1: 'Tillgangar',
1: 'Tillgångar',
2: 'Eget kapital och skulder',
3: 'Rorelseintatker',
4: 'Varuinkop och material',
5: 'Ovriga externa kostnader',
6: 'Ovriga externa kostnader',
3: 'Rörelseintäkter',
4: 'Varuinköp och material',
5: 'Övriga externa kostnader',
6: 'Övriga externa kostnader',
7: 'Personalkostnader och avskrivningar',
8: 'Finansiella poster och resultat',
}
@@ -49,100 +49,100 @@ export const ACCOUNT_CLASS_LABELS: Record<number, string> = {
/** Swedish labels for BAS account groups (first two digits) */
export const ACCOUNT_GROUP_LABELS: Record<string, string> = {
// Class 1 - Assets
'10': 'Immateriella anlaggningstillgangar',
'10': 'Immateriella anläggningstillgångar',
'11': 'Byggnader och mark',
'12': 'Maskiner respektive inventarier',
'13': 'Finansiella anlaggningstillgangar',
'14': 'Lager, produkter i arbete och pagaende arbeten',
'13': 'Finansiella anläggningstillgångar',
'14': 'Lager, produkter i arbete och pågående arbeten',
'15': 'Kundfordringar',
'16': 'Ovriga kortfristiga fordringar',
'17': 'Forutbetalda kostnader och upplupna intakter',
'16': 'Övriga kortfristiga fordringar',
'17': 'Förutbetalda kostnader och upplupna intäkter',
'18': 'Kortfristiga placeringar',
'19': 'Kassa och bank',
// Class 2 - Equity & Liabilities
'20': 'Eget kapital',
'21': 'Obeskattade reserver',
'22': 'Avsattningar',
'23': 'Langfristiga skulder',
'24': 'Kortfristiga skulder till kreditinstitut, kunder och leverantorer',
'22': 'Avsättningar',
'23': 'Långfristiga skulder',
'24': 'Kortfristiga skulder till kreditinstitut, kunder och leverantörer',
'25': 'Skatteskulder',
'26': 'Moms och punktskatter',
'27': 'Personalens skatter, avgifter och loneavdrag',
'28': 'Ovriga kortfristiga skulder',
'29': 'Upplupna kostnader och forutbetalda intakter',
'27': 'Personalens skatter, avgifter och löneavdrag',
'28': 'Övriga kortfristiga skulder',
'29': 'Upplupna kostnader och förutbetalda intäkter',
// Class 3 - Revenue
'30': 'Huvudintakter',
'31': 'Forsaljning av varor utanfor Sverige',
'32': 'Forsaljning VMB och omvand moms',
'33': 'Forsaljning av tjanster utanfor Sverige',
'34': 'Forsaljning, egna uttag',
'30': 'Huvudintäkter',
'31': 'Försäljning av varor utanför Sverige',
'32': 'Försäljning VMB och omvänd moms',
'33': 'Försäljning av tjänster utanför Sverige',
'34': 'Försäljning, egna uttag',
'35': 'Fakturerade kostnader',
'36': 'Rorelsens sidointakter',
'37': 'Intaktskorrigeringar',
'38': 'Aktiverat arbete for egen rakning',
'39': 'Ovriga rorelseintakter',
'36': 'Rörelsens sidointäkter',
'37': 'Intäktskorrigeringar',
'38': 'Aktiverat arbete för egen räkning',
'39': 'Övriga rörelseintäkter',
// Class 4 - Cost of goods
'40': 'Inkop av handelsvaror',
'41': 'Inkop av varor och material',
'42': 'Salda handelsvaror VMB',
'43': 'Inkop av ravaror och material i Sverige',
'44': 'Inkop av ravaror m.m., omvand betalningsskyldighet',
'45': 'Inkop av ravaror m.m. fran utlandet',
'46': 'Inkop av tjanster, underentreprenader och legoarbeten',
'47': 'Reduktion av inkopspriser',
'40': 'Inköp av handelsvaror',
'41': 'Inköp av varor och material',
'42': 'Sålda handelsvaror VMB',
'43': 'Inköp av råvaror och material i Sverige',
'44': 'Inköp av råvaror m.m., omvänd betalningsskyldighet',
'45': 'Inköp av råvaror m.m. från utlandet',
'46': 'Inköp av tjänster, underentreprenader och legoarbeten',
'47': 'Reduktion av inköpspriser',
'48': 'Andra produktionskostnader',
'49': 'Forandring av lager, produkter i arbete och pagaende arbeten',
'49': 'Förändring av lager, produkter i arbete och pågående arbeten',
// Class 5 - External expenses
'50': 'Lokalkostnader',
'51': 'Fastighetskostnader',
'52': 'Hyra av anlaggningstillgangar',
'53': 'Energikostnader for drift',
'54': 'Forbrukningsinventarier och forbrukningsmaterial',
'55': 'Reparation och underhall',
'56': 'Kostnader for transportmedel',
'52': 'Hyra av anläggningstillgångar',
'53': 'Energikostnader för drift',
'54': 'Förbrukningsinventarier och förbrukningsmaterial',
'55': 'Reparation och underhåll',
'56': 'Kostnader för transportmedel',
'57': 'Frakter och transporter',
'58': 'Resekostnader',
'59': 'Reklam och PR',
// Class 6 - Other external expenses
'60': 'Ovriga forsaljningskostnader',
'60': 'Övriga försäljningskostnader',
'61': 'Kontorsmateriel och trycksaker',
'62': 'Tele, data och post',
'63': 'Foretagsforsakringar och ovriga riskkostnader',
'64': 'Forvaltningskostnader',
'65': 'Ovriga externa tjanster',
'63': 'Företagsförsäkringar och övriga riskkostnader',
'64': 'Förvaltningskostnader',
'65': 'Övriga externa tjänster',
'66': 'Franchisingavgifter',
'67': 'Sarskilt for ideella foreningar och stiftelser',
'67': 'Särskilt för ideella föreningar och stiftelser',
'68': 'Inhyrd personal',
'69': 'Ovriga externa kostnader',
'69': 'Övriga externa kostnader',
// Class 7 - Personnel
'70': 'Loner till kollektivanstallda',
'71': 'Loner till anstallda',
'72': 'Loner till tjansteman och foretagsledare',
'73': 'Kostnadsersattningar och formaner',
'70': 'Löner till kollektivanställda',
'71': 'Löner till anställda',
'72': 'Löner till tjänstemän och företagsledare',
'73': 'Kostnadsersättningar och förmåner',
'74': 'Pensionskostnader',
'75': 'Sociala och andra avgifter enligt lag och avtal',
'76': 'Ovriga personalkostnader',
'77': 'Nedskrivningar och aterforing av nedskrivningar',
'76': 'Övriga personalkostnader',
'77': 'Nedskrivningar och återföring av nedskrivningar',
'78': 'Avskrivningar enligt plan',
'79': 'Ovriga rorelsekostnader',
'79': 'Övriga rörelsekostnader',
// Class 8 - Financial
'80': 'Resultat fran andelar i koncernforetag',
'81': 'Resultat fran andelar i intresseforetag',
'82': 'Resultat fran ovriga vardepapper och langfristiga fordringar',
'83': 'Ovriga ranteintakter och liknande resultatposter',
'84': 'Rantekostnader och liknande resultatposter',
'85': 'Extraordinara intakter',
'86': 'Extraordinara kostnader',
'87': 'Bokslutsdispositioner (intakter)',
'80': 'Resultat från andelar i koncernföretag',
'81': 'Resultat från andelar i intresseföretag',
'82': 'Resultat från övriga värdepapper och långfristiga fordringar',
'83': 'Övriga ränteintäkter och liknande resultatposter',
'84': 'Räntekostnader och liknande resultatposter',
'85': 'Extraordinära intäkter',
'86': 'Extraordinära kostnader',
'87': 'Bokslutsdispositioner (intäkter)',
'88': 'Bokslutsdispositioner',
'89': 'Skatter och arets resultat',
'89': 'Skatter och årets resultat',
}
// ---------------------------------------------------------------------------
+191 -19
View File
@@ -57,6 +57,8 @@ export interface BookingTemplate {
default_private: boolean
fallback_category: TransactionCategory
description_sv: string
common: boolean
requires_vat_registration_data?: boolean
}
export interface TemplateGroupInfo {
@@ -96,7 +98,7 @@ const GROUP_LABELS: Record<TemplateGroup, { sv: string; en: string }> = {
}
// ============================================================
// Template Data (48 templates)
// Template Data
// ============================================================
export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
@@ -123,6 +125,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_office',
description_sv: 'Månadshyra för kontors- eller affärslokal',
common: true,
},
{
id: 'premises_rent_vat',
@@ -146,6 +149,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_office',
description_sv: 'Lokalhyra med moms (frivilligt momsregistrerad hyresvärd)',
common: false,
},
{
id: 'premises_electricity',
@@ -168,6 +172,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_office',
description_sv: 'El, uppvärmning och vatten för kontors- eller affärslokal',
common: true,
},
// --- VEHICLE (4) ---
@@ -193,6 +198,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_travel',
description_sv: 'Drivmedel (bensin/diesel) eller laddning (elbil) för tjänstefordon',
common: true,
},
{
id: 'vehicle_leasing',
@@ -217,6 +223,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_travel',
description_sv: 'Leasingavgift för tjänstefordon',
common: false,
},
{
id: 'vehicle_repairs',
@@ -239,6 +246,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_travel',
description_sv: 'Reparation, service och underhåll av fordon',
common: false,
},
{
id: 'vehicle_parking',
@@ -261,6 +269,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_travel',
description_sv: 'Parkeringsavgift och trängselskatt vid tjänsteärende',
common: false,
},
// --- IT & SOFTWARE (3) ---
@@ -285,6 +294,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_software',
description_sv: 'Programvarulicens eller SaaS-prenumeration (svensk leverantör med moms)',
common: false,
},
{
id: 'it_saas_eu',
@@ -308,6 +318,8 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_software',
description_sv: 'Programvara från utländsk leverantör med omvänd skattskyldighet',
common: true,
requires_vat_registration_data: true,
},
{
id: 'it_cloud_hosting',
@@ -331,6 +343,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_software',
description_sv: 'Molnbaserade tjänster, webbhotell, serverhosting och domännamn',
common: true,
},
// --- OFFICE SUPPLIES (2) ---
@@ -355,6 +368,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_office',
description_sv: 'Kontorsmaterial, trycksaker och förbrukningsvaror',
common: true,
},
{
id: 'office_postage',
@@ -378,13 +392,14 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_office',
description_sv: 'Porto och fraktkostnader',
common: true,
},
// --- MARKETING (2) ---
// --- MARKETING (3) ---
{
id: 'marketing_online_ads',
name_sv: 'Annonsering & Marknadsföring',
name_en: 'Advertising & Marketing',
id: 'marketing_online_ads_eu',
name_sv: 'Annonsering EU (omvänd moms)',
name_en: 'Online ads EU (reverse charge)',
group: 'marketing',
direction: 'expense',
entity_applicability: 'all',
@@ -402,7 +417,33 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
auto_match_confidence: 0.90,
default_private: false,
fallback_category: 'expense_marketing',
description_sv: 'Digital annonsering och marknadsföring',
description_sv: 'Digital annonsering från EU-leverantör (Google/Meta från Irland)',
common: true,
requires_vat_registration_data: true,
},
{
id: 'marketing_online_ads_domestic',
name_sv: 'Annonsering (svensk moms)',
name_en: 'Online ads (domestic VAT)',
group: 'marketing',
direction: 'expense',
entity_applicability: 'all',
debit_account: '5910',
credit_account: '1930',
vat_treatment: 'standard_25',
vat_rate: 0.25,
deductibility: 'full',
special_rules_sv: 'Svensk leverantör med momsregistrering',
mcc_codes: [7311],
keywords: ['annons', 'reklam', 'advertising', 'kampanj', 'blocket', 'eniro'],
risk_level: 'NONE',
requires_review: false,
impact_score: 7,
auto_match_confidence: 0.80,
default_private: false,
fallback_category: 'expense_marketing',
description_sv: 'Digital annonsering från svensk leverantör med 25% moms',
common: false,
},
{
id: 'marketing_design',
@@ -425,6 +466,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_marketing',
description_sv: 'Grafisk design, reklam, foto/video och marknadsföringsverktyg',
common: false,
},
// --- TRAVEL (3) ---
@@ -450,6 +492,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_travel',
description_sv: 'Resor i tjänsten: flyg, tåg, taxi, hyrbil (6% moms)',
common: true,
},
{
id: 'travel_international',
@@ -473,6 +516,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_travel',
description_sv: 'Utrikesresor i tjänsten (momsfritt)',
common: false,
},
{
id: 'travel_hotel',
@@ -496,9 +540,10 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_travel',
description_sv: 'Hotellövernattning i tjänsten (12% moms)',
common: true,
},
// --- REPRESENTATION (2) ---
// --- REPRESENTATION (3) ---
{
id: 'representation_external',
name_sv: 'Extern representation',
@@ -511,7 +556,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
vat_treatment: 'reduced_12',
vat_rate: 0.12,
deductibility: 'conditional',
deductibility_note_sv: 'Max 300 kr/person exkl moms (IL 16 kap 2§)',
deductibility_note_sv: 'Avdragsgill moms max 46 kr/person. Representationskostnad max 300 kr/person exkl moms (IL 16 kap 2§)',
special_rules_sv: 'Dokumentera: syfte, deltagare, företag. Momsavdrag max 300 kr/person.',
mcc_codes: [5812, 5813, 5814],
keywords: ['representation', 'lunch', 'middag', 'restaurang', 'restaurant', 'kund', 'kundmöte', 'gåva', 'present', 'representationsgåva'],
@@ -522,6 +567,32 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_other',
description_sv: 'Representation med kund/affärspartner (dokumentera noggrant)',
common: true,
},
{
id: 'representation_internal',
name_sv: 'Intern representation',
name_en: 'Internal representation',
group: 'representation',
direction: 'expense',
entity_applicability: 'all',
debit_account: '7622',
credit_account: '1930',
vat_treatment: null,
vat_rate: 0,
deductibility: 'conditional',
deductibility_note_sv: 'Max 60 kr/person',
special_rules_sv: 'Personalfest, intern lunch etc. Momsfritt. Max 60 kr/person för avdragsrätt.',
mcc_codes: [5812, 5813, 5814],
keywords: ['personalfest', 'intern representation', 'teamlunch', 'personallunch', 'fika', 'julfest', 'after work', 'intern lunch'],
risk_level: 'LOW',
requires_review: false,
impact_score: 5,
auto_match_confidence: 0.70,
default_private: false,
fallback_category: 'expense_representation',
description_sv: 'Intern representation (personalfest, teamlunch)',
common: true,
},
{
id: 'representation_conference',
@@ -544,6 +615,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_education',
description_sv: 'Avgifter för konferenser och mässor',
common: false,
},
// --- INSURANCE (3) ---
@@ -569,6 +641,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_other',
description_sv: 'Företags-, ansvars- och fordonsförsäkring (momsfritt)',
common: true,
},
{
id: 'insurance_pension_ef',
@@ -593,6 +666,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_other',
description_sv: 'Pensionssparande för enskild firma (granska avdragsregel)',
common: false,
},
{
id: 'insurance_pension_ab',
@@ -616,6 +690,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_other',
description_sv: 'Tjänstepension för anställda i aktiebolag',
common: false,
},
// --- PROFESSIONAL SERVICES (2) ---
@@ -640,6 +715,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_professional_services',
description_sv: 'Redovisning, bokföring, revision och juridiska tjänster',
common: true,
},
{
id: 'prof_consulting',
@@ -662,6 +738,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_professional_services',
description_sv: 'Konsultarvoden och rådgivningstjänster',
common: true,
},
// --- BANK & FINANCE (5) ---
@@ -687,6 +764,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_bank_fees',
description_sv: 'Bankavgifter, kontoavgifter och kortavgifter',
common: true,
},
{
id: 'bank_interest_income',
@@ -709,6 +787,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'income_other',
description_sv: 'Ränteintäkter på bankkonto eller placeringar',
common: false,
},
{
id: 'bank_interest_expense',
@@ -731,6 +810,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_other',
description_sv: 'Räntekostnad på lån eller kredit',
common: false,
},
{
id: 'bank_currency_loss',
@@ -753,6 +833,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_currency_exchange',
description_sv: 'Valutakursförluster vid betalning i utländsk valuta',
common: false,
},
{
id: 'bank_currency_gain',
@@ -775,6 +856,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'income_other',
description_sv: 'Valutakursvinster vid betalning i utländsk valuta',
common: false,
},
// --- TELECOM (2) ---
@@ -800,6 +882,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_other',
description_sv: 'Telefon och mobilabonnemang (granska yrkesmässig andel)',
common: true,
},
{
id: 'telecom_internet',
@@ -822,6 +905,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_other',
description_sv: 'Internetanslutning för kontor',
common: false,
},
// --- EDUCATION (2) ---
@@ -848,6 +932,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_education',
description_sv: 'Yrkesrelaterade kurser och utbildningar',
common: false,
},
{
id: 'education_membership',
@@ -871,6 +956,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_education',
description_sv: 'Medlemsavgift i branschorganisation eller yrkesförening',
common: false,
},
// --- PERSONNEL (3) ---
@@ -886,6 +972,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
vat_treatment: null,
vat_rate: 0,
deductibility: 'full',
special_rules_sv: 'OBS: Denna mall bokför nettolön. Personalskatt (2710) och arbetsgivaravgifter (2731) måste bokföras separat.',
mcc_codes: [],
keywords: ['lön', 'salary', 'nettolön', 'löneutbetalning'],
risk_level: 'NONE',
@@ -895,6 +982,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_other',
description_sv: 'Nettolön till anställd',
common: true,
},
{
id: 'personnel_employer_tax',
@@ -903,11 +991,12 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
group: 'personnel',
direction: 'expense',
entity_applicability: 'aktiebolag',
debit_account: '7510',
debit_account: '2731',
credit_account: '1930',
vat_treatment: null,
vat_rate: 0,
deductibility: 'full',
special_rules_sv: 'Betalning av arbetsgivaravgift-skuld. Kostnad (D: 7510 / K: 2731) bokförs vid lönekörning.',
mcc_codes: [],
keywords: ['arbetsgivaravgift', 'sociala avgifter', 'employer tax', 'skattekonto'],
risk_level: 'NONE',
@@ -916,7 +1005,8 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
auto_match_confidence: 0.75,
default_private: false,
fallback_category: 'expense_other',
description_sv: 'Arbetsgivaravgifter (31.42%)',
description_sv: 'Betalning av arbetsgivaravgifter',
common: true,
},
{
id: 'personnel_preliminary_tax',
@@ -939,6 +1029,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_other',
description_sv: 'Preliminärskatt till Skatteverket',
common: false,
},
// --- REVENUE (4) ---
@@ -963,6 +1054,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'income_services',
description_sv: 'Intäkter från tjänste- eller varuförsäljning med 25% moms',
common: true,
},
{
id: 'revenue_reduced_12',
@@ -985,6 +1077,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'income_products',
description_sv: 'Intäkter från livsmedelsförsäljning eller logi med 12% moms',
common: true,
},
{
id: 'revenue_eu_services',
@@ -1008,6 +1101,8 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'income_services',
description_sv: 'Tjänsteförsäljning till EU-företag (momsfritt, ruta 39)',
common: true,
requires_vat_registration_data: true,
},
{
id: 'revenue_export',
@@ -1031,6 +1126,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'income_services',
description_sv: 'Export av varor/tjänster utanför EU (momsfritt, ruta 40)',
common: true,
},
// --- FINANCIAL (2) ---
@@ -1055,6 +1151,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_other',
description_sv: 'Amortering av banklån',
common: false,
},
{
id: 'financial_tax_account',
@@ -1077,9 +1174,10 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_other',
description_sv: 'Insättning på skattekonto hos Skatteverket',
common: true,
},
// --- PRIVATE TRANSFERS (4) ---
// --- PRIVATE TRANSFERS (5) ---
{
id: 'private_withdrawal_ef',
name_sv: 'Eget uttag (EF)',
@@ -1101,6 +1199,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: true,
fallback_category: 'private',
description_sv: 'Privat uttag från företagskonto (enskild firma)',
common: true,
},
{
id: 'private_deposit_ef',
@@ -1123,28 +1222,53 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: true,
fallback_category: 'private',
description_sv: 'Egen insättning till företagskonto (enskild firma)',
common: true,
},
{
id: 'private_shareholder_loan',
name_sv: 'Skuld till aktieägare (AB)',
name_en: 'Shareholder loan (AB)',
id: 'shareholder_loan_received',
name_sv: 'Lån från ägare (AB)',
name_en: 'Shareholder loan received (AB)',
group: 'private_transfers',
direction: 'transfer',
entity_applicability: 'aktiebolag',
debit_account: '2893',
debit_account: '1930',
credit_account: '2393',
vat_treatment: null,
vat_rate: 0,
deductibility: 'non_deductible',
mcc_codes: [],
keywords: ['aktieägare', 'lån', 'shareholder', 'skuld till ägare', 'insättning', 'tillskott'],
risk_level: 'LOW',
requires_review: true,
impact_score: 6,
auto_match_confidence: 0.75,
default_private: false,
fallback_category: 'income_other',
description_sv: 'Ägare lånar pengar till bolaget (skuld till ägare)',
common: true,
},
{
id: 'shareholder_loan_disbursed',
name_sv: 'Fordran på ägare (AB)',
name_en: 'Shareholder loan disbursed (AB)',
group: 'private_transfers',
direction: 'transfer',
entity_applicability: 'aktiebolag',
debit_account: '1680',
credit_account: '1930',
vat_treatment: null,
vat_rate: 0,
deductibility: 'non_deductible',
mcc_codes: [],
keywords: ['aktieägare', 'lån', 'shareholder', 'skuld till ägare'],
risk_level: 'LOW',
keywords: ['aktieägare', 'lån till ägare', 'shareholder loan', 'fordran ägare'],
risk_level: 'HIGH',
requires_review: true,
impact_score: 6,
auto_match_confidence: 0.75,
auto_match_confidence: 0.70,
default_private: true,
fallback_category: 'private',
description_sv: 'Utbetalning bokförd som skuld till aktieägare',
description_sv: 'Bolaget betalar ut till ägare (fordran på ägare)',
common: false,
},
{
id: 'private_expense',
@@ -1168,6 +1292,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: true,
fallback_category: 'private',
description_sv: 'Privat kostnad betald från företagskonto',
common: false,
},
// --- EQUIPMENT (2) ---
@@ -1193,6 +1318,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_equipment',
description_sv: 'Inventarier under halva prisbasbeloppet (IT-utrustning, möbler, verktyg)',
common: true,
},
{
id: 'equipment_capital',
@@ -1216,6 +1342,7 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [
default_private: false,
fallback_category: 'expense_equipment',
description_sv: 'Inventarie som ska aktiveras och skrivas av',
common: false,
},
]
@@ -1301,6 +1428,51 @@ export function searchTemplates(query: string, entityType?: EntityType): Booking
})
}
/**
* Get common templates, filtered by entity type and direction.
*/
export function getCommonTemplates(
entityType?: EntityType,
direction?: 'expense' | 'income' | 'transfer'
): BookingTemplate[] {
return BOOKING_TEMPLATES.filter((t) => {
if (!t.common) return false
if (entityType && t.entity_applicability !== 'all' && t.entity_applicability !== entityType) return false
if (direction && t.direction !== direction) return false
return true
})
}
/**
* Get advanced (non-common) templates, filtered by entity type and direction.
*/
export function getAdvancedTemplates(
entityType?: EntityType,
direction?: 'expense' | 'income' | 'transfer'
): BookingTemplate[] {
return BOOKING_TEMPLATES.filter((t) => {
if (t.common) return false
if (entityType && t.entity_applicability !== 'all' && t.entity_applicability !== entityType) return false
if (direction && t.direction !== direction) return false
return true
})
}
/**
* Validate that a template is valid for the given entity type.
*/
export function validateTemplateForEntity(
template: BookingTemplate,
entityType: EntityType
): { valid: boolean; error?: string } {
if (template.entity_applicability === 'all') return { valid: true }
if (template.entity_applicability === entityType) return { valid: true }
return {
valid: false,
error: `Template "${template.name_sv}" is only valid for ${template.entity_applicability}. Your entity type is ${entityType}.`,
}
}
/**
* Multi-signal matching against a transaction.
* Returns top matches sorted by confidence descending.
+21 -21
View File
@@ -7,52 +7,52 @@
const ACCOUNT_NAMES: Record<string, string> = {
// Assets (1xxx)
'1510': 'Kundfordringar',
'1930': 'Foretagskonto',
'1930': 'Företagskonto',
// Equity & Liabilities (2xxx)
'2013': 'Ovriga egna uttag',
'2018': 'Egna insattningar',
'2440': 'Leverantorsskulder',
'2013': 'Övriga egna uttag',
'2018': 'Egna insättningar',
'2440': 'Leverantörsskulder',
'2611': 'Utg. moms 25%',
'2621': 'Utg. moms 12%',
'2631': 'Utg. moms 6%',
'2614': 'Utg. moms omvand',
'2614': 'Utg. moms omvänd',
'2641': 'Ing. moms',
'2645': 'Beraknad ing. moms',
'2893': 'Skuld till agare',
'2645': 'Beräknad ing. moms',
'2893': 'Skuld till ägare',
// Revenue (3xxx)
'3001': 'Forsaljning 25%',
'3002': 'Forsaljning 12%',
'3003': 'Forsaljning 6%',
'3004': 'Momsfri forsaljning',
'3305': 'Exportforsaljning',
'3308': 'EU-tjanster',
'3900': 'Ovriga rorelseintakter',
'3001': 'Försäljning 25%',
'3002': 'Försäljning 12%',
'3003': 'Försäljning 6%',
'3004': 'Momsfri försäljning',
'3305': 'Exportförsäljning',
'3308': 'EU-tjänster',
'3900': 'Övriga rörelseintäkter',
// Cost of goods (4xxx)
'4010': 'Varuinkop',
'4010': 'Varuinköp',
// External expenses (5xxx)
'5010': 'Lokalhyra',
'5410': 'Forbrukningsinventarier',
'5410': 'Förbrukningsinventarier',
'5420': 'Programvaror',
'5460': 'Forbrukningsvaror',
'5460': 'Förbrukningsvaror',
'5611': 'Drivmedel bil',
'5800': 'Resekostnader',
'5910': 'Annonsering',
// Other external expenses (6xxx)
'6071': 'Representation',
'6110': 'Kontorsforbrukning',
'6110': 'Kontorsförbrukning',
'6200': 'Telefon & internet',
'6530': 'Redovisningstjanster',
'6530': 'Redovisningstjänster',
'6570': 'Bankavgifter',
'6991': 'Ovriga kostnader',
'6991': 'Övriga kostnader',
// Personnel (7xxx)
'7610': 'Utbildning',
'7960': 'Valutakursforluster',
'7960': 'Valutakursförluster',
'3960': 'Valutakursvinster',
}
+317
View File
@@ -0,0 +1,317 @@
import type { SupabaseClient } from '@supabase/supabase-js'
import { fetchMultipleRates } from '@/lib/currency/riksbanken'
import { createJournalEntry } from '@/lib/bookkeeping/engine'
import type {
Currency,
Invoice,
SupplierInvoice,
RevaluationItem,
CurrencyRevaluationPreview,
CurrencyRevaluationResult,
CreateJournalEntryLineInput,
} from '@/types'
/**
* Fetch open foreign-currency receivables (invoices).
* Returns invoices with status 'sent' or 'overdue', non-SEK currency,
* and a known exchange rate.
*/
export async function getOpenForeignCurrencyReceivables(
supabase: SupabaseClient,
userId: string
): Promise<Invoice[]> {
const { data, error } = await supabase
.from('invoices')
.select('*')
.eq('user_id', userId)
.in('status', ['sent', 'overdue'])
.neq('currency', 'SEK')
.not('exchange_rate', 'is', null)
if (error) {
throw new Error(`Failed to fetch foreign currency receivables: ${error.message}`)
}
return (data || []) as Invoice[]
}
/**
* Fetch open foreign-currency payables (supplier invoices).
* Returns supplier invoices with open statuses, non-SEK currency,
* and a known exchange rate. Uses remaining_amount for partial payments.
*/
export async function getOpenForeignCurrencyPayables(
supabase: SupabaseClient,
userId: string
): Promise<SupplierInvoice[]> {
const { data, error } = await supabase
.from('supplier_invoices')
.select('*')
.eq('user_id', userId)
.in('status', ['registered', 'approved', 'overdue', 'partially_paid'])
.neq('currency', 'SEK')
.not('exchange_rate', 'is', null)
if (error) {
throw new Error(`Failed to fetch foreign currency payables: ${error.message}`)
}
return (data || []) as SupplierInvoice[]
}
/**
* Preview currency revaluation without persisting.
* Computes per-item differences and aggregated journal lines.
*
* Receivables (1510):
* closing > original → gain: Debit 1510, Credit 3960
* closing < original → loss: Credit 1510, Debit 7960
*
* Payables (2440):
* closing > original → loss (liability grew): Debit 7960, Credit 2440
* closing < original → gain (liability shrank): Debit 2440, Credit 3960
*/
export async function previewCurrencyRevaluation(
supabase: SupabaseClient,
userId: string,
closingDate: string
): Promise<CurrencyRevaluationPreview> {
const [receivables, payables] = await Promise.all([
getOpenForeignCurrencyReceivables(supabase, userId),
getOpenForeignCurrencyPayables(supabase, userId),
])
// Collect distinct currencies
const currencies = new Set<Currency>()
for (const inv of receivables) {
currencies.add(inv.currency)
}
for (const si of payables) {
currencies.add(si.currency as Currency)
}
if (currencies.size === 0) {
return {
items: [],
lines: [],
closingRates: {},
totalGain: 0,
totalLoss: 0,
netEffect: 0,
}
}
// Fetch closing rates
const rateMap = await fetchMultipleRates(
Array.from(currencies),
new Date(closingDate)
)
const closingRates: Record<string, number> = {}
for (const [currency, rate] of rateMap) {
closingRates[currency] = rate.rate
}
const items: RevaluationItem[] = []
// Process receivables
for (const inv of receivables) {
const closingRate = rateMap.get(inv.currency)?.rate
if (!closingRate || !inv.exchange_rate) continue
const amountInCurrency = inv.total
const originalSek = Math.round(amountInCurrency * inv.exchange_rate * 100) / 100
const closingSek = Math.round(amountInCurrency * closingRate * 100) / 100
const difference = Math.round((closingSek - originalSek) * 100) / 100
if (Math.abs(difference) < 0.01) continue
items.push({
type: 'receivable',
source_id: inv.id,
reference: inv.invoice_number,
currency: inv.currency,
amount_in_currency: amountInCurrency,
original_rate: inv.exchange_rate,
closing_rate: closingRate,
original_sek: originalSek,
closing_sek: closingSek,
difference_sek: difference,
})
}
// Process payables (use remaining_amount for partial payments)
for (const si of payables) {
const closingRate = rateMap.get(si.currency as Currency)?.rate
if (!closingRate || !si.exchange_rate) continue
const amountInCurrency = si.remaining_amount
if (amountInCurrency <= 0) continue
const originalSek = Math.round(amountInCurrency * si.exchange_rate * 100) / 100
const closingSek = Math.round(amountInCurrency * closingRate * 100) / 100
const difference = Math.round((closingSek - originalSek) * 100) / 100
if (Math.abs(difference) < 0.01) continue
items.push({
type: 'payable',
source_id: si.id,
reference: si.supplier_invoice_number,
currency: si.currency as Currency,
amount_in_currency: amountInCurrency,
original_rate: si.exchange_rate,
closing_rate: closingRate,
original_sek: originalSek,
closing_sek: closingSek,
difference_sek: difference,
})
}
// Build aggregated journal lines
let debit1510 = 0 // Receivable gain (revalue up)
let credit1510 = 0 // Receivable loss (revalue down)
let debit2440 = 0 // Payable gain (liability shrank)
let credit2440 = 0 // Payable loss (liability grew)
let debit3960 = 0 // Placeholder — we won't debit 3960
let credit3960 = 0 // Gains
let debit7960 = 0 // Losses
let credit7960 = 0 // Placeholder — we won't credit 7960
for (const item of items) {
if (item.type === 'receivable') {
if (item.difference_sek > 0) {
// Closing > original → gain: Debit 1510, Credit 3960
debit1510 += item.difference_sek
credit3960 += item.difference_sek
} else {
// Closing < original → loss: Credit 1510, Debit 7960
credit1510 += Math.abs(item.difference_sek)
debit7960 += Math.abs(item.difference_sek)
}
} else {
// Payable
if (item.difference_sek > 0) {
// Closing > original → loss (liability grew): Debit 7960, Credit 2440
debit7960 += item.difference_sek
credit2440 += item.difference_sek
} else {
// Closing < original → gain (liability shrank): Debit 2440, Credit 3960
debit2440 += Math.abs(item.difference_sek)
credit3960 += Math.abs(item.difference_sek)
}
}
}
const lines: CreateJournalEntryLineInput[] = []
if (debit1510 > 0) {
lines.push({
account_number: '1510',
debit_amount: Math.round(debit1510 * 100) / 100,
credit_amount: 0,
line_description: 'Omvärdering kundfordringar — orealiserad kursvinst',
})
}
if (credit1510 > 0) {
lines.push({
account_number: '1510',
debit_amount: 0,
credit_amount: Math.round(credit1510 * 100) / 100,
line_description: 'Omvärdering kundfordringar — orealiserad kursförlust',
})
}
if (debit2440 > 0) {
lines.push({
account_number: '2440',
debit_amount: Math.round(debit2440 * 100) / 100,
credit_amount: 0,
line_description: 'Omvärdering leverantörsskulder — orealiserad kursvinst',
})
}
if (credit2440 > 0) {
lines.push({
account_number: '2440',
debit_amount: 0,
credit_amount: Math.round(credit2440 * 100) / 100,
line_description: 'Omvärdering leverantörsskulder — orealiserad kursförlust',
})
}
if (credit3960 > 0) {
lines.push({
account_number: '3960',
debit_amount: 0,
credit_amount: Math.round(credit3960 * 100) / 100,
line_description: 'Orealiserade valutakursvinster',
})
}
if (debit7960 > 0) {
lines.push({
account_number: '7960',
debit_amount: Math.round(debit7960 * 100) / 100,
credit_amount: 0,
line_description: 'Orealiserade valutakursförluster',
})
}
const totalGain = Math.round(credit3960 * 100) / 100
const totalLoss = Math.round(debit7960 * 100) / 100
const netEffect = Math.round((totalGain - totalLoss) * 100) / 100
return {
items,
lines,
closingRates,
totalGain,
totalLoss,
netEffect,
}
}
/**
* Execute currency revaluation for a fiscal period.
* Creates a journal entry with source_type 'currency_revaluation'.
*
* Returns null if no foreign-currency items exist.
* Throws if a revaluation entry already exists for this period (idempotency).
*/
export async function executeCurrencyRevaluation(
supabase: SupabaseClient,
userId: string,
closingDate: string,
fiscalPeriodId: string
): Promise<CurrencyRevaluationResult | null> {
// Idempotency check: prevent double revaluation
const { count, error: checkError } = await supabase
.from('journal_entries')
.select('id', { count: 'exact', head: true })
.eq('user_id', userId)
.eq('fiscal_period_id', fiscalPeriodId)
.eq('source_type', 'currency_revaluation')
.eq('status', 'posted')
if (checkError) {
throw new Error(`Failed to check existing revaluation: ${checkError.message}`)
}
if ((count ?? 0) > 0) {
throw new Error('Currency revaluation already exists for this period')
}
const preview = await previewCurrencyRevaluation(supabase, userId, closingDate)
if (preview.items.length === 0 || preview.lines.length === 0) {
return null
}
const entry = await createJournalEntry(supabase, userId, {
fiscal_period_id: fiscalPeriodId,
entry_date: closingDate,
description: `Omvärdering utländsk valuta ${closingDate}`,
source_type: 'currency_revaluation',
voucher_series: 'A',
lines: preview.lines,
})
return { entry, preview }
}
@@ -11,7 +11,7 @@ let results: Array<{ data?: unknown; error?: unknown; count?: number | null }>
function makeBuilder() {
const b: Record<string, unknown> = {}
for (const m of ['select', 'eq', 'insert', 'update', 'delete', 'lte', 'gte', 'in', 'not', 'or', 'order', 'limit', 'is']) {
for (const m of ['select', 'eq', 'insert', 'update', 'delete', 'lte', 'gte', 'in', 'neq', 'not', 'or', 'order', 'limit', 'is']) {
b[m] = vi.fn().mockReturnValue(b)
}
b.single = vi.fn().mockImplementation(async () => results[resultIdx++] ?? { data: null, error: null })
@@ -39,6 +39,18 @@ vi.mock('@/lib/bookkeeping/engine', () => ({
createJournalEntry: vi.fn(),
}))
vi.mock('@/lib/bookkeeping/currency-revaluation', () => ({
previewCurrencyRevaluation: vi.fn().mockResolvedValue({
items: [],
lines: [],
closingRates: {},
totalGain: 0,
totalLoss: 0,
netEffect: 0,
}),
executeCurrencyRevaluation: vi.fn().mockResolvedValue(null),
}))
vi.mock('../period-service', () => ({
lockPeriod: vi.fn(),
closePeriod: vi.fn(),
@@ -67,6 +79,12 @@ describe('validateYearEndReadiness', () => {
{ data: null, error: null, count: 3 },
// 2: count posted entries (thenable chain) — count: 10
{ data: null, error: null, count: 10 },
// 3: count revaluation entries — count: 0
{ data: null, error: null, count: 0 },
// 4: count fx receivables — count: 0
{ data: null, error: null, count: 0 },
// 5: count fx payables — count: 0
{ data: null, error: null, count: 0 },
]
vi.mocked(generateTrialBalance).mockResolvedValue({
@@ -89,6 +107,9 @@ describe('validateYearEndReadiness', () => {
{ data: period, error: null },
{ data: null, error: null, count: 0 }, // no drafts
{ data: null, error: null, count: 5 }, // some posted
{ data: null, error: null, count: 0 }, // no revaluation
{ data: null, error: null, count: 0 }, // no fx receivables
{ data: null, error: null, count: 0 }, // no fx payables
]
vi.mocked(generateTrialBalance).mockResolvedValue({
@@ -121,8 +142,11 @@ describe('validateYearEndReadiness', () => {
resultIdx = 0
results = [
{ data: period, error: null },
{ data: null, error: null, count: 0 },
{ data: null, error: null, count: 5 },
{ data: null, error: null, count: 0 }, // no drafts
{ data: null, error: null, count: 5 }, // some posted
{ data: null, error: null, count: 0 }, // no revaluation
{ data: null, error: null, count: 0 }, // no fx receivables
{ data: null, error: null, count: 0 }, // no fx payables
]
vi.mocked(generateTrialBalance).mockResolvedValue({
@@ -143,6 +167,8 @@ describe('previewYearEndClosing', () => {
results = [
// 0: fetch company_settings (.single)
{ data: { entity_type: 'aktiebolag' }, error: null },
// 1: fetch fiscal period for closing date (.single)
{ data: { period_end: '2024-12-31' }, error: null },
]
vi.mocked(generateIncomeStatement).mockResolvedValue({
@@ -173,6 +199,8 @@ describe('previewYearEndClosing', () => {
it('uses 2010 for EF entity type', async () => {
results = [
{ data: { entity_type: 'enskild_firma' }, error: null },
// fetch fiscal period for closing date (.single)
{ data: { period_end: '2024-12-31' }, error: null },
]
vi.mocked(generateIncomeStatement).mockResolvedValue({ net_result: 50000 } as never)
+77 -7
View File
@@ -4,6 +4,10 @@ import { createJournalEntry } from '@/lib/bookkeeping/engine'
import { generateTrialBalance } from '@/lib/reports/trial-balance'
import { generateIncomeStatement } from '@/lib/reports/income-statement'
import { lockPeriod, closePeriod, createNextPeriod } from './period-service'
import {
previewCurrencyRevaluation,
executeCurrencyRevaluation,
} from '@/lib/bookkeeping/currency-revaluation'
import type {
YearEndValidation,
YearEndPreview,
@@ -105,6 +109,40 @@ export async function validateYearEndReadiness(
warnings.push('No posted journal entries in this period')
}
// Check: foreign currency items exist but haven't been revalued
const { count: revalCount } = await supabase
.from('journal_entries')
.select('id', { count: 'exact', head: true })
.eq('user_id', userId)
.eq('fiscal_period_id', fiscalPeriodId)
.eq('source_type', 'currency_revaluation')
.eq('status', 'posted')
if ((revalCount ?? 0) === 0) {
// Check if there are any open foreign currency items
const { count: fxReceivables } = await supabase
.from('invoices')
.select('id', { count: 'exact', head: true })
.eq('user_id', userId)
.in('status', ['sent', 'overdue'])
.neq('currency', 'SEK')
.not('exchange_rate', 'is', null)
const { count: fxPayables } = await supabase
.from('supplier_invoices')
.select('id', { count: 'exact', head: true })
.eq('user_id', userId)
.in('status', ['registered', 'approved', 'overdue', 'partially_paid'])
.neq('currency', 'SEK')
.not('exchange_rate', 'is', null)
if (((fxReceivables ?? 0) + (fxPayables ?? 0)) > 0) {
warnings.push(
'Open foreign currency items exist but have not been revalued (ÅRL 4:13)'
)
}
}
return {
ready: errors.length === 0,
errors,
@@ -212,12 +250,33 @@ export async function previewYearEndClosing(
}
}
// Fetch fiscal period for closing date
const { data: periodData } = await supabase
.from('fiscal_periods')
.select('period_end')
.eq('id', fiscalPeriodId)
.eq('user_id', userId)
.single()
let currencyRevaluation = null
if (periodData) {
const revalPreview = await previewCurrencyRevaluation(
supabase,
userId,
periodData.period_end
)
if (revalPreview.items.length > 0) {
currencyRevaluation = revalPreview
}
}
return {
netResult,
closingAccount,
closingAccountName,
closingLines,
resultAccountSummary,
currencyRevaluation,
}
}
@@ -255,14 +314,24 @@ export async function executeYearEndClosing(
throw new Error('Fiscal period not found')
}
// 2. Get closing preview
// 2. Execute currency revaluation BEFORE closing entry
// Revaluation posts to 3960/7960 (class 3/7 result accounts) which
// the closing entry then zeros out.
const revaluationResult = await executeCurrencyRevaluation(
supabase,
userId,
period.period_end,
fiscalPeriodId
)
// 3. Get closing preview (now includes revaluation effects in trial balance)
const preview = await previewYearEndClosing(supabase, userId, fiscalPeriodId)
if (preview.closingLines.length === 0) {
throw new Error('No result accounts to close — period has no activity')
}
// 3. Create closing entry via the journal engine
// 4. Create closing entry via the journal engine
const closingEntry = await createJournalEntry(supabase, userId, {
fiscal_period_id: fiscalPeriodId,
entry_date: period.period_end,
@@ -272,7 +341,7 @@ export async function executeYearEndClosing(
lines: preview.closingLines,
})
// 4. Update fiscal period with closing_entry_id
// 5. Update fiscal period with closing_entry_id
const { error: updateError } = await supabase
.from('fiscal_periods')
.update({ closing_entry_id: closingEntry.id })
@@ -283,16 +352,16 @@ export async function executeYearEndClosing(
throw new Error(`Failed to set closing_entry_id: ${updateError.message}`)
}
// 5. Lock the period
// 6. Lock the period
await lockPeriod(supabase, userId, fiscalPeriodId)
// 6. Close the period
// 7. Close the period
await closePeriod(supabase, userId, fiscalPeriodId)
// 7. Create next period
// 8. Create next period
const nextPeriod = await createNextPeriod(supabase, userId, fiscalPeriodId)
// 8. Generate opening balances in next period
// 9. Generate opening balances in next period
const openingBalanceEntry = await generateOpeningBalances(
supabase,
userId,
@@ -319,6 +388,7 @@ export async function executeYearEndClosing(
closingEntry,
nextPeriod,
openingBalanceEntry,
revaluationEntry: revaluationResult?.entry ?? null,
}
}
@@ -2,7 +2,6 @@
export const ENABLED_EXTENSION_IDS: ReadonlySet<string> = new Set([
'enable-banking',
'ai-categorization',
'ai-chat',
'email',
])
@@ -1,13 +1,11 @@
// AUTO-GENERATED — do not edit. Run `npm run setup:extensions` to regenerate.
import type { Extension } from '../types'
import { enableBankingExtension } from '@/extensions/general/enable-banking'
import { aiCategorizationExtension } from '@/extensions/general/ai-categorization'
import { aiChatExtension } from '@/extensions/general/ai-chat'
import { emailExtension } from '@/extensions/general/email'
export const FIRST_PARTY_EXTENSIONS: Extension[] = [
enableBankingExtension,
aiCategorizationExtension,
aiChatExtension,
emailExtension,
]
@@ -15,19 +15,6 @@ export const EXTENSION_DEFINITIONS: Record<string, ExtensionDefinition[]> = {
"hasOwnData": true,
"subscriptionNotice": "Denna integration kräver ett aktivt Enable Banking-abonnemang. Utan abonnemang kommer bankintegration inte att fungera."
},
{
"slug": "ai-categorization",
"name": "AI-kategorisering",
"sector": "general",
"category": "operations",
"icon": "Sparkles",
"dataPattern": "core",
"description": "AI-drivna kategoriförslag för transaktioner",
"longDescription": "Använder AI för att automatiskt föreslå BAS-kontokategorier för dina banktransaktioner. Lär sig från dina tidigare bokföringsval.",
"readsCoreTables": [
"transactions"
]
},
{
"slug": "ai-chat",
"name": "AI-assistent",
@@ -5,6 +5,5 @@ import type { WorkspaceComponentProps } from '../workspace-registry'
export const WORKSPACES: Record<string, ComponentType<WorkspaceComponentProps>> = {
'general/enable-banking': dynamic(() => import('@/components/extensions/general/EnableBankingWorkspace')),
'general/ai-categorization': dynamic(() => import('@/components/extensions/general/AiCategorizationWorkspace')),
'general/ai-chat': dynamic(() => import('@/components/extensions/general/AiChatWorkspace')),
}
-80
View File
@@ -1,80 +0,0 @@
'use client'
import { useState, useEffect, useCallback, useRef } from 'react'
interface AccountTotal {
account_number: string
debit: number
credit: number
net: number
}
interface MonthlyTotal {
month: string
account_number: string
debit: number
credit: number
net: number
}
interface UseAccountTotalsOptions {
from: string
to: string
dateFrom?: string
dateTo?: string
groupBy?: 'month'
}
export function useAccountTotals(options: UseAccountTotalsOptions) {
const [totals, setTotals] = useState<AccountTotal[]>([])
const [monthly, setMonthly] = useState<MonthlyTotal[]>([])
const [isLoading, setIsLoading] = useState(true)
const mountedRef = useRef(true)
useEffect(() => {
mountedRef.current = true
return () => { mountedRef.current = false }
}, [])
const refresh = useCallback(async () => {
setIsLoading(true)
try {
const params = new URLSearchParams({
from: options.from,
to: options.to,
})
if (options.dateFrom) params.set('date_from', options.dateFrom)
if (options.dateTo) params.set('date_to', options.dateTo)
if (options.groupBy) params.set('group_by', options.groupBy)
const res = await fetch(`/api/bookkeeping/account-totals?${params}`)
if (res.ok) {
const json = await res.json()
if (mountedRef.current) {
setTotals(json.totals ?? [])
setMonthly(json.monthly ?? [])
}
}
} finally {
if (mountedRef.current) setIsLoading(false)
}
}, [options.from, options.to, options.dateFrom, options.dateTo, options.groupBy])
useEffect(() => {
refresh()
}, [refresh])
const totalDebit = totals.reduce((sum, t) => sum + t.debit, 0)
const totalCredit = totals.reduce((sum, t) => sum + t.credit, 0)
const totalNet = totals.reduce((sum, t) => sum + t.net, 0)
return {
totals,
monthly,
isLoading,
totalDebit: Math.round(totalDebit * 100) / 100,
totalCredit: Math.round(totalCredit * 100) / 100,
totalNet: Math.round(totalNet * 100) / 100,
refresh,
}
}
-85
View File
@@ -1,85 +0,0 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { useExtensionData } from './use-extension-data'
interface MockMeta {
importedAt: string
source: 'csv' | 'json'
fileName: string
rowCount: number
}
interface UseMockDataResult<T> {
mockReport: T | null
isMockActive: boolean
isLoading: boolean
importedAt: string | null
meta: MockMeta | null
saveMockData: (report: T, meta: Omit<MockMeta, 'importedAt'>) => Promise<void>
clearMockData: () => Promise<void>
}
export function useMockData<T>(sector: string, slug: string): UseMockDataResult<T> {
const { getByKey, save, remove, isLoading } = useExtensionData(sector, slug)
const [mockReport, setMockReport] = useState<T | null>(null)
const [isMockActive, setIsMockActive] = useState(false)
const [meta, setMeta] = useState<MockMeta | null>(null)
// Read mock state from extension data on load
useEffect(() => {
if (isLoading) return
const enabledRecord = getByKey('mock:enabled')
const reportRecord = getByKey('mock:report')
const metaRecord = getByKey('mock:meta')
if (enabledRecord && (enabledRecord.value as { enabled?: boolean }).enabled && reportRecord) {
setIsMockActive(true)
setMockReport(reportRecord.value as T)
if (metaRecord) {
setMeta(metaRecord.value as unknown as MockMeta)
}
} else {
setIsMockActive(false)
setMockReport(null)
setMeta(null)
}
}, [isLoading, getByKey])
const saveMockData = useCallback(async (report: T, metaInput: Omit<MockMeta, 'importedAt'>) => {
const fullMeta: MockMeta = {
...metaInput,
importedAt: new Date().toISOString(),
}
await save('mock:enabled', { enabled: true })
await save('mock:report', report as unknown as Record<string, unknown>)
await save('mock:meta', fullMeta as unknown as Record<string, unknown>)
setIsMockActive(true)
setMockReport(report)
setMeta(fullMeta)
}, [save])
const clearMockData = useCallback(async () => {
await remove('mock:enabled')
await remove('mock:report')
await remove('mock:meta')
setIsMockActive(false)
setMockReport(null)
setMeta(null)
}, [remove])
return {
mockReport,
isMockActive,
isLoading,
importedAt: meta?.importedAt ?? null,
meta,
saveMockData,
clearMockData,
}
}
-148
View File
@@ -1,148 +0,0 @@
import type { SupabaseClient } from '@supabase/supabase-js'
import { fetchAllRows } from '@/lib/supabase/fetch-all'
import type { JournalEntry, JournalEntryLine } from '@/types'
/**
* SRU aggregation engine
*
* Fetches posted journal entries for a fiscal period, computes net balance
* per account, and groups by sru_code from chart_of_accounts.
*/
export interface SRUBalance {
sruCode: string
amount: number
accounts: Array<{
accountNumber: string
accountName: string
amount: number
}>
}
export interface SRUCoverageStats {
totalAccounts: number
accountsWithSRU: number
accountsWithoutSRU: number
coveragePercent: number
missingAccounts: Array<{
accountNumber: string
accountName: string
}>
}
/**
* Aggregate account balances by SRU code for a given fiscal period.
* Returns a Map of sru_code → summed amount, plus per-account detail.
*/
export async function aggregateBalancesBySRU(
supabase: SupabaseClient,
userId: string,
fiscalPeriodId: string
): Promise<Map<string, SRUBalance>> {
// Fetch all posted journal entries with lines for this period
const { data: entries, error: entriesError } = await supabase
.from('journal_entries')
.select('*, lines:journal_entry_lines(*)')
.eq('user_id', userId)
.eq('fiscal_period_id', fiscalPeriodId)
.eq('status', 'posted')
if (entriesError) {
throw new Error(`Failed to fetch journal entries: ${entriesError.message}`)
}
// Fetch chart of accounts with SRU codes
const accounts = await fetchAllRows<{ account_number: string; account_name: string; sru_code: string | null; normal_balance: string }>(({ from, to }) =>
supabase
.from('chart_of_accounts')
.select('account_number, account_name, sru_code, normal_balance')
.eq('user_id', userId)
.eq('is_active', true)
.range(from, to)
)
// Build lookup maps
const accountSRUMap = new Map<string, string>()
const accountNameMap = new Map<string, string>()
for (const acc of accounts) {
if (acc.sru_code) {
accountSRUMap.set(acc.account_number, acc.sru_code)
}
accountNameMap.set(acc.account_number, acc.account_name)
}
// Calculate net balances per account (debit - credit)
const accountBalances = new Map<string, number>()
for (const entry of (entries as JournalEntry[]) || []) {
const lines = (entry.lines as JournalEntryLine[]) || []
for (const line of lines) {
const current = accountBalances.get(line.account_number) || 0
const netAmount = (Number(line.debit_amount) || 0) - (Number(line.credit_amount) || 0)
accountBalances.set(line.account_number, current + netAmount)
}
}
// Group balances by SRU code
const sruBalances = new Map<string, SRUBalance>()
for (const [accountNumber, balance] of accountBalances) {
if (Math.abs(balance) < 0.01) continue
const sruCode = accountSRUMap.get(accountNumber)
if (!sruCode) continue
let entry = sruBalances.get(sruCode)
if (!entry) {
entry = { sruCode, amount: 0, accounts: [] }
sruBalances.set(sruCode, entry)
}
entry.amount += balance
entry.accounts.push({
accountNumber,
accountName: accountNameMap.get(accountNumber) || `Konto ${accountNumber}`,
amount: Math.round(balance),
})
}
// Round totals
for (const entry of sruBalances.values()) {
entry.amount = Math.round(entry.amount)
}
return sruBalances
}
/**
* Get SRU code coverage stats for a user's chart of accounts.
* Returns how many accounts have vs lack SRU codes.
*/
export async function getSRUCoverage(supabase: SupabaseClient, userId: string): Promise<SRUCoverageStats> {
const accounts = await fetchAllRows<{ account_number: string; account_name: string; sru_code: string | null }>(({ from, to }) =>
supabase
.from('chart_of_accounts')
.select('account_number, account_name, sru_code')
.eq('user_id', userId)
.eq('is_active', true)
.order('account_number')
.range(from, to)
)
const withSRU = accounts.filter((a) => a.sru_code)
const withoutSRU = accounts.filter((a) => !a.sru_code)
return {
totalAccounts: accounts.length,
accountsWithSRU: withSRU.length,
accountsWithoutSRU: withoutSRU.length,
coveragePercent: accounts.length > 0
? Math.round((withSRU.length / accounts.length) * 100)
: 0,
missingAccounts: withoutSRU.map((a) => ({
accountNumber: a.account_number,
accountName: a.account_name,
})),
}
}
@@ -1,140 +0,0 @@
import type { SRUFile, SRURecord } from '@/lib/reports/ne-bilaga/types'
import { sruFileToString, validateSRUFile } from './sru-generator'
import type { SRUBalance } from './sru-engine'
/**
* Generic SRU file generator
*
* Generates SRU files from aggregated SRU balances for any form type
* (NE for enskild firma, INK2 for aktiebolag).
*
* Reuses sruFileToString() and validateSRUFile() from the existing
* NE-specific generator.
*/
export type SRUFormType = 'NE' | 'INK2'
export interface GenericSRUParams {
formType: SRUFormType
orgNumber: string | null
companyName: string
fiscalYearStart: string // YYYY-MM-DD
fiscalYearEnd: string // YYYY-MM-DD
sruBalances: Map<string, SRUBalance>
}
/**
* SRU code descriptions for display
*/
export const SRU_CODE_DESCRIPTIONS: Record<string, string> = {
// NE form (EF)
'7310': 'Försäljning med moms',
'7311': 'Momsfria intäkter',
'7312': 'Bil/bostadsförmån',
'7313': 'Ränteintäkter',
'7320': 'Varuinköp',
'7321': 'Övriga kostnader',
'7322': 'Lönekostnader',
'7323': 'Räntekostnader',
'7324': 'Avskrivningar fastighet',
'7325': 'Avskrivningar övrigt',
'7350': 'Årets resultat',
// INK2 form (AB) — balance sheet
'7201': 'Immateriella anläggningstillgångar',
'7202': 'Materiella anläggningstillgångar',
'7203': 'Finansiella anläggningstillgångar',
'7210': 'Varulager',
'7211': 'Kundfordringar',
'7212': 'Övriga omsättningstillgångar',
'7220': 'Aktiekapital',
'7221': 'Övrigt eget kapital',
'7222': 'Årets resultat',
'7230': 'Skulder',
'7231': 'Övriga skulder',
// INK2 form (AB) — income statement
'7330': 'Övriga externa kostnader',
'7340': 'Personalkostnader',
'7360': 'Övriga rörelsekostnader',
'7370': 'Finansiella poster',
'7380': 'Extraordinära poster',
}
/**
* Generate a generic SRU file from aggregated SRU balances.
*/
export function generateGenericSRU(params: GenericSRUParams): SRUFile {
const { formType, orgNumber, fiscalYearStart, fiscalYearEnd, sruBalances } = params
const records: SRURecord[] = []
const now = new Date()
// File header
records.push({ fieldCode: 'PRODUKT', value: 'KONTROLLUPPGIFTER' })
records.push({ fieldCode: 'SESSION', value: '1' })
records.push({ fieldCode: 'PROGRAMNAMN', value: 'ERPBase' })
records.push({ fieldCode: 'PROGRAMVERSION', value: '1.0' })
records.push({ fieldCode: 'SKAPAT', value: formatSRUDate(now) })
// Form declaration
records.push({ fieldCode: 'BLANKETT', value: formType })
// Company identification
if (orgNumber) {
const cleanOrgNumber = orgNumber.replace(/-/g, '')
records.push({ fieldCode: 'IDENTITET', value: cleanOrgNumber })
}
// Fiscal year
const startSRU = fiscalYearStart.replace(/-/g, '')
const endSRU = fiscalYearEnd.replace(/-/g, '')
records.push({
fieldCode: 'UPPGIFT',
value: `7000 ${startSRU}-${endSRU}`,
})
// SRU balance entries — one #UPPGIFT per non-zero SRU code
const sortedEntries = Array.from(sruBalances.entries())
.sort(([a], [b]) => a.localeCompare(b))
for (const [sruCode, balance] of sortedEntries) {
if (balance.amount !== 0) {
records.push({
fieldCode: 'UPPGIFT',
value: `${sruCode} ${Math.round(balance.amount)}`,
})
}
}
// End of form
records.push({ fieldCode: 'BLANKETTSLUT', value: '' })
return {
records,
generatedAt: now.toISOString(),
}
}
/**
* Get filename for generic SRU file download
*/
export function getGenericSRUFilename(
formType: SRUFormType,
orgNumber: string | null,
fiscalYearStart: string
): string {
const year = fiscalYearStart.substring(0, 4)
const cleanOrg = orgNumber?.replace(/-/g, '') || 'unknown'
return `${formType}_${cleanOrg}_${year}.sru`
}
/**
* Format date for SRU: YYYYMMDD
*/
function formatSRUDate(date: Date): string {
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
return `${y}${m}${d}`
}
// Re-export helpers from the existing SRU generator
export { sruFileToString, validateSRUFile }
-28
View File
@@ -1,28 +0,0 @@
import type { EntityType } from '@/types'
// Generic SRU export types
export interface SRUExportResult {
formType: 'NE' | 'INK2'
entityType: EntityType
companyName: string | null
orgNumber: string | null
fiscalYear: {
id: string
name: string
start: string
end: string
}
balances: Array<{
sruCode: string
description: string
amount: number
accounts: Array<{
accountNumber: string
accountName: string
amount: number
}>
}>
warnings: string[]
}
export { type SRUCoverageStats } from './sru-engine'
+115 -25
View File
@@ -1,6 +1,6 @@
import { suggestCategory } from '@/lib/tax/expense-warnings'
import { getExpenseAccountForCategory } from '@/lib/bookkeeping/category-mapping'
import { findMatchingTemplates, type TemplateMatch } from '@/lib/bookkeeping/booking-templates'
import { findMatchingTemplates, getTemplateById, type TemplateMatch } from '@/lib/bookkeeping/booking-templates'
import { extensionRegistry } from '@/lib/extensions/registry'
import type { Transaction, TransactionCategory, EntityType, MappingRule } from '@/types'
@@ -246,42 +246,132 @@ export interface SuggestedTemplate {
requires_review: boolean
}
/**
* Get recently used templates from mapping rules.
* Extracts unique template_id values and returns them as suggestions.
*/
export function getRecentlyUsedTemplates(
mappingRules: MappingRule[],
entityType?: EntityType,
direction?: 'expense' | 'income' | 'transfer'
): SuggestedTemplate[] {
const seen = new Set<string>()
const results: SuggestedTemplate[] = []
// Sort by most recent (highest priority first)
const sorted = [...mappingRules]
.filter((r) => r.is_active && r.template_id)
.sort((a, b) => (b.confidence_score || 0) - (a.confidence_score || 0))
for (const rule of sorted) {
if (!rule.template_id || seen.has(rule.template_id)) continue
seen.add(rule.template_id)
const template = getTemplateById(rule.template_id)
if (!template) continue
// Filter by entity applicability
if (entityType && template.entity_applicability !== 'all' && template.entity_applicability !== entityType) continue
// Filter by direction
if (direction && template.direction !== direction && template.direction !== 'transfer') continue
results.push({
template_id: template.id,
name_sv: template.name_sv,
name_en: template.name_en,
group: template.group,
debit_account: template.debit_account,
credit_account: template.credit_account,
confidence: 0.85,
description_sv: template.description_sv,
risk_level: template.risk_level,
requires_review: template.requires_review,
})
if (results.length >= 5) break
}
return results
}
/**
* Get suggested booking templates for a transaction.
* Tries embedding-based semantic search first, falls back to keyword matching.
* Keyword matching as primary, AI embedding search as optional enhancer.
*/
export async function getSuggestedTemplates(
transaction: Transaction,
entityType?: EntityType
entityType?: EntityType,
mappingRules?: MappingRule[]
): Promise<SuggestedTemplate[]> {
let matches: TemplateMatch[]
const seen = new Set<string>()
const results: SuggestedTemplate[] = []
// 1. Boost recently-used templates from mapping rules
if (mappingRules) {
const direction = transaction.amount < 0 ? 'expense' : 'income'
const recent = getRecentlyUsedTemplates(mappingRules, entityType, direction)
for (const r of recent) {
if (!seen.has(r.template_id)) {
seen.add(r.template_id)
results.push(r)
}
}
}
// 2. Keyword + MCC matching (always available, no API keys needed)
const keywordMatches = findMatchingTemplates(transaction, entityType)
for (const m of keywordMatches) {
if (!seen.has(m.template.id)) {
seen.add(m.template.id)
results.push({
template_id: m.template.id,
name_sv: m.template.name_sv,
name_en: m.template.name_en,
group: m.template.group,
debit_account: m.template.debit_account,
credit_account: m.template.credit_account,
confidence: m.confidence,
description_sv: m.template.description_sv,
risk_level: m.template.risk_level,
requires_review: m.template.requires_review,
})
}
}
// 3. If AI extension loaded, merge in embedding-based matches (higher confidence)
try {
const aiExt = extensionRegistry.get('ai-categorization')
if (aiExt?.services?.findSimilarTemplates) {
matches = await aiExt.services.findSimilarTemplates(transaction, entityType)
} else {
matches = []
const aiMatches: TemplateMatch[] = await aiExt.services.findSimilarTemplates(transaction, entityType)
for (const m of aiMatches) {
const existing = results.find((r) => r.template_id === m.template.id)
if (existing) {
// AI match upgrades confidence if higher
if (m.confidence > existing.confidence) {
existing.confidence = m.confidence
}
} else {
results.push({
template_id: m.template.id,
name_sv: m.template.name_sv,
name_en: m.template.name_en,
group: m.template.group,
debit_account: m.template.debit_account,
credit_account: m.template.credit_account,
confidence: m.confidence,
description_sv: m.template.description_sv,
risk_level: m.template.risk_level,
requires_review: m.template.requires_review,
})
}
}
}
} catch {
matches = []
// AI enhancement is non-blocking
}
// Fall back to keyword matching if embedding search returns nothing
if (matches.length === 0) {
matches = findMatchingTemplates(transaction, entityType)
}
return matches.map((m: TemplateMatch) => ({
template_id: m.template.id,
name_sv: m.template.name_sv,
name_en: m.template.name_en,
group: m.template.group,
debit_account: m.template.debit_account,
credit_account: m.template.credit_account,
confidence: m.confidence,
description_sv: m.template.description_sv,
risk_level: m.template.risk_level,
requires_review: m.template.requires_review,
}))
return results
.sort((a, b) => b.confidence - a.confidence)
.slice(0, 10)
}
+35 -35
View File
@@ -12,31 +12,31 @@
/** Momsdeklaration box number */
export type MomsBox =
| '05' // Momspliktig forsaljning (taxable sales)
| '05' // Momspliktig försäljning (taxable sales)
| '06' // Momspliktiga uttag (taxable withdrawals)
| '07' // Vinstmarginalbeskattning (margin scheme)
| '08' // Hyresinkomster frivillig beskattning (rental)
| '10' // Utgaende moms 25%
| '11' // Utgaende moms 12%
| '12' // Utgaende moms 6%
| '20' // Inkop varor fran EU
| '21' // Inkop tjanster fran EU
| '22' // Inkop tjanster utanfor EU
| '23' // Inkop varor Sverige omvand skattskyldighet
| '24' // Inkop tjanster Sverige omvand skattskyldighet
| '30' // Utgaende moms inkop 25%
| '31' // Utgaende moms inkop 12%
| '32' // Utgaende moms inkop 6%
| '35' // Varuforssaljning till annat EU-land
| '36' // Varuforssaljning utanfor EU (export)
| '37' // Mellanmans inkop trepartshandel
| '38' // Mellanmans forsaljning trepartshandel
| '39' // Tjansteforssaljning EU (huvudregeln)
| '40' // Ovrig forsaljning av tjanster utomlands
| '41' // Forsaljning omvand skattskyldighet Sverige
| '42' // Ovrig forsaljning m.m.
| '48' // Ingaende moms att dra av
| '49' // Moms att betala eller fa tillbaka
| '10' // Utgående moms 25%
| '11' // Utgående moms 12%
| '12' // Utgående moms 6%
| '20' // Inköp varor från EU
| '21' // Inköp tjänster från EU
| '22' // Inköp tjänster utanför EU
| '23' // Inköp varor Sverige omvänd skattskyldighet
| '24' // Inköp tjänster Sverige omvänd skattskyldighet
| '30' // Utgående moms inköp 25%
| '31' // Utgående moms inköp 12%
| '32' // Utgående moms inköp 6%
| '35' // Varuförsäljning till annat EU-land
| '36' // Varuförsäljning utanför EU (export)
| '37' // Mellanmans inköp trepartshandel
| '38' // Mellanmans försäljning trepartshandel
| '39' // Tjänsteförsäljning EU (huvudregeln)
| '40' // Övrig försäljning av tjänster utomlands
| '41' // Försäljning omvänd skattskyldighet Sverige
| '42' // Övrig försäljning m.m.
| '48' // Ingående moms att dra av
| '49' // Moms att betala eller få tillbaka
| '50' // Importbeskattningsunderlag
| '60' // Importmoms 25%
| '61' // Importmoms 12%
@@ -45,35 +45,35 @@ export type MomsBox =
/** Map BAS revenue account to momsdeklaration box */
export const ACCOUNT_TO_BOX: Record<string, MomsBox> = {
// Domestic revenue (taxable) → Box 05
'3001': '05', // Forsaljning varor/tjanster 25%
'3002': '05', // Forsaljning varor/tjanster 12%
'3003': '05', // Forsaljning varor/tjanster 6%
'3001': '05', // Försäljning varor/tjänster 25%
'3002': '05', // Försäljning varor/tjänster 12%
'3003': '05', // Försäljning varor/tjänster 6%
// EU goods (reverse charge, VAT-free) → Box 35
'3108': '35', // Forsaljning varor till annat EU-land
'3108': '35', // Försäljning varor till annat EU-land
'3521': '35', // Fakturerade frakter EU (follows goods treatment)
// Non-EU goods export (zero-rated) → Box 36
'3105': '36', // Forsaljning varor export utanfor EU
'3105': '36', // Försäljning varor export utanför EU
'3522': '36', // Fakturerade frakter export
// Triangular trade → Box 38
'3109': '38', // Mellanmans forsaljning trepartshandel
'3109': '38', // Mellanmans försäljning trepartshandel
// EU services (reverse charge, main rule) → Box 39
'3308': '39', // Forsaljning tjanster EU
'3308': '39', // Försäljning tjänster EU
// Non-EU services → Box 40
'3305': '40', // Forsaljning tjanster export utanfor EU
'3305': '40', // Försäljning tjänster export utanför EU
// Output VAT → Boxes 10, 11, 12
'2611': '10', // Utgaende moms 25%
'2621': '11', // Utgaende moms 12%
'2631': '12', // Utgaende moms 6%
'2611': '10', // Utgående moms 25%
'2621': '11', // Utgående moms 12%
'2631': '12', // Utgående moms 6%
// Input VAT → Box 48
'2641': '48', // Ingaende moms
'2645': '48', // Beraknad ingaende moms (EU forvarv)
'2641': '48', // Ingående moms
'2645': '48', // Beräknad ingående moms (EU förvärv)
}
/** Swedish labels for each momsdeklaration box */