feat(invoice): add e-invoice support with XRechnung/ZUGFeRD and advanced export features
This commit is contained in:
249
ts/skr.export.ledger.ts
Normal file
249
ts/skr.export.ledger.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as path from 'path';
|
||||
import type { ITransactionData, IJournalEntry, IJournalEntryLine } from './skr.types.js';
|
||||
import { createWriteStream, type WriteStream } from 'fs';
|
||||
|
||||
// Extended interfaces for export with additional tracking fields
|
||||
export interface ITransactionDataExport extends ITransactionData {
|
||||
_id?: string;
|
||||
postingDate?: Date;
|
||||
currency?: string;
|
||||
createdAt?: Date | string;
|
||||
modifiedAt?: Date | string;
|
||||
reversalOf?: string;
|
||||
reversedBy?: string;
|
||||
taxCode?: string;
|
||||
project?: string;
|
||||
vatAccount?: string;
|
||||
}
|
||||
|
||||
export interface IJournalEntryExport extends IJournalEntry {
|
||||
_id?: string;
|
||||
postingDate?: Date;
|
||||
currency?: string;
|
||||
journal?: string;
|
||||
createdAt?: Date | string;
|
||||
modifiedAt?: Date | string;
|
||||
reversalOf?: string;
|
||||
reversedBy?: string;
|
||||
}
|
||||
|
||||
export interface IJournalEntryLineExport extends IJournalEntryLine {
|
||||
taxCode?: string;
|
||||
project?: string;
|
||||
}
|
||||
|
||||
export interface ILedgerEntry {
|
||||
schema_version: string;
|
||||
entry_id: string;
|
||||
booking_date: string;
|
||||
posting_date: string;
|
||||
period?: string;
|
||||
currency: string;
|
||||
journal: string;
|
||||
description: string;
|
||||
reference?: string;
|
||||
lines: ILedgerLine[];
|
||||
document_refs?: IDocumentRef[];
|
||||
created_at: string;
|
||||
modified_at?: string;
|
||||
user?: string;
|
||||
reversal_of?: string;
|
||||
reversed_by?: string;
|
||||
}
|
||||
|
||||
export interface ILedgerLine {
|
||||
posting_id: string;
|
||||
account_code: string;
|
||||
debit: string;
|
||||
credit: string;
|
||||
tax_code?: string;
|
||||
cost_center?: string;
|
||||
project?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface IDocumentRef {
|
||||
content_hash: string;
|
||||
doc_role: 'invoice' | 'receipt' | 'contract' | 'bank-statement' | 'other';
|
||||
doc_mime: string;
|
||||
doc_original_name?: string;
|
||||
}
|
||||
|
||||
export class LedgerExporter {
|
||||
private exportPath: string;
|
||||
private stream: WriteStream | null = null;
|
||||
private entryCount: number = 0;
|
||||
|
||||
constructor(exportPath: string) {
|
||||
this.exportPath = exportPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the NDJSON export stream
|
||||
*/
|
||||
public async initialize(): Promise<void> {
|
||||
const ledgerPath = path.join(this.exportPath, 'data', 'accounting', 'ledger.ndjson');
|
||||
await plugins.smartfile.fs.ensureDir(path.dirname(ledgerPath));
|
||||
|
||||
this.stream = createWriteStream(ledgerPath, {
|
||||
encoding: 'utf8',
|
||||
flags: 'w'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a transaction as a ledger entry
|
||||
*/
|
||||
public async exportTransaction(transaction: ITransactionDataExport): Promise<void> {
|
||||
if (!this.stream) {
|
||||
throw new Error('Ledger exporter not initialized');
|
||||
}
|
||||
|
||||
const entry: ILedgerEntry = {
|
||||
schema_version: '1.0',
|
||||
entry_id: transaction._id || plugins.smartunique.shortId(),
|
||||
booking_date: this.formatDate(transaction.date),
|
||||
posting_date: this.formatDate(transaction.postingDate || transaction.date),
|
||||
currency: transaction.currency || 'EUR',
|
||||
journal: 'GL',
|
||||
description: transaction.description,
|
||||
reference: transaction.reference,
|
||||
lines: [],
|
||||
created_at: transaction.createdAt ? new Date(transaction.createdAt).toISOString() : new Date().toISOString(),
|
||||
modified_at: transaction.modifiedAt ? new Date(transaction.modifiedAt).toISOString() : undefined,
|
||||
reversal_of: transaction.reversalOf,
|
||||
reversed_by: transaction.reversedBy
|
||||
};
|
||||
|
||||
// Add debit line
|
||||
if (transaction.amount > 0) {
|
||||
entry.lines.push({
|
||||
posting_id: `${entry.entry_id}-1`,
|
||||
account_code: transaction.debitAccount,
|
||||
debit: transaction.amount.toFixed(2),
|
||||
credit: '0.00',
|
||||
tax_code: transaction.taxCode,
|
||||
cost_center: transaction.costCenter,
|
||||
project: transaction.project
|
||||
});
|
||||
|
||||
// Add credit line
|
||||
entry.lines.push({
|
||||
posting_id: `${entry.entry_id}-2`,
|
||||
account_code: transaction.creditAccount,
|
||||
debit: '0.00',
|
||||
credit: transaction.amount.toFixed(2)
|
||||
});
|
||||
}
|
||||
|
||||
// Add VAT lines if applicable
|
||||
if (transaction.vatAmount && transaction.vatAmount > 0) {
|
||||
entry.lines.push({
|
||||
posting_id: `${entry.entry_id}-3`,
|
||||
account_code: transaction.vatAccount || '1576', // Default VAT account
|
||||
debit: transaction.vatAmount.toFixed(2),
|
||||
credit: '0.00',
|
||||
description: 'Vorsteuer'
|
||||
});
|
||||
}
|
||||
|
||||
await this.writeLine(entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a journal entry
|
||||
*/
|
||||
public async exportJournalEntry(journalEntry: IJournalEntryExport): Promise<void> {
|
||||
if (!this.stream) {
|
||||
throw new Error('Ledger exporter not initialized');
|
||||
}
|
||||
|
||||
const entry: ILedgerEntry = {
|
||||
schema_version: '1.0',
|
||||
entry_id: journalEntry._id || plugins.smartunique.shortId(),
|
||||
booking_date: this.formatDate(journalEntry.date),
|
||||
posting_date: this.formatDate(journalEntry.postingDate || journalEntry.date),
|
||||
currency: journalEntry.currency || 'EUR',
|
||||
journal: journalEntry.journal || 'GL',
|
||||
description: journalEntry.description,
|
||||
reference: journalEntry.reference,
|
||||
lines: [],
|
||||
created_at: journalEntry.createdAt ? new Date(journalEntry.createdAt).toISOString() : new Date().toISOString(),
|
||||
modified_at: journalEntry.modifiedAt ? new Date(journalEntry.modifiedAt).toISOString() : undefined,
|
||||
reversal_of: journalEntry.reversalOf,
|
||||
reversed_by: journalEntry.reversedBy
|
||||
};
|
||||
|
||||
// Convert journal entry lines
|
||||
journalEntry.lines.forEach((line, index) => {
|
||||
const extLine = line as IJournalEntryLineExport;
|
||||
entry.lines.push({
|
||||
posting_id: `${entry.entry_id}-${index + 1}`,
|
||||
account_code: line.accountNumber,
|
||||
debit: (line.debit || 0).toFixed(2),
|
||||
credit: (line.credit || 0).toFixed(2),
|
||||
tax_code: extLine.taxCode,
|
||||
cost_center: line.costCenter,
|
||||
project: extLine.project,
|
||||
description: line.description
|
||||
});
|
||||
});
|
||||
|
||||
await this.writeLine(entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a single NDJSON line
|
||||
*/
|
||||
private async writeLine(entry: ILedgerEntry): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.stream) {
|
||||
reject(new Error('Stream not initialized'));
|
||||
return;
|
||||
}
|
||||
|
||||
const line = JSON.stringify(entry) + '\n';
|
||||
this.stream.write(line, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
this.entryCount++;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a date to ISO date string
|
||||
*/
|
||||
private formatDate(date: Date | string): string {
|
||||
if (typeof date === 'string') {
|
||||
return date.split('T')[0];
|
||||
}
|
||||
return date.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the export stream
|
||||
*/
|
||||
public async close(): Promise<number> {
|
||||
return new Promise((resolve) => {
|
||||
if (this.stream) {
|
||||
this.stream.end(() => {
|
||||
resolve(this.entryCount);
|
||||
});
|
||||
} else {
|
||||
resolve(this.entryCount);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of exported entries
|
||||
*/
|
||||
public getEntryCount(): number {
|
||||
return this.entryCount;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user