feat(invoice): add e-invoice support with XRechnung/ZUGFeRD and advanced export features
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

This commit is contained in:
2025-08-12 12:37:01 +00:00
parent 08d7803be2
commit 73b46f7857
19 changed files with 6211 additions and 20 deletions

270
ts/skr.export.balances.ts Normal file
View File

@@ -0,0 +1,270 @@
import * as plugins from './plugins.js';
import * as path from 'path';
import type { IAccountBalance } from './skr.types.js';
// Extended interface for export with additional fields
export interface IAccountBalanceExport extends IAccountBalance {
openingBalance?: number;
transactionCount?: number;
}
export interface IBalanceExportRow {
account_code: string;
account_name: string;
fiscal_year: number;
period?: string;
opening_balance: string;
closing_balance: string;
debit_sum: string;
credit_sum: string;
balance: string;
transaction_count: number;
}
export class BalancesExporter {
private exportPath: string;
private balances: IBalanceExportRow[] = [];
private fiscalYear: number;
constructor(exportPath: string, fiscalYear: number) {
this.exportPath = exportPath;
this.fiscalYear = fiscalYear;
}
/**
* Adds a balance entry to the export
*/
public addBalance(
accountCode: string,
accountName: string,
balance: IAccountBalanceExport,
period?: string
): void {
const exportRow: IBalanceExportRow = {
account_code: accountCode,
account_name: accountName,
fiscal_year: this.fiscalYear,
period: period,
opening_balance: (balance.openingBalance || 0).toFixed(2),
closing_balance: balance.balance.toFixed(2),
debit_sum: balance.debitTotal.toFixed(2),
credit_sum: balance.creditTotal.toFixed(2),
balance: balance.balance.toFixed(2),
transaction_count: balance.transactionCount || 0
};
this.balances.push(exportRow);
}
/**
* Exports balances to CSV format
*/
public async exportToCSV(): Promise<void> {
const csvPath = path.join(this.exportPath, 'data', 'accounting', 'balances.csv');
await plugins.smartfile.fs.ensureDir(path.dirname(csvPath));
// Create CSV header
const headers = [
'account_code',
'account_name',
'fiscal_year',
'period',
'opening_balance',
'closing_balance',
'debit_sum',
'credit_sum',
'balance',
'transaction_count'
];
let csvContent = headers.join(',') + '\n';
// Sort balances by account code
this.balances.sort((a, b) => a.account_code.localeCompare(b.account_code));
// Add balance rows
for (const balance of this.balances) {
const row = [
this.escapeCSV(balance.account_code),
this.escapeCSV(balance.account_name),
balance.fiscal_year.toString(),
this.escapeCSV(balance.period || ''),
balance.opening_balance,
balance.closing_balance,
balance.debit_sum,
balance.credit_sum,
balance.balance,
balance.transaction_count.toString()
];
csvContent += row.join(',') + '\n';
}
await plugins.smartfile.memory.toFs(csvContent, csvPath);
}
/**
* Exports trial balance (Summen- und Saldenliste)
*/
public async exportTrialBalance(): Promise<void> {
const csvPath = path.join(this.exportPath, 'data', 'accounting', 'trial_balance.csv');
await plugins.smartfile.fs.ensureDir(path.dirname(csvPath));
// Create CSV header for trial balance
const headers = [
'Konto',
'Bezeichnung',
'Anfangssaldo',
'Soll',
'Haben',
'Saldo',
'Endsaldo'
];
let csvContent = headers.join(',') + '\n';
// Add rows with German formatting
for (const balance of this.balances) {
const row = [
this.escapeCSV(balance.account_code),
this.escapeCSV(balance.account_name),
this.formatGermanNumber(parseFloat(balance.opening_balance)),
this.formatGermanNumber(parseFloat(balance.debit_sum)),
this.formatGermanNumber(parseFloat(balance.credit_sum)),
this.formatGermanNumber(parseFloat(balance.debit_sum) - parseFloat(balance.credit_sum)),
this.formatGermanNumber(parseFloat(balance.closing_balance))
];
csvContent += row.join(',') + '\n';
}
// Add totals row
const totalDebit = this.balances.reduce((sum, b) => sum + parseFloat(b.debit_sum), 0);
const totalCredit = this.balances.reduce((sum, b) => sum + parseFloat(b.credit_sum), 0);
csvContent += '\n';
csvContent += [
'SUMME',
'',
'',
this.formatGermanNumber(totalDebit),
this.formatGermanNumber(totalCredit),
this.formatGermanNumber(totalDebit - totalCredit),
''
].join(',') + '\n';
await plugins.smartfile.memory.toFs(csvContent, csvPath);
}
/**
* Exports balances to JSON format
*/
public async exportToJSON(): Promise<void> {
const jsonPath = path.join(this.exportPath, 'data', 'accounting', 'balances.json');
await plugins.smartfile.fs.ensureDir(path.dirname(jsonPath));
const jsonData = {
schema_version: '1.0',
export_date: new Date().toISOString(),
fiscal_year: this.fiscalYear,
balances: this.balances,
totals: {
total_debit: this.balances.reduce((sum, b) => sum + parseFloat(b.debit_sum), 0).toFixed(2),
total_credit: this.balances.reduce((sum, b) => sum + parseFloat(b.credit_sum), 0).toFixed(2),
account_count: this.balances.length
}
};
await plugins.smartfile.memory.toFs(
JSON.stringify(jsonData, null, 2),
jsonPath
);
}
/**
* Generates balance summary for specific account classes
*/
public async exportClassSummary(): Promise<void> {
const csvPath = path.join(this.exportPath, 'data', 'accounting', 'class_summary.csv');
await plugins.smartfile.fs.ensureDir(path.dirname(csvPath));
// Group balances by account class (first digit of account code)
const classSummary: { [key: string]: { debit: number; credit: number; balance: number } } = {};
for (const balance of this.balances) {
const accountClass = balance.account_code.charAt(0);
if (!classSummary[accountClass]) {
classSummary[accountClass] = { debit: 0, credit: 0, balance: 0 };
}
classSummary[accountClass].debit += parseFloat(balance.debit_sum);
classSummary[accountClass].credit += parseFloat(balance.credit_sum);
classSummary[accountClass].balance += parseFloat(balance.balance);
}
// Create CSV
let csvContent = 'Kontenklasse,Bezeichnung,Soll,Haben,Saldo\n';
const classNames: { [key: string]: string } = {
'0': 'Anlagevermögen',
'1': 'Umlaufvermögen',
'2': 'Eigenkapital',
'3': 'Fremdkapital',
'4': 'Betriebliche Erträge',
'5': 'Materialaufwand',
'6': 'Betriebsaufwand',
'7': 'Weitere Aufwendungen',
'8': 'Erträge',
'9': 'Abschlusskonten'
};
for (const [classNum, summary] of Object.entries(classSummary)) {
const row = [
classNum,
this.escapeCSV(classNames[classNum] || `Klasse ${classNum}`),
this.formatGermanNumber(summary.debit),
this.formatGermanNumber(summary.credit),
this.formatGermanNumber(summary.balance)
];
csvContent += row.join(',') + '\n';
}
await plugins.smartfile.memory.toFs(csvContent, csvPath);
}
/**
* Escapes CSV values
*/
private escapeCSV(value: string): string {
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
}
/**
* Formats number in German format (1.234,56)
*/
private formatGermanNumber(value: number): string {
return value.toLocaleString('de-DE', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
}
/**
* Gets the number of balance entries
*/
public getBalanceCount(): number {
return this.balances.length;
}
/**
* Clears the balances list
*/
public clear(): void {
this.balances = [];
}
}