738 lines
22 KiB
TypeScript
738 lines
22 KiB
TypeScript
import * as plugins from './plugins.js';
|
|
import { JournalEntry } from './skr.classes.journalentry.js';
|
|
import { SKRInvoiceMapper } from './skr.invoice.mapper.js';
|
|
import type { TSKRType, IJournalEntry, IJournalEntryLine } from './skr.types.js';
|
|
import type {
|
|
IInvoice,
|
|
IInvoiceLine,
|
|
IBookingRules,
|
|
IBookingInfo,
|
|
TTaxScenario,
|
|
IPaymentInfo
|
|
} from './skr.invoice.entity.js';
|
|
|
|
/**
|
|
* Options for booking an invoice
|
|
*/
|
|
export interface IBookingOptions {
|
|
autoBook?: boolean;
|
|
confidenceThreshold?: number;
|
|
bookingDate?: Date;
|
|
bookingReference?: string;
|
|
skipValidation?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Result of booking an invoice
|
|
*/
|
|
export interface IBookingResult {
|
|
success: boolean;
|
|
journalEntry?: JournalEntry;
|
|
bookingInfo?: IBookingInfo;
|
|
confidence: number;
|
|
warnings?: string[];
|
|
errors?: string[];
|
|
}
|
|
|
|
/**
|
|
* Automatic booking engine for invoices
|
|
* Creates journal entries from invoice data based on SKR mapping rules
|
|
*/
|
|
export class InvoiceBookingEngine {
|
|
private logger: plugins.smartlog.ConsoleLog;
|
|
private skrType: TSKRType;
|
|
private mapper: SKRInvoiceMapper;
|
|
|
|
constructor(skrType: TSKRType) {
|
|
this.skrType = skrType;
|
|
this.mapper = new SKRInvoiceMapper(skrType);
|
|
this.logger = new plugins.smartlog.ConsoleLog();
|
|
}
|
|
|
|
/**
|
|
* Book an invoice to the ledger
|
|
*/
|
|
public async bookInvoice(
|
|
invoice: IInvoice,
|
|
bookingRules?: Partial<IBookingRules>,
|
|
options?: IBookingOptions
|
|
): Promise<IBookingResult> {
|
|
try {
|
|
// Get complete booking rules
|
|
const rules = this.mapper.mapInvoiceToSKR(invoice, bookingRules);
|
|
|
|
// Calculate confidence
|
|
const confidence = this.mapper.calculateConfidence(invoice, rules);
|
|
|
|
// Check if auto-booking is allowed
|
|
if (options?.autoBook && confidence < (options.confidenceThreshold || 80)) {
|
|
return {
|
|
success: false,
|
|
confidence,
|
|
warnings: [`Confidence score ${confidence}% is below threshold ${options.confidenceThreshold || 80}%`]
|
|
};
|
|
}
|
|
|
|
// Validate invoice before booking
|
|
if (!options?.skipValidation) {
|
|
const validationErrors = this.validateInvoice(invoice);
|
|
if (validationErrors.length > 0) {
|
|
return {
|
|
success: false,
|
|
confidence,
|
|
errors: validationErrors
|
|
};
|
|
}
|
|
}
|
|
|
|
// Build journal entry
|
|
const journalEntry = await this.buildJournalEntry(invoice, rules, options);
|
|
|
|
// Create booking info
|
|
const bookingInfo: IBookingInfo = {
|
|
journalEntryId: journalEntry.id,
|
|
transactionIds: journalEntry.transactionIds || [],
|
|
bookedAt: new Date(),
|
|
bookedBy: 'system',
|
|
bookingRules: {
|
|
vendorAccount: rules.vendorControlAccount,
|
|
customerAccount: rules.customerControlAccount,
|
|
expenseAccounts: this.getUsedExpenseAccounts(invoice, rules),
|
|
revenueAccounts: this.getUsedRevenueAccounts(invoice, rules),
|
|
vatAccounts: this.getUsedVATAccounts(invoice, rules)
|
|
},
|
|
confidence,
|
|
autoBooked: options?.autoBook || false
|
|
};
|
|
|
|
// Post the journal entry
|
|
// TODO: When MongoDB transactions are available, wrap this in a transaction
|
|
// Example: await db.withTransaction(async (session) => { ... })
|
|
try {
|
|
await journalEntry.validate();
|
|
await journalEntry.post();
|
|
|
|
// Mark invoice as posted if we have a reference to it
|
|
if (invoice.status !== 'posted') {
|
|
invoice.status = 'posted';
|
|
}
|
|
} catch (postError) {
|
|
this.logger.log('error', `Failed to post journal entry: ${postError}`);
|
|
throw postError; // Re-throw to trigger rollback when transactions are available
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
journalEntry,
|
|
bookingInfo,
|
|
confidence,
|
|
warnings: this.generateWarnings(invoice, rules)
|
|
};
|
|
} catch (error) {
|
|
this.logger.log('error', `Failed to book invoice: ${error}`);
|
|
return {
|
|
success: false,
|
|
confidence: 0,
|
|
errors: [`Booking failed: ${error.message}`]
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build journal entry from invoice
|
|
*/
|
|
private async buildJournalEntry(
|
|
invoice: IInvoice,
|
|
rules: IBookingRules,
|
|
options?: IBookingOptions
|
|
): Promise<JournalEntry> {
|
|
const lines: IJournalEntryLine[] = [];
|
|
const isInbound = invoice.direction === 'inbound';
|
|
const isCredit = invoice.invoiceTypeCode === '381'; // Credit note
|
|
|
|
// Determine if we need to reverse the normal booking direction
|
|
const reverseDirection = isCredit;
|
|
|
|
if (isInbound) {
|
|
// Inbound invoice (AP)
|
|
lines.push(...this.buildAPEntry(invoice, rules, reverseDirection));
|
|
} else {
|
|
// Outbound invoice (AR)
|
|
lines.push(...this.buildAREntry(invoice, rules, reverseDirection));
|
|
}
|
|
|
|
// Create journal entry
|
|
const journalData: IJournalEntry = {
|
|
date: options?.bookingDate || invoice.issueDate,
|
|
description: this.buildDescription(invoice),
|
|
reference: options?.bookingReference || invoice.invoiceNumber,
|
|
lines,
|
|
skrType: this.skrType
|
|
};
|
|
|
|
const journalEntry = new JournalEntry(journalData);
|
|
return journalEntry;
|
|
}
|
|
|
|
/**
|
|
* Build AP (Accounts Payable) journal entry lines
|
|
*/
|
|
private buildAPEntry(
|
|
invoice: IInvoice,
|
|
rules: IBookingRules,
|
|
reverseDirection: boolean
|
|
): IJournalEntryLine[] {
|
|
const lines: IJournalEntryLine[] = [];
|
|
|
|
// Group lines by account
|
|
const accountGroups = this.groupLinesByAccount(invoice, rules);
|
|
|
|
// Create expense/asset entries
|
|
for (const [accountNumber, group] of Object.entries(accountGroups)) {
|
|
const amount = group.reduce((sum, line) => sum + line.netAmount, 0);
|
|
|
|
if (reverseDirection) {
|
|
// Credit note: credit expense account
|
|
lines.push({
|
|
accountNumber,
|
|
credit: Math.abs(amount),
|
|
description: this.getAccountDescription(accountNumber, group)
|
|
});
|
|
} else {
|
|
// Regular invoice: debit expense account
|
|
lines.push({
|
|
accountNumber,
|
|
debit: Math.abs(amount),
|
|
description: this.getAccountDescription(accountNumber, group)
|
|
});
|
|
}
|
|
}
|
|
|
|
// Create VAT entries
|
|
const vatLines = this.buildVATLines(invoice, rules, 'input', reverseDirection);
|
|
lines.push(...vatLines);
|
|
|
|
// Create vendor control account entry
|
|
const controlAccount = this.mapper.getControlAccount(invoice, rules);
|
|
const totalAmount = Math.abs(invoice.payableAmount);
|
|
|
|
if (reverseDirection) {
|
|
// Credit note: debit vendor account
|
|
lines.push({
|
|
accountNumber: controlAccount,
|
|
debit: totalAmount,
|
|
description: `${invoice.supplier.name} - Credit Note ${invoice.invoiceNumber}`
|
|
});
|
|
} else {
|
|
// Regular invoice: credit vendor account
|
|
lines.push({
|
|
accountNumber: controlAccount,
|
|
credit: totalAmount,
|
|
description: `${invoice.supplier.name} - Invoice ${invoice.invoiceNumber}`
|
|
});
|
|
}
|
|
|
|
return lines;
|
|
}
|
|
|
|
/**
|
|
* Build AR (Accounts Receivable) journal entry lines
|
|
*/
|
|
private buildAREntry(
|
|
invoice: IInvoice,
|
|
rules: IBookingRules,
|
|
reverseDirection: boolean
|
|
): IJournalEntryLine[] {
|
|
const lines: IJournalEntryLine[] = [];
|
|
|
|
// Group lines by account
|
|
const accountGroups = this.groupLinesByAccount(invoice, rules);
|
|
|
|
// Create revenue entries
|
|
for (const [accountNumber, group] of Object.entries(accountGroups)) {
|
|
const amount = group.reduce((sum, line) => sum + line.netAmount, 0);
|
|
|
|
if (reverseDirection) {
|
|
// Credit note: debit revenue account
|
|
lines.push({
|
|
accountNumber,
|
|
debit: Math.abs(amount),
|
|
description: this.getAccountDescription(accountNumber, group)
|
|
});
|
|
} else {
|
|
// Regular invoice: credit revenue account
|
|
lines.push({
|
|
accountNumber,
|
|
credit: Math.abs(amount),
|
|
description: this.getAccountDescription(accountNumber, group)
|
|
});
|
|
}
|
|
}
|
|
|
|
// Create VAT entries
|
|
const vatLines = this.buildVATLines(invoice, rules, 'output', reverseDirection);
|
|
lines.push(...vatLines);
|
|
|
|
// Create customer control account entry
|
|
const controlAccount = this.mapper.getControlAccount(invoice, rules);
|
|
const totalAmount = Math.abs(invoice.payableAmount);
|
|
|
|
if (reverseDirection) {
|
|
// Credit note: credit customer account
|
|
lines.push({
|
|
accountNumber: controlAccount,
|
|
credit: totalAmount,
|
|
description: `${invoice.customer.name} - Credit Note ${invoice.invoiceNumber}`
|
|
});
|
|
} else {
|
|
// Regular invoice: debit customer account
|
|
lines.push({
|
|
accountNumber: controlAccount,
|
|
debit: totalAmount,
|
|
description: `${invoice.customer.name} - Invoice ${invoice.invoiceNumber}`
|
|
});
|
|
}
|
|
|
|
return lines;
|
|
}
|
|
|
|
/**
|
|
* Build VAT lines
|
|
*/
|
|
private buildVATLines(
|
|
invoice: IInvoice,
|
|
rules: IBookingRules,
|
|
direction: 'input' | 'output',
|
|
reverseDirection: boolean
|
|
): IJournalEntryLine[] {
|
|
const lines: IJournalEntryLine[] = [];
|
|
const taxScenario = invoice.taxScenario || 'domestic_taxed';
|
|
|
|
// Handle reverse charge specially
|
|
if (taxScenario === 'reverse_charge') {
|
|
return this.buildReverseChargeVATLines(invoice, rules);
|
|
}
|
|
|
|
// Standard VAT booking
|
|
for (const vatBreak of invoice.vatBreakdown) {
|
|
if (vatBreak.taxAmount === 0) continue;
|
|
|
|
const vatAccount = this.mapper.getVATAccount(
|
|
vatBreak.vatCategory,
|
|
direction,
|
|
taxScenario
|
|
);
|
|
|
|
const amount = Math.abs(vatBreak.taxAmount);
|
|
const description = `VAT ${vatBreak.vatCategory.rate}%`;
|
|
|
|
if (direction === 'input') {
|
|
// Input VAT (Vorsteuer)
|
|
if (reverseDirection) {
|
|
lines.push({ accountNumber: vatAccount, credit: amount, description });
|
|
} else {
|
|
lines.push({ accountNumber: vatAccount, debit: amount, description });
|
|
}
|
|
} else {
|
|
// Output VAT (Umsatzsteuer)
|
|
if (reverseDirection) {
|
|
lines.push({ accountNumber: vatAccount, debit: amount, description });
|
|
} else {
|
|
lines.push({ accountNumber: vatAccount, credit: amount, description });
|
|
}
|
|
}
|
|
}
|
|
|
|
return lines;
|
|
}
|
|
|
|
/**
|
|
* Calculate VAT amount from taxable amount and rate
|
|
*/
|
|
private calculateVAT(taxableAmount: number, rate: number): number {
|
|
return Math.round(taxableAmount * rate / 100 * 100) / 100; // Round to 2 decimals
|
|
}
|
|
|
|
/**
|
|
* Calculate effective VAT rate for the invoice (weighted average)
|
|
*/
|
|
private calculateEffectiveVATRate(invoice: IInvoice): number {
|
|
const totalTaxable = invoice.vatBreakdown.reduce((sum, vb) => sum + vb.taxableAmount, 0);
|
|
if (totalTaxable === 0) {
|
|
return 19; // Default to standard German VAT rate
|
|
}
|
|
|
|
// Calculate weighted average VAT rate
|
|
const weightedRate = invoice.vatBreakdown.reduce((sum, vb) => {
|
|
return sum + (vb.vatCategory.rate * vb.taxableAmount);
|
|
}, 0);
|
|
|
|
return Math.round(weightedRate / totalTaxable * 100) / 100;
|
|
}
|
|
|
|
/**
|
|
* Build reverse charge VAT lines (§13b UStG)
|
|
*/
|
|
private buildReverseChargeVATLines(
|
|
invoice: IInvoice,
|
|
rules: IBookingRules
|
|
): IJournalEntryLine[] {
|
|
const lines: IJournalEntryLine[] = [];
|
|
|
|
// For reverse charge, we book both input and output VAT
|
|
for (const vatBreak of invoice.vatBreakdown) {
|
|
// For reverse charge, calculate VAT if not provided
|
|
const amount = vatBreak.taxAmount > 0
|
|
? Math.abs(vatBreak.taxAmount)
|
|
: this.calculateVAT(Math.abs(vatBreak.taxableAmount), vatBreak.vatCategory.rate);
|
|
|
|
// Input VAT (deductible)
|
|
const inputVATAccount = this.mapper.getVATAccount(
|
|
vatBreak.vatCategory,
|
|
'input',
|
|
'reverse_charge'
|
|
);
|
|
|
|
// Output VAT (payable)
|
|
const outputVATAccount = this.mapper.getVATAccount(
|
|
vatBreak.vatCategory,
|
|
'output',
|
|
'reverse_charge'
|
|
);
|
|
|
|
lines.push(
|
|
{
|
|
accountNumber: inputVATAccount,
|
|
debit: amount,
|
|
description: `Reverse charge input VAT ${vatBreak.vatCategory.rate}%`
|
|
},
|
|
{
|
|
accountNumber: outputVATAccount,
|
|
credit: amount,
|
|
description: `Reverse charge output VAT ${vatBreak.vatCategory.rate}%`
|
|
}
|
|
);
|
|
}
|
|
|
|
return lines;
|
|
}
|
|
|
|
/**
|
|
* Group invoice lines by account
|
|
*/
|
|
private groupLinesByAccount(
|
|
invoice: IInvoice,
|
|
rules: IBookingRules
|
|
): Record<string, IInvoiceLine[]> {
|
|
const groups: Record<string, IInvoiceLine[]> = {};
|
|
|
|
for (const line of invoice.lines) {
|
|
const account = this.mapper.mapInvoiceLineToAccount(line, invoice, rules);
|
|
|
|
if (!groups[account]) {
|
|
groups[account] = [];
|
|
}
|
|
groups[account].push(line);
|
|
}
|
|
|
|
return groups;
|
|
}
|
|
|
|
/**
|
|
* Book payment for an invoice
|
|
*/
|
|
public async bookPayment(
|
|
invoice: IInvoice,
|
|
payment: IPaymentInfo,
|
|
rules: IBookingRules
|
|
): Promise<IBookingResult> {
|
|
try {
|
|
const lines: IJournalEntryLine[] = [];
|
|
const isInbound = invoice.direction === 'inbound';
|
|
const controlAccount = this.mapper.getControlAccount(invoice, rules);
|
|
|
|
// Check for skonto
|
|
const skontoAmount = payment.skontoTaken || 0;
|
|
const paymentAmount = payment.amount;
|
|
const fullAmount = paymentAmount + skontoAmount;
|
|
|
|
if (isInbound) {
|
|
// Payment for vendor invoice
|
|
lines.push(
|
|
{
|
|
accountNumber: controlAccount,
|
|
debit: fullAmount,
|
|
description: `Payment to ${invoice.supplier.name}`
|
|
},
|
|
{
|
|
accountNumber: '1000', // Bank account (would be configurable)
|
|
credit: paymentAmount,
|
|
description: `Bank payment ${payment.endToEndId || payment.paymentId}`
|
|
}
|
|
);
|
|
|
|
// Book skonto if taken
|
|
if (skontoAmount > 0) {
|
|
const skontoAccounts = this.mapper.getSkontoAccounts(invoice);
|
|
lines.push({
|
|
accountNumber: skontoAccounts.skontoAccount,
|
|
credit: skontoAmount,
|
|
description: `Skonto received`
|
|
});
|
|
|
|
// VAT correction for skonto
|
|
if (rules.skontoMethod === 'gross') {
|
|
const effectiveRate = this.calculateEffectiveVATRate(invoice);
|
|
const vatCorrection = Math.round(skontoAmount * effectiveRate / (100 + effectiveRate) * 100) / 100;
|
|
lines.push(
|
|
{
|
|
accountNumber: skontoAccounts.vatCorrectionAccount,
|
|
credit: vatCorrection,
|
|
description: `Skonto VAT correction`
|
|
}
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
// Payment from customer
|
|
lines.push(
|
|
{
|
|
accountNumber: '1000', // Bank account
|
|
debit: paymentAmount,
|
|
description: `Payment from ${invoice.customer.name}`
|
|
},
|
|
{
|
|
accountNumber: controlAccount,
|
|
credit: fullAmount,
|
|
description: `Customer payment ${payment.endToEndId || payment.paymentId}`
|
|
}
|
|
);
|
|
|
|
// Book skonto if granted
|
|
if (skontoAmount > 0) {
|
|
const skontoAccounts = this.mapper.getSkontoAccounts(invoice);
|
|
lines.push({
|
|
accountNumber: skontoAccounts.skontoAccount,
|
|
debit: skontoAmount,
|
|
description: `Skonto granted`
|
|
});
|
|
|
|
// VAT correction for skonto
|
|
if (rules.skontoMethod === 'gross') {
|
|
const effectiveRate = this.calculateEffectiveVATRate(invoice);
|
|
const vatCorrection = Math.round(skontoAmount * effectiveRate / (100 + effectiveRate) * 100) / 100;
|
|
lines.push(
|
|
{
|
|
accountNumber: skontoAccounts.vatCorrectionAccount,
|
|
debit: vatCorrection,
|
|
description: `Skonto VAT correction`
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create journal entry for payment
|
|
const journalData: IJournalEntry = {
|
|
date: payment.paymentDate,
|
|
description: `Payment for invoice ${invoice.invoiceNumber}`,
|
|
reference: payment.endToEndId || payment.remittanceInfo || payment.paymentId,
|
|
lines,
|
|
skrType: this.skrType
|
|
};
|
|
|
|
const journalEntry = new JournalEntry(journalData);
|
|
await journalEntry.validate();
|
|
await journalEntry.post();
|
|
|
|
return {
|
|
success: true,
|
|
journalEntry,
|
|
confidence: 100
|
|
};
|
|
} catch (error) {
|
|
this.logger.log('error', `Failed to book payment: ${error}`);
|
|
return {
|
|
success: false,
|
|
confidence: 0,
|
|
errors: [`Payment booking failed: ${error.message}`]
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate invoice before booking
|
|
*/
|
|
private validateInvoice(invoice: IInvoice): string[] {
|
|
const errors: string[] = [];
|
|
|
|
// Check required fields
|
|
if (!invoice.invoiceNumber) {
|
|
errors.push('Invoice number is required');
|
|
}
|
|
|
|
if (!invoice.issueDate) {
|
|
errors.push('Issue date is required');
|
|
}
|
|
|
|
if (!invoice.supplier || !invoice.supplier.name) {
|
|
errors.push('Supplier information is required');
|
|
}
|
|
|
|
if (!invoice.customer || !invoice.customer.name) {
|
|
errors.push('Customer information is required');
|
|
}
|
|
|
|
if (invoice.lines.length === 0) {
|
|
errors.push('Invoice must have at least one line item');
|
|
}
|
|
|
|
// Validate amounts
|
|
const calculatedNet = invoice.lines.reduce((sum, line) => sum + line.netAmount, 0);
|
|
const tolerance = 0.01;
|
|
|
|
if (Math.abs(calculatedNet - invoice.lineNetAmount) > tolerance) {
|
|
errors.push(`Line net amount mismatch: calculated ${calculatedNet}, stated ${invoice.lineNetAmount}`);
|
|
}
|
|
|
|
// Validate VAT
|
|
const calculatedVAT = invoice.vatBreakdown.reduce((sum, vb) => sum + vb.taxAmount, 0);
|
|
if (Math.abs(calculatedVAT - invoice.totalVATAmount) > tolerance) {
|
|
errors.push(`VAT amount mismatch: calculated ${calculatedVAT}, stated ${invoice.totalVATAmount}`);
|
|
}
|
|
|
|
// Validate total
|
|
const calculatedTotal = invoice.taxExclusiveAmount + invoice.totalVATAmount;
|
|
if (Math.abs(calculatedTotal - invoice.taxInclusiveAmount) > tolerance) {
|
|
errors.push(`Total amount mismatch: calculated ${calculatedTotal}, stated ${invoice.taxInclusiveAmount}`);
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
/**
|
|
* Generate warnings for the booking
|
|
*/
|
|
private generateWarnings(invoice: IInvoice, rules: IBookingRules): string[] {
|
|
const warnings: string[] = [];
|
|
|
|
// Warn about default account usage
|
|
const hasDefaultAccounts = invoice.lines.some(line =>
|
|
!line.accountNumber && !line.productCode
|
|
);
|
|
if (hasDefaultAccounts) {
|
|
warnings.push('Some lines are using default expense/revenue accounts');
|
|
}
|
|
|
|
// Warn about mixed VAT rates
|
|
if (invoice.vatBreakdown.length > 1) {
|
|
warnings.push('Invoice contains mixed VAT rates');
|
|
}
|
|
|
|
// Warn about reverse charge
|
|
if (invoice.taxScenario === 'reverse_charge') {
|
|
warnings.push('Reverse charge procedure applied - verify VAT treatment');
|
|
}
|
|
|
|
// Warn about credit notes
|
|
if (invoice.invoiceTypeCode === '381') {
|
|
warnings.push('This is a credit note - amounts will be reversed');
|
|
}
|
|
|
|
// Warn about foreign currency
|
|
if (invoice.currencyCode !== 'EUR') {
|
|
warnings.push(`Invoice is in foreign currency: ${invoice.currencyCode}`);
|
|
}
|
|
|
|
return warnings;
|
|
}
|
|
|
|
/**
|
|
* Build description for journal entry
|
|
*/
|
|
private buildDescription(invoice: IInvoice): string {
|
|
const type = invoice.invoiceTypeCode === '381' ? 'Credit Note' : 'Invoice';
|
|
const party = invoice.direction === 'inbound'
|
|
? invoice.supplier.name
|
|
: invoice.customer.name;
|
|
|
|
return `${type} ${invoice.invoiceNumber} - ${party}`;
|
|
}
|
|
|
|
/**
|
|
* Get account description for a group of lines
|
|
*/
|
|
private getAccountDescription(accountNumber: string, lines: IInvoiceLine[]): string {
|
|
if (lines.length === 1) {
|
|
return lines[0].description;
|
|
}
|
|
|
|
return `${this.mapper.getAccountDescription(accountNumber)} (${lines.length} items)`;
|
|
}
|
|
|
|
/**
|
|
* Get used expense accounts
|
|
*/
|
|
private getUsedExpenseAccounts(invoice: IInvoice, rules: IBookingRules): string[] {
|
|
if (invoice.direction !== 'inbound') return [];
|
|
|
|
const accounts = new Set<string>();
|
|
for (const line of invoice.lines) {
|
|
const account = this.mapper.mapInvoiceLineToAccount(line, invoice, rules);
|
|
accounts.add(account);
|
|
}
|
|
return Array.from(accounts);
|
|
}
|
|
|
|
/**
|
|
* Get used revenue accounts
|
|
*/
|
|
private getUsedRevenueAccounts(invoice: IInvoice, rules: IBookingRules): string[] {
|
|
if (invoice.direction !== 'outbound') return [];
|
|
|
|
const accounts = new Set<string>();
|
|
for (const line of invoice.lines) {
|
|
const account = this.mapper.mapInvoiceLineToAccount(line, invoice, rules);
|
|
accounts.add(account);
|
|
}
|
|
return Array.from(accounts);
|
|
}
|
|
|
|
/**
|
|
* Get used VAT accounts
|
|
*/
|
|
private getUsedVATAccounts(invoice: IInvoice, rules: IBookingRules): string[] {
|
|
const accounts = new Set<string>();
|
|
const direction = invoice.direction === 'inbound' ? 'input' : 'output';
|
|
const taxScenario = invoice.taxScenario || 'domestic_taxed';
|
|
|
|
for (const vatBreak of invoice.vatBreakdown) {
|
|
const account = this.mapper.getVATAccount(
|
|
vatBreak.vatCategory,
|
|
direction,
|
|
taxScenario
|
|
);
|
|
accounts.add(account);
|
|
}
|
|
|
|
// Add reverse charge accounts if applicable
|
|
if (taxScenario === 'reverse_charge') {
|
|
for (const vatBreak of invoice.vatBreakdown) {
|
|
const inputAccount = this.mapper.getVATAccount(
|
|
vatBreak.vatCategory,
|
|
'input',
|
|
'reverse_charge'
|
|
);
|
|
const outputAccount = this.mapper.getVATAccount(
|
|
vatBreak.vatCategory,
|
|
'output',
|
|
'reverse_charge'
|
|
);
|
|
accounts.add(inputAccount);
|
|
accounts.add(outputAccount);
|
|
}
|
|
}
|
|
|
|
return Array.from(accounts);
|
|
}
|
|
} |