/** * EU Sales List XML Generator (SKV 5740 format) * * Generates XML compatible with Skatteverket's e-filing system for * periodisk sammanställning (EC Sales List / recapitulative statement). * * Reference: Skatteverket SKV 5740, KVPS XML schema * Filing: Monthly for goods, quarterly for services * * The XML structure follows Skatteverket's KVPS (Kvartalsvis Periodisk * Sammanställning) format with elements for reporter info, period, and * per-customer goods/services/triangulation amounts in whole SEK. */ import type { ECSalesListReport } from './eu-sales-list-engine' /** * Generate SKV-compatible XML for the EC Sales List report. * * Structure: * * — reporter/sender information * — reporting period * — one per customer VAT number * — buyer VAT number * — buyer country code * — goods amount (box 35) * — services amount (box 39) * — triangulation (box 38) * * * * All amounts are rounded to whole SEK (no decimals). */ export function generateSKVXml(report: ECSalesListReport): string { const lines: string[] = [] lines.push('') lines.push('') // Reporter/sender info lines.push(' ') lines.push(` ${escapeXml(report.reporterVatNumber)}`) lines.push(` ${escapeXml(report.reporterName)}`) lines.push(' ') // Period info lines.push(' ') lines.push(` ${report.period.year}`) if (report.period.month !== undefined) { lines.push(` ${String(report.period.month).padStart(2, '0')}`) } if (report.period.quarter !== undefined) { lines.push(` ${report.period.quarter}`) } lines.push(` ${report.filingType === 'monthly' ? 'Manad' : 'Kvartal'}`) lines.push(' ') // Customer lines for (const line of report.lines) { const goods = Math.round(line.goodsAmount) const services = Math.round(line.servicesAmount) const triangulation = Math.round(line.triangulationAmount) // Skip lines with all zero amounts if (goods === 0 && services === 0 && triangulation === 0) continue lines.push(' ') lines.push(` ${escapeXml(line.customerVatNumber)}`) lines.push(` ${escapeXml(line.customerCountry)}`) if (goods !== 0) { lines.push(` ${goods}`) } if (services !== 0) { lines.push(` ${services}`) } if (triangulation !== 0) { lines.push(` ${triangulation}`) } lines.push(' ') } // Totals lines.push(' ') lines.push(` ${Math.round(report.totals.goods)}`) lines.push(` ${Math.round(report.totals.services)}`) lines.push(` ${Math.round(report.totals.triangulation)}`) lines.push(` ${Math.round(report.totals.total)}`) lines.push(' ') lines.push('') return lines.join('\n') + '\n' } /** * Generate a filename for the XML download. * * Format: KVPS__.xml * Example: KVPS_SE556677889901_2026-Q1.xml */ export function generateXMLFilename(report: ECSalesListReport): string { const vat = report.reporterVatNumber.replace(/\s/g, '') const period = formatPeriod(report) return `KVPS_${vat}_${period}.xml` } function formatPeriod(report: ECSalesListReport): string { const { year, month, quarter } = report.period if (month !== undefined) { return `${year}-${String(month).padStart(2, '0')}` } if (quarter !== undefined) { return `${year}-Q${quarter}` } return `${year}` } /** Escape special XML characters to prevent injection */ function escapeXml(str: string): string { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') }