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:
co-authored by
Claude Opus 4.6
parent
a9bf24ce0b
commit
66a4027f1e
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')),
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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'
|
||||
@@ -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
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user