486 lines
16 KiB
TypeScript
486 lines
16 KiB
TypeScript
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);
|
|
}
|
|
} |