Files
skr/ts/skr.invoice.mapper.ts

486 lines
16 KiB
TypeScript
Raw Normal View History

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<string, { skr03: string; skr04: string }> = {
'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>
): 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<string, string[]> = {
'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<string, string> = {
// 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);
}
}