Files
skr/ts/skr.postingkeys.ts

248 lines
6.5 KiB
TypeScript
Raw Normal View History

/**
* 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<TPostingKey, IPostingKeyRule> = {
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
// Skip VAT amount requirement if posting TO a VAT account (the line itself IS the VAT)
const isVATAccount = accountNumber === '1571' || accountNumber === '1771' || accountNumber === '1576';
if (rule.requiresVAT && !vatAmount && !isVATAccount) {
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);
}