import * as plugins from './plugins.js'; import type { TSKRType } from './skr.types.js'; import type { IInvoice, IInvoiceLine, IBookingRules, TTaxScenario, IVATCategory } from './skr.invoice.entity.js'; /** * Maps invoice data to SKR accounts * Handles both SKR03 and SKR04 account mappings */ export class SKRInvoiceMapper { private logger: plugins.smartlog.ConsoleLog; private skrType: TSKRType; // SKR03 account mappings private readonly SKR03_ACCOUNTS = { // Control accounts vendorControl: '1600', // Verbindlichkeiten aus Lieferungen und Leistungen customerControl: '1200', // Forderungen aus Lieferungen und Leistungen // VAT accounts inputVAT19: '1576', // Abziehbare Vorsteuer 19% inputVAT7: '1571', // Abziehbare Vorsteuer 7% outputVAT19: '1776', // Umsatzsteuer 19% outputVAT7: '1771', // Umsatzsteuer 7% reverseChargeVAT: '1577', // Abziehbare Vorsteuer §13b UStG reverseChargePayable: '1787', // Umsatzsteuer §13b UStG // Default expense/revenue accounts defaultExpense: '4610', // Werbekosten defaultRevenue: '8400', // Erlöse 19% USt revenueReduced: '8300', // Erlöse 7% USt revenueTaxFree: '8120', // Steuerfreie Umsätze // Common expense accounts by category materialExpense: '5000', // Aufwendungen für Roh-, Hilfs- und Betriebsstoffe merchandiseExpense: '5400', // Aufwendungen für Waren personnelExpense: '6000', // Löhne und Gehälter rentExpense: '4200', // Miete officeExpense: '4930', // Bürobedarf travelExpense: '4670', // Reisekosten vehicleExpense: '4530', // Kfz-Kosten // Skonto accounts skontoExpense: '4736', // Erhaltene Skonti 19% USt skontoRevenue: '8736', // Gewährte Skonti 19% USt // Intra-EU accounts intraEUAcquisition: '8125', // Steuerfreie innergemeinschaftliche Erwerbe intraEUSupply: '8125' // Steuerfreie innergemeinschaftliche Lieferungen }; // SKR04 account mappings private readonly SKR04_ACCOUNTS = { // Control accounts vendorControl: '3300', // Verbindlichkeiten aus Lieferungen und Leistungen customerControl: '1400', // Forderungen aus Lieferungen und Leistungen // VAT accounts inputVAT19: '1406', // Abziehbare Vorsteuer 19% inputVAT7: '1401', // Abziehbare Vorsteuer 7% outputVAT19: '3806', // Umsatzsteuer 19% outputVAT7: '3801', // Umsatzsteuer 7% reverseChargeVAT: '1407', // Abziehbare Vorsteuer §13b UStG reverseChargePayable: '3837', // Umsatzsteuer §13b UStG // Default expense/revenue accounts defaultExpense: '6300', // Sonstige betriebliche Aufwendungen defaultRevenue: '4400', // Erlöse 19% USt revenueReduced: '4300', // Erlöse 7% USt revenueTaxFree: '4120', // Steuerfreie Umsätze // Common expense accounts by category materialExpense: '5000', // Aufwendungen für Roh-, Hilfs- und Betriebsstoffe merchandiseExpense: '5400', // Aufwendungen für Waren personnelExpense: '6000', // Löhne rentExpense: '6310', // Miete officeExpense: '6815', // Bürobedarf travelExpense: '6670', // Reisekosten vehicleExpense: '6530', // Kfz-Kosten // Skonto accounts skontoExpense: '4736', // Erhaltene Skonti 19% USt skontoRevenue: '8736', // Gewährte Skonti 19% USt // Intra-EU accounts intraEUAcquisition: '4125', // Steuerfreie innergemeinschaftliche Erwerbe intraEUSupply: '4125' // Steuerfreie innergemeinschaftliche Lieferungen }; // Product category to account mappings private readonly CATEGORY_MAPPINGS: Record = { 'MATERIAL': { skr03: '5000', skr04: '5000' }, 'MERCHANDISE': { skr03: '5400', skr04: '5400' }, 'SERVICE': { skr03: '4610', skr04: '6300' }, 'OFFICE': { skr03: '4930', skr04: '6815' }, 'IT': { skr03: '4940', skr04: '6825' }, 'TRAVEL': { skr03: '4670', skr04: '6670' }, 'VEHICLE': { skr03: '4530', skr04: '6530' }, 'RENT': { skr03: '4200', skr04: '6310' }, 'UTILITIES': { skr03: '4240', skr04: '6320' }, 'INSURANCE': { skr03: '4360', skr04: '6420' }, 'MARKETING': { skr03: '4610', skr04: '6600' }, 'CONSULTING': { skr03: '4640', skr04: '6650' }, 'LEGAL': { skr03: '4790', skr04: '6790' }, 'TELECOMMUNICATION': { skr03: '4920', skr04: '6805' } }; constructor(skrType: TSKRType) { this.skrType = skrType; this.logger = new plugins.smartlog.ConsoleLog(); } /** * Get account mappings for current SKR type */ private getAccounts() { return this.skrType === 'SKR03' ? this.SKR03_ACCOUNTS : this.SKR04_ACCOUNTS; } /** * Map invoice to booking rules */ public mapInvoiceToSKR( invoice: IInvoice, customMappings?: Partial ): IBookingRules { const accounts = this.getAccounts(); const taxScenario = invoice.taxScenario || 'domestic_taxed'; // Base booking rules const bookingRules: IBookingRules = { skrType: this.skrType, // Control accounts vendorControlAccount: customMappings?.vendorControlAccount || accounts.vendorControl, customerControlAccount: customMappings?.customerControlAccount || accounts.customerControl, // VAT accounts vatAccounts: { inputVAT19: accounts.inputVAT19, inputVAT7: accounts.inputVAT7, outputVAT19: accounts.outputVAT19, outputVAT7: accounts.outputVAT7, reverseChargeVAT: accounts.reverseChargeVAT }, // Default accounts defaultExpenseAccount: accounts.defaultExpense, defaultRevenueAccount: accounts.defaultRevenue, // Skonto skontoMethod: customMappings?.skontoMethod || 'gross', skontoExpenseAccount: accounts.skontoExpense, skontoRevenueAccount: accounts.skontoRevenue, // Custom mappings productCategoryMapping: customMappings?.productCategoryMapping || {}, vendorMapping: customMappings?.vendorMapping || {}, customerMapping: customMappings?.customerMapping || {} }; return bookingRules; } /** * Map invoice line to SKR account */ public mapInvoiceLineToAccount( line: IInvoiceLine, invoice: IInvoice, bookingRules: IBookingRules ): string { // Check if account is already specified if (line.accountNumber) { return line.accountNumber; } // For revenue (outbound invoices) if (invoice.direction === 'outbound') { return this.mapRevenueAccount(line, invoice, bookingRules); } // For expenses (inbound invoices) return this.mapExpenseAccount(line, invoice, bookingRules); } /** * Map revenue account based on VAT rate and scenario */ private mapRevenueAccount( line: IInvoiceLine, invoice: IInvoice, bookingRules: IBookingRules ): string { const accounts = this.getAccounts(); const vatRate = line.vatCategory.rate; // Check tax scenario switch (invoice.taxScenario) { case 'intra_eu_supply': return accounts.intraEUSupply; case 'export': case 'domestic_exempt': return accounts.revenueTaxFree; case 'domestic_taxed': default: // Map by VAT rate if (vatRate === 19) { return accounts.defaultRevenue; } else if (vatRate === 7) { return accounts.revenueReduced; } else if (vatRate === 0) { return accounts.revenueTaxFree; } return accounts.defaultRevenue; } } /** * Map expense account based on product category and vendor */ private mapExpenseAccount( line: IInvoiceLine, invoice: IInvoice, bookingRules: IBookingRules ): string { const accounts = this.getAccounts(); // Check vendor-specific mapping const vendorId = invoice.supplier.id; if (bookingRules.vendorMapping && bookingRules.vendorMapping[vendorId]) { return bookingRules.vendorMapping[vendorId]; } // Try to determine category from line description const category = this.detectProductCategory(line.description); if (category) { const mapping = this.CATEGORY_MAPPINGS[category]; if (mapping) { return this.skrType === 'SKR03' ? mapping.skr03 : mapping.skr04; } } // Check product category mapping if (line.productCode && bookingRules.productCategoryMapping) { const mappedAccount = bookingRules.productCategoryMapping[line.productCode]; if (mappedAccount) { return mappedAccount; } } // Default expense account return bookingRules.defaultExpenseAccount; } /** * Detect product category from description */ private detectProductCategory(description: string): string | undefined { const lowerDesc = description.toLowerCase(); const categoryKeywords: Record = { 'MATERIAL': ['material', 'rohstoff', 'raw material', 'component'], 'MERCHANDISE': ['ware', 'merchandise', 'product', 'artikel'], 'SERVICE': ['service', 'dienstleistung', 'beratung', 'support'], 'OFFICE': ['büro', 'office', 'papier', 'stationery'], 'IT': ['software', 'hardware', 'computer', 'lizenz', 'license'], 'TRAVEL': ['reise', 'travel', 'hotel', 'flug', 'flight'], 'VEHICLE': ['kfz', 'vehicle', 'auto', 'benzin', 'fuel'], 'RENT': ['miete', 'rent', 'lease', 'pacht'], 'UTILITIES': ['strom', 'wasser', 'gas', 'energie', 'electricity', 'water'], 'INSURANCE': ['versicherung', 'insurance'], 'MARKETING': ['werbung', 'marketing', 'advertising', 'kampagne'], 'CONSULTING': ['beratung', 'consulting', 'advisory'], 'LEGAL': ['rechts', 'legal', 'anwalt', 'lawyer', 'notar'], 'TELECOMMUNICATION': ['telefon', 'internet', 'mobilfunk', 'telekom'] }; for (const [category, keywords] of Object.entries(categoryKeywords)) { if (keywords.some(keyword => lowerDesc.includes(keyword))) { return category; } } return undefined; } /** * Get VAT account for given VAT category and rate */ public getVATAccount( vatCategory: IVATCategory, direction: 'input' | 'output', taxScenario: TTaxScenario ): string { const accounts = this.getAccounts(); // Handle reverse charge if (taxScenario === 'reverse_charge' || vatCategory.code === 'AE') { return direction === 'input' ? accounts.reverseChargeVAT : accounts.reverseChargePayable; } // Standard VAT accounts by rate if (direction === 'input') { if (vatCategory.rate === 19) { return accounts.inputVAT19; } else if (vatCategory.rate === 7) { return accounts.inputVAT7; } } else { if (vatCategory.rate === 19) { return accounts.outputVAT19; } else if (vatCategory.rate === 7) { return accounts.outputVAT7; } } // Default to 19% if rate is not standard return direction === 'input' ? accounts.inputVAT19 : accounts.outputVAT19; } /** * Get control account for party */ public getControlAccount( invoice: IInvoice, bookingRules: IBookingRules ): string { if (invoice.direction === 'inbound') { // Check vendor-specific control account const vendorId = invoice.supplier.id; if (bookingRules.vendorMapping && bookingRules.vendorMapping[vendorId]) { const customAccount = bookingRules.vendorMapping[vendorId]; // Check if it's a control account (starts with 16 for SKR03 or 33 for SKR04) if (this.isControlAccount(customAccount)) { return customAccount; } } return bookingRules.vendorControlAccount; } else { // Check customer-specific control account const customerId = invoice.customer.id; if (bookingRules.customerMapping && bookingRules.customerMapping[customerId]) { const customAccount = bookingRules.customerMapping[customerId]; // Check if it's a control account (starts with 12 for SKR03 or 14 for SKR04) if (this.isControlAccount(customAccount)) { return customAccount; } } return bookingRules.customerControlAccount; } } /** * Check if account is a control account */ private isControlAccount(accountNumber: string): boolean { if (this.skrType === 'SKR03') { return accountNumber.startsWith('12') || accountNumber.startsWith('16'); } else { return accountNumber.startsWith('14') || accountNumber.startsWith('33'); } } /** * Get skonto accounts */ public getSkontoAccounts(invoice: IInvoice): { skontoAccount: string; vatCorrectionAccount: string; } { const accounts = this.getAccounts(); if (invoice.direction === 'inbound') { // Received skonto (expense reduction) return { skontoAccount: accounts.skontoExpense, vatCorrectionAccount: accounts.inputVAT19 // VAT correction }; } else { // Granted skonto (revenue reduction) return { skontoAccount: accounts.skontoRevenue, vatCorrectionAccount: accounts.outputVAT19 // VAT correction }; } } /** * Validate account number format */ public validateAccountNumber(accountNumber: string): boolean { // SKR accounts are typically 4 digits, sometimes with sub-accounts const accountPattern = /^\d{4}(\d{0,2})?$/; return accountPattern.test(accountNumber); } /** * Get account description */ public getAccountDescription(accountNumber: string): string { // This would typically look up from a complete SKR account database // For now, return a basic description const commonAccounts: Record = { // SKR03 '1200': 'Forderungen aus Lieferungen und Leistungen', '1600': 'Verbindlichkeiten aus Lieferungen und Leistungen', '1576': 'Abziehbare Vorsteuer 19%', '1571': 'Abziehbare Vorsteuer 7%', '1776': 'Umsatzsteuer 19%', '1771': 'Umsatzsteuer 7%', '4610': 'Werbekosten', '8400': 'Erlöse 19% USt', '8300': 'Erlöse 7% USt', // SKR04 '1400': 'Forderungen aus Lieferungen und Leistungen', '3300': 'Verbindlichkeiten aus Lieferungen und Leistungen', '1406': 'Abziehbare Vorsteuer 19%', '1401': 'Abziehbare Vorsteuer 7%', '3806': 'Umsatzsteuer 19%', '3801': 'Umsatzsteuer 7%', '6300': 'Sonstige betriebliche Aufwendungen', '4400': 'Erlöse 19% USt', '4300': 'Erlöse 7% USt' }; return commonAccounts[accountNumber] || `Account ${accountNumber}`; } /** * Calculate booking confidence score */ public calculateConfidence( invoice: IInvoice, bookingRules: IBookingRules ): number { let confidence = 100; // Reduce confidence for missing or uncertain mappings invoice.lines.forEach(line => { if (!line.accountNumber) { confidence -= 10; // No explicit account mapping } if (!line.productCode) { confidence -= 5; // No product code for mapping } }); // Reduce confidence for complex tax scenarios if (invoice.taxScenario === 'reverse_charge' || invoice.taxScenario === 'intra_eu_acquisition') { confidence -= 15; } // Reduce confidence for mixed VAT rates if (invoice.vatBreakdown.length > 1) { confidence -= 10; } // Reduce confidence if no vendor/customer mapping exists if (invoice.direction === 'inbound') { if (!bookingRules.vendorMapping?.[invoice.supplier.id]) { confidence -= 10; } } else { if (!bookingRules.customerMapping?.[invoice.customer.id]) { confidence -= 10; } } // Reduce confidence for credit notes if (invoice.invoiceTypeCode === '381') { confidence -= 10; } return Math.max(0, confidence); } }