Files
skr/ts/skr.export.ledger.ts
Juergen Kunz 73b46f7857
Some checks failed
Default (tags) / security (push) Successful in 48s
Default (tags) / test (push) Failing after 4m3s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
feat(invoice): add e-invoice support with XRechnung/ZUGFeRD and advanced export features
2025-08-12 12:37:01 +00:00

249 lines
6.8 KiB
TypeScript

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;
}
}