/** * DATEV Posting Keys (Buchungsschlüssel) for German Accounting * * Posting keys control automatic VAT booking and are automatically checked * in German tax audits (Betriebsprüfungen). Using incorrect posting keys * can have serious tax consequences. * * Reference: DATEV Buchungsschlüssel-Verzeichnis */ import type { TPostingKey, IPostingKeyRule } from './skr.types.js'; /** * Posting key definitions with validation rules */ export const POSTING_KEY_RULES: Record = { 3: { key: 3, description: 'Zahlungseingang mit 19% Umsatzsteuer', vatRate: 19, requiresVAT: true, disablesVATAutomatism: false, allowedScenarios: ['domestic_taxed'] }, 8: { key: 8, description: '7% Vorsteuer', vatRate: 7, requiresVAT: true, disablesVATAutomatism: false, allowedScenarios: ['domestic_taxed'] }, 9: { key: 9, description: '19% Vorsteuer', vatRate: 19, requiresVAT: true, disablesVATAutomatism: false, allowedScenarios: ['domestic_taxed'] }, 19: { key: 19, description: '19% Vorsteuer bei innergemeinschaftlichen Lieferungen', vatRate: 19, requiresVAT: true, disablesVATAutomatism: false, allowedScenarios: ['intra_eu'] }, 40: { key: 40, description: 'Steuerfrei / Aufhebung der Automatik', vatRate: 0, requiresVAT: false, disablesVATAutomatism: true, allowedScenarios: ['tax_free', 'export', 'reverse_charge'] }, 94: { key: 94, description: '19% Vorsteuer/Umsatzsteuer bei Erwerb aus EU oder Drittland (Reverse Charge)', vatRate: 19, requiresVAT: true, disablesVATAutomatism: false, allowedScenarios: ['reverse_charge', 'intra_eu', 'third_country'] } }; /** * Validate posting key for a journal entry line */ export function validatePostingKey( postingKey: TPostingKey, accountNumber: string, amount: number, vatAmount?: number, taxScenario?: string ): { isValid: boolean; errors: string[]; warnings: string[] } { const errors: string[] = []; const warnings: string[] = []; // Get posting key rule const rule = POSTING_KEY_RULES[postingKey]; if (!rule) { errors.push(`Invalid posting key: ${postingKey}`); return { isValid: false, errors, warnings }; } // Validate VAT requirement if (rule.requiresVAT && !vatAmount) { errors.push( `Posting key ${postingKey} requires VAT amount, but none provided. ` + `Description: ${rule.description}` ); } // Validate VAT rate if specified if (rule.vatRate && vatAmount && rule.vatRate > 0) { const expectedVAT = Math.round(amount * rule.vatRate) / 100; const tolerance = 0.02; // 2 cent tolerance for rounding if (Math.abs(vatAmount - expectedVAT) > tolerance) { warnings.push( `VAT amount ${vatAmount} does not match expected ${expectedVAT.toFixed(2)} ` + `for posting key ${postingKey} (${rule.vatRate}%)` ); } } // Validate tax scenario if (rule.allowedScenarios && taxScenario) { if (!rule.allowedScenarios.includes(taxScenario)) { errors.push( `Posting key ${postingKey} is not valid for tax scenario '${taxScenario}'. ` + `Allowed scenarios: ${rule.allowedScenarios.join(', ')}` ); } } // Validate automatism disabling if (rule.disablesVATAutomatism && vatAmount && vatAmount > 0) { warnings.push( `Posting key ${postingKey} disables VAT automatism but VAT amount is provided. ` + `This may cause incorrect tax reporting.` ); } return { isValid: errors.length === 0, errors, warnings }; } /** * Get posting key description */ export function getPostingKeyDescription(postingKey: TPostingKey): string { const rule = POSTING_KEY_RULES[postingKey]; return rule ? rule.description : `Unknown posting key: ${postingKey}`; } /** * Get appropriate posting key for a transaction */ export function suggestPostingKey(params: { vatRate: number; taxScenario?: string; isPayment?: boolean; }): TPostingKey { const { vatRate, taxScenario, isPayment } = params; // Tax-free or reverse charge scenarios if (taxScenario === 'tax_free' || taxScenario === 'export') { return 40; } // Reverse charge if (taxScenario === 'reverse_charge' || taxScenario === 'third_country') { return 94; } // Intra-EU with VAT if (taxScenario === 'intra_eu' && vatRate === 19) { return 19; } // Payment with 19% VAT if (isPayment && vatRate === 19) { return 3; } // Input VAT based on rate if (vatRate === 19) { return 9; } if (vatRate === 7) { return 8; } // Default to tax-free if no VAT if (vatRate === 0) { return 40; } // Fallback to 19% input VAT return 9; } /** * Validate all posting keys for consistency */ export function validatePostingKeyConsistency(lines: Array<{ postingKey: TPostingKey; accountNumber: string; debit?: number; credit?: number; vatAmount?: number; }>): { isValid: boolean; errors: string[]; warnings: string[] } { const errors: string[] = []; const warnings: string[] = []; // Check for mixing tax-free and taxed transactions const hasTaxFree = lines.some(line => line.postingKey === 40); const hasTaxed = lines.some(line => [3, 8, 9, 19, 94].includes(line.postingKey)); if (hasTaxFree && hasTaxed) { warnings.push( 'Journal entry mixes tax-free (key 40) and taxed transactions. ' + 'Verify this is intentional.' ); } // Check for reverse charge consistency const hasReverseCharge = lines.some(line => line.postingKey === 94); if (hasReverseCharge) { const reverseChargeLines = lines.filter(line => line.postingKey === 94); if (reverseChargeLines.length % 2 !== 0) { errors.push( 'Reverse charge (posting key 94) requires both input and output VAT entries. ' + 'Found odd number of reverse charge lines.' ); } } return { isValid: errors.length === 0, errors, warnings }; } /** * Check if posting key requires automatic VAT booking */ export function requiresAutomaticVAT(postingKey: TPostingKey): boolean { const rule = POSTING_KEY_RULES[postingKey]; return rule ? !rule.disablesVATAutomatism : false; } /** * Get all valid posting keys */ export function getAllPostingKeys(): TPostingKey[] { return Object.keys(POSTING_KEY_RULES).map(k => Number(k) as TPostingKey); }