- 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>
210 lines
5.5 KiB
TypeScript
210 lines
5.5 KiB
TypeScript
import type { NEDeclaration, SRUFile, SRURecord } from '@/lib/reports/ne-bilaga/types'
|
|
|
|
/**
|
|
* SRU File Generator
|
|
*
|
|
* Generates SRU (Standardiserat Räkenskapsutdrag) files for electronic
|
|
* submission to Skatteverket. The SRU format is used for tax declarations.
|
|
*
|
|
* Format specification:
|
|
* - Each line starts with # followed by field code
|
|
* - Values follow the field code
|
|
* - File must be plain text (ISO-8859-1 encoding traditionally, but UTF-8 is often accepted)
|
|
*
|
|
* NE-bilaga field codes:
|
|
* - #BLANKETT NE - Declares this is an NE form
|
|
* - #IDENTITET - Organization number + name
|
|
* - #UPPGIFT - Individual field values
|
|
*/
|
|
|
|
/**
|
|
* SRU field codes for NE declaration
|
|
* These are the official Skatteverket field codes for NE-bilaga
|
|
*/
|
|
const NE_SRU_FIELD_CODES: Record<string, string> = {
|
|
// Company identification
|
|
ORG_NUMBER: '201',
|
|
COMPANY_NAME: '202',
|
|
|
|
// NE rutor - Income
|
|
R1: '7310', // Försäljning och andra intäkter med moms
|
|
R2: '7311', // Momsfria intäkter (ej skattepliktiga)
|
|
R3: '7312', // Bil- och bostadsförmån m.m.
|
|
R4: '7313', // Ränteintäkter
|
|
|
|
// NE rutor - Expenses
|
|
R5: '7320', // Varuinköp
|
|
R6: '7321', // Övriga externa kostnader
|
|
R7: '7322', // Anställdas löner
|
|
R8: '7323', // Räntekostnader
|
|
R9: '7324', // Avskrivningar på byggnader och markanläggningar
|
|
R10: '7325', // Avskrivningar på maskiner och inventarier
|
|
|
|
// Result
|
|
R11: '7350', // Årets resultat
|
|
}
|
|
|
|
/**
|
|
* Generate SRU file content from NE declaration
|
|
*/
|
|
export function generateSRUFile(declaration: NEDeclaration): SRUFile {
|
|
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: 'NE' })
|
|
|
|
// Company identification
|
|
if (declaration.companyInfo.orgNumber) {
|
|
// Remove any dashes from org number
|
|
const cleanOrgNumber = declaration.companyInfo.orgNumber.replace(/-/g, '')
|
|
records.push({
|
|
fieldCode: 'IDENTITET',
|
|
value: cleanOrgNumber,
|
|
})
|
|
}
|
|
|
|
// Fiscal year
|
|
records.push({
|
|
fieldCode: 'UPPGIFT',
|
|
value: `7000 ${formatSRUDateRange(declaration.fiscalYear.start, declaration.fiscalYear.end)}`,
|
|
})
|
|
|
|
// NE rutor values
|
|
const rutaEntries: [keyof typeof NE_SRU_FIELD_CODES, number][] = [
|
|
['R1', declaration.rutor.R1],
|
|
['R2', declaration.rutor.R2],
|
|
['R3', declaration.rutor.R3],
|
|
['R4', declaration.rutor.R4],
|
|
['R5', declaration.rutor.R5],
|
|
['R6', declaration.rutor.R6],
|
|
['R7', declaration.rutor.R7],
|
|
['R8', declaration.rutor.R8],
|
|
['R9', declaration.rutor.R9],
|
|
['R10', declaration.rutor.R10],
|
|
['R11', declaration.rutor.R11],
|
|
]
|
|
|
|
for (const [ruta, value] of rutaEntries) {
|
|
// Only include non-zero values
|
|
if (value !== 0) {
|
|
const fieldCode = NE_SRU_FIELD_CODES[ruta]
|
|
records.push({
|
|
fieldCode: 'UPPGIFT',
|
|
value: `${fieldCode} ${formatSRUAmount(value)}`,
|
|
})
|
|
}
|
|
}
|
|
|
|
// End of form
|
|
records.push({ fieldCode: 'BLANKETTSLUT', value: '' })
|
|
|
|
return {
|
|
records,
|
|
generatedAt: now.toISOString(),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert SRU file to string content
|
|
*/
|
|
export function sruFileToString(sruFile: SRUFile): string {
|
|
const lines: string[] = []
|
|
|
|
for (const record of sruFile.records) {
|
|
if (record.value === '') {
|
|
lines.push(`#${record.fieldCode}`)
|
|
} else {
|
|
lines.push(`#${record.fieldCode} ${record.value}`)
|
|
}
|
|
}
|
|
|
|
// SRU files should end with a newline
|
|
return lines.join('\r\n') + '\r\n'
|
|
}
|
|
|
|
/**
|
|
* 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}`
|
|
}
|
|
|
|
/**
|
|
* Format date string (YYYY-MM-DD) to SRU format (YYYYMMDD)
|
|
*/
|
|
function dateStringToSRU(dateStr: string): string {
|
|
return dateStr.replace(/-/g, '')
|
|
}
|
|
|
|
/**
|
|
* Format fiscal year date range for SRU
|
|
*/
|
|
function formatSRUDateRange(startDate: string, endDate: string): string {
|
|
return `${dateStringToSRU(startDate)}-${dateStringToSRU(endDate)}`
|
|
}
|
|
|
|
/**
|
|
* Format amount for SRU
|
|
* - Whole numbers (no decimals)
|
|
* - No thousands separator
|
|
* - Negative values with minus sign
|
|
*/
|
|
function formatSRUAmount(amount: number): string {
|
|
return Math.round(amount).toString()
|
|
}
|
|
|
|
/**
|
|
* Validate SRU file content
|
|
*/
|
|
export function validateSRUFile(sruFile: SRUFile): {
|
|
isValid: boolean
|
|
errors: string[]
|
|
} {
|
|
const errors: string[] = []
|
|
|
|
// Check for required records
|
|
const hasHeader = sruFile.records.some(r => r.fieldCode === 'PRODUKT')
|
|
const hasBlankett = sruFile.records.some(r => r.fieldCode === 'BLANKETT')
|
|
const hasBlankettslut = sruFile.records.some(r => r.fieldCode === 'BLANKETTSLUT')
|
|
|
|
if (!hasHeader) {
|
|
errors.push('Missing PRODUKT header')
|
|
}
|
|
|
|
if (!hasBlankett) {
|
|
errors.push('Missing BLANKETT declaration')
|
|
}
|
|
|
|
if (!hasBlankettslut) {
|
|
errors.push('Missing BLANKETTSLUT')
|
|
}
|
|
|
|
return {
|
|
isValid: errors.length === 0,
|
|
errors,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get filename for SRU file download
|
|
*/
|
|
export function getSRUFilename(declaration: NEDeclaration): string {
|
|
const year = declaration.fiscalYear.start.substring(0, 4)
|
|
const orgNumber = declaration.companyInfo.orgNumber?.replace(/-/g, '') || 'unknown'
|
|
return `NE_${orgNumber}_${year}.sru`
|
|
}
|