270 lines
7.7 KiB
TypeScript
270 lines
7.7 KiB
TypeScript
|
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 = [];
|
||
|
}
|
||
|
}
|