Files
accounted/lib/invoice/vat-rules.ts
T
Jakob WennbergandClaude Opus 4.6 91e2c1705a feat: per-line VAT, invoice document types, ledger-based VAT declaration, bank reconciliation, and pagination
Per-line VAT rates:
- Add generatePerRateLines() to group invoice items by vat_rate with separate
  revenue + VAT lines per rate group (invoice-entries.ts)
- Add getAvailableVatRates() and getVatTreatmentForRate() (vat-rules.ts)
- PDF template shows per-line VAT column and per-rate totals for mixed-rate invoices
- Invoice create/review UI supports per-line rate selection
- Types: add vat_rate/vat_amount to InvoiceItem, vat_rate to CreateInvoiceItemInput

Invoice document types (proforma, delivery note):
- Add InvoiceDocumentType, document_type and converted_from_id to Invoice type
- PDF hides prices for delivery notes, adds proforma notice
- Email templates support all document types
- mark-paid skips journal entries for non-invoice document types
- Migration 031: invoice_document_type

Accounting method support:
- Add AccountingMethod type (accrual/cash)
- Migration 032: add_accounting_method column to company_settings

VAT declaration rewrite:
- Rewrite to read directly from general ledger (26xx/3xxx account lines)
  instead of aggregating invoices/transactions/receipts
- ACCOUNT_RUTA mapping drives momsdeklaration boxes from GL balances

Bank reconciliation:
- Transaction ingest now pre-fetches unlinked GL lines and attempts
  auto-reconciliation during import
- Add transaction.reconciled event type
- Add ReconciliationMethod type and reconciliation_method on Transaction
- Migration 030: bank_reconciliation
- New reconciliation engine, API routes, and BankReconciliationView component

Pagination (fetchAllRows):
- New lib/supabase/fetch-all.ts overcomes PostgREST 1000-row limit
- Adopted in all report generators, SIE/SRU export, account list APIs

Fiscal period validation:
- New validate-period-duration.ts enforces max 18 months per BFL 3 kap.
- Applied in period-service.ts and fiscal-periods API

Account mapper simplification:
- Remove Levenshtein/fuzzy matching, use exact account number match only

Swedbank parser improvements:
- Support abbreviated headers (Clnr, Bokfdag, Radnr)
- Use Referens column as counterparty

Chart of accounts management:
- Add DELETE endpoint with system account and usage protection
- PUT uses partial updates
- New AccountCombobox, AddAccountDialog, EditAccountDialog, ChartOfAccountsManager

Tax deadline corrections:
- Rewrite inkomstdeklaration_ab using Skatteverket lookup table
- Rewrite arsredovisning deadline to 7 months after FY end per ÅRL 8:3

Onboarding first fiscal year:
- Add first fiscal year toggle with date pickers and 18-month validation

UI terminology:
- Change "okategoriserad/kategorisera" to "obokförd/bokföra" throughout

Report column fix:
- Fix start_date/end_date to period_start/period_end in report queries

Supplier invoice input:
- CreateSupplierInvoiceItemInput uses amount field (legacy quantity/unit_price kept)

Misc:
- SIE import uses upsert for idempotent account creation
- account-descriptions.ts falls back to BAS reference data
- Add invoice_default_notes to CompanySettings
- Update CLAUDE.md to reflect current project state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:57:15 +01:00

169 lines
4.2 KiB
TypeScript

import type { CustomerType, VatTreatment } from '@/types'
export interface VatRateOption {
rate: number
label: string
treatment: VatTreatment
}
/**
* Get available VAT rates for invoice line items based on customer type.
*
* Swedish/EU-unvalidated customers can choose between 25%, 12%, 6%, and 0% (exempt).
* Reverse charge and export customers are locked to 0%.
*/
export function getAvailableVatRates(
customerType: CustomerType,
vatNumberValidated: boolean = false
): VatRateOption[] {
// EU business with validated VAT → reverse charge, locked to 0%
if (customerType === 'eu_business' && vatNumberValidated) {
return [{ rate: 0, label: '0% (omvänd skattskyldighet)', treatment: 'reverse_charge' }]
}
// Non-EU → export, locked to 0%
if (customerType === 'non_eu_business') {
return [{ rate: 0, label: '0% (export)', treatment: 'export' }]
}
// Swedish customers (or EU without validated VAT) can choose any rate
return [
{ rate: 25, label: '25%', treatment: 'standard_25' },
{ rate: 12, label: '12%', treatment: 'reduced_12' },
{ rate: 6, label: '6%', treatment: 'reduced_6' },
{ rate: 0, label: '0% (momsfritt)', treatment: 'exempt' },
]
}
/**
* Map a numeric VAT rate to a VatTreatment.
*/
export function getVatTreatmentForRate(rate: number): VatTreatment {
switch (rate) {
case 25:
return 'standard_25'
case 12:
return 'reduced_12'
case 6:
return 'reduced_6'
case 0:
return 'exempt'
default:
return 'standard_25'
}
}
export interface VatRule {
treatment: VatTreatment
rate: number
momsRuta: string
reverseChargeText?: string
}
/**
* Determine VAT treatment based on customer type and VAT validation status
*
* Rules:
* - Swedish customers: 25% VAT, moms ruta 05
* - EU business with validated VAT: 0% reverse charge, moms ruta 39
* - EU business without validated VAT: 25% VAT, moms ruta 05
* - Non-EU business: 0% export, moms ruta 40
*/
export function getVatRules(
customerType: CustomerType,
vatNumberValidated: boolean = false
): VatRule {
switch (customerType) {
case 'individual':
case 'swedish_business':
return {
treatment: 'standard_25',
rate: 25,
momsRuta: '05',
}
case 'eu_business':
if (vatNumberValidated) {
return {
treatment: 'reverse_charge',
rate: 0,
momsRuta: '39',
reverseChargeText: 'Omvänd skattskyldighet / Reverse charge - VAT to be accounted for by the recipient as per Article 196, Council Directive 2006/112/EC',
}
}
// EU business without validated VAT number must be charged Swedish VAT
return {
treatment: 'standard_25',
rate: 25,
momsRuta: '05',
}
case 'non_eu_business':
return {
treatment: 'export',
rate: 0,
momsRuta: '40',
}
default:
return {
treatment: 'standard_25',
rate: 25,
momsRuta: '05',
}
}
}
/**
* Calculate VAT amount
*/
export function calculateVat(subtotal: number, vatRate: number): number {
return subtotal * (vatRate / 100)
}
/**
* Calculate total including VAT
*/
export function calculateTotal(subtotal: number, vatRate: number): number {
return subtotal + calculateVat(subtotal, vatRate)
}
/**
* Format VAT rate for display
*/
export function formatVatRate(rate: number): string {
if (rate === 0) {
return '0%'
}
return `${rate}%`
}
/**
* Get VAT treatment label in Swedish
*/
export function getVatTreatmentLabel(treatment: VatTreatment): string {
const labels: Record<VatTreatment, string> = {
standard_25: '25% moms',
reduced_12: '12% moms',
reduced_6: '6% moms',
reverse_charge: 'Omvänd skattskyldighet (0%)',
export: 'Export (0%)',
exempt: 'Momsfritt',
}
return labels[treatment]
}
/**
* Get moms ruta description
*/
export function getMomsRutaDescription(ruta: string): string {
const descriptions: Record<string, string> = {
'05': 'Utgående moms 25%',
'06': 'Utgående moms 12%',
'07': 'Utgående moms 6%',
'39': 'Försäljning av tjänster till annat EU-land',
'40': 'Export utanför EU',
}
return descriptions[ruta] || ruta
}