Files
skr/ts/skr.classes.reports.ts
Juergen Kunz 8a9056e767
Some checks failed
Default (tags) / security (push) Successful in 44s
Default (tags) / test (push) Failing after 4m4s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
feat(core): initial release of SKR03/SKR04 German accounting standards implementation
- Complete implementation of German standard charts of accounts
- SKR03 (Process Structure Principle) for trading/service companies
- SKR04 (Financial Classification Principle) for manufacturing companies
- Double-entry bookkeeping with MongoDB persistence
- Comprehensive reporting suite with DATEV export
- Full TypeScript support and type safety
2025-08-09 12:00:40 +00:00

722 lines
20 KiB
TypeScript

import * as plugins from './plugins.js';
import { Account } from './skr.classes.account.js';
import { Transaction } from './skr.classes.transaction.js';
import { Ledger } from './skr.classes.ledger.js';
import type {
TSKRType,
ITrialBalanceReport,
ITrialBalanceEntry,
IIncomeStatement,
IIncomeStatementEntry,
IBalanceSheet,
IBalanceSheetEntry,
IReportParams,
} from './skr.types.js';
export class Reports {
private logger: plugins.smartlog.Smartlog;
private ledger: Ledger;
constructor(private skrType: TSKRType) {
this.logger = new plugins.smartlog.Smartlog({
logContext: {
company: 'fin.cx',
companyunit: 'skr',
containerName: 'Reports',
environment: 'local',
runtime: 'node',
zone: 'local',
},
});
this.ledger = new Ledger(skrType);
}
/**
* Generate Trial Balance
*/
public async getTrialBalance(
params?: IReportParams,
): Promise<ITrialBalanceReport> {
this.logger.log('info', 'Generating trial balance');
const accounts = await Account.getInstances({
skrType: this.skrType,
isActive: true,
});
const entries: ITrialBalanceEntry[] = [];
let totalDebits = 0;
let totalCredits = 0;
for (const account of accounts) {
// Get balance for the period if specified
const balance = params?.dateTo
? await this.ledger.getAccountBalance(
account.accountNumber,
params.dateTo,
)
: await this.ledger.getAccountBalance(account.accountNumber);
if (balance.debitTotal !== 0 || balance.creditTotal !== 0) {
const entry: ITrialBalanceEntry = {
accountNumber: account.accountNumber,
accountName: account.accountName,
debitBalance: balance.debitTotal,
creditBalance: balance.creditTotal,
netBalance: balance.balance,
};
entries.push(entry);
totalDebits += balance.debitTotal;
totalCredits += balance.creditTotal;
}
}
// Sort entries by account number
entries.sort((a, b) => a.accountNumber.localeCompare(b.accountNumber));
const report: ITrialBalanceReport = {
date: params?.dateTo || new Date(),
skrType: this.skrType,
entries,
totalDebits,
totalCredits,
isBalanced: Math.abs(totalDebits - totalCredits) < 0.01,
};
this.logger.log(
'info',
`Trial balance generated with ${entries.length} accounts`,
);
return report;
}
/**
* Generate Income Statement (P&L)
*/
public async getIncomeStatement(
params?: IReportParams,
): Promise<IIncomeStatement> {
this.logger.log('info', 'Generating income statement');
// Get revenue accounts
const revenueAccounts = await Account.getAccountsByType(
'revenue',
this.skrType,
);
const expenseAccounts = await Account.getAccountsByType(
'expense',
this.skrType,
);
const revenueEntries: IIncomeStatementEntry[] = [];
const expenseEntries: IIncomeStatementEntry[] = [];
let totalRevenue = 0;
let totalExpenses = 0;
// Process revenue accounts
for (const account of revenueAccounts) {
const balance = await this.getAccountBalanceForPeriod(account, params);
if (balance !== 0) {
const entry: IIncomeStatementEntry = {
accountNumber: account.accountNumber,
accountName: account.accountName,
amount: Math.abs(balance),
};
revenueEntries.push(entry);
totalRevenue += Math.abs(balance);
}
}
// Process expense accounts
for (const account of expenseAccounts) {
const balance = await this.getAccountBalanceForPeriod(account, params);
if (balance !== 0) {
const entry: IIncomeStatementEntry = {
accountNumber: account.accountNumber,
accountName: account.accountName,
amount: Math.abs(balance),
};
expenseEntries.push(entry);
totalExpenses += Math.abs(balance);
}
}
// Calculate percentages
revenueEntries.forEach((entry) => {
entry.percentage =
totalRevenue > 0 ? (entry.amount / totalRevenue) * 100 : 0;
});
expenseEntries.forEach((entry) => {
entry.percentage =
totalRevenue > 0 ? (entry.amount / totalRevenue) * 100 : 0;
});
// Sort entries by account number
revenueEntries.sort((a, b) =>
a.accountNumber.localeCompare(b.accountNumber),
);
expenseEntries.sort((a, b) =>
a.accountNumber.localeCompare(b.accountNumber),
);
const report: IIncomeStatement = {
date: params?.dateTo || new Date(),
skrType: this.skrType,
revenue: revenueEntries,
expenses: expenseEntries,
totalRevenue,
totalExpenses,
netIncome: totalRevenue - totalExpenses,
};
this.logger.log(
'info',
`Income statement generated: Revenue ${totalRevenue}, Expenses ${totalExpenses}`,
);
return report;
}
/**
* Generate Balance Sheet
*/
public async getBalanceSheet(params?: IReportParams): Promise<IBalanceSheet> {
this.logger.log('info', 'Generating balance sheet');
// Get accounts by type
const assetAccounts = await Account.getAccountsByType(
'asset',
this.skrType,
);
const liabilityAccounts = await Account.getAccountsByType(
'liability',
this.skrType,
);
const equityAccounts = await Account.getAccountsByType(
'equity',
this.skrType,
);
// Process assets
const currentAssets: IBalanceSheetEntry[] = [];
const fixedAssets: IBalanceSheetEntry[] = [];
let totalAssets = 0;
for (const account of assetAccounts) {
const balance = await this.getAccountBalanceForPeriod(account, params);
if (balance !== 0) {
const entry: IBalanceSheetEntry = {
accountNumber: account.accountNumber,
accountName: account.accountName,
amount: Math.abs(balance),
};
// Classify as current or fixed based on account class
if (account.accountClass === 1) {
currentAssets.push(entry);
} else {
fixedAssets.push(entry);
}
totalAssets += Math.abs(balance);
}
}
// Process liabilities
const currentLiabilities: IBalanceSheetEntry[] = [];
const longTermLiabilities: IBalanceSheetEntry[] = [];
let totalLiabilities = 0;
for (const account of liabilityAccounts) {
const balance = await this.getAccountBalanceForPeriod(account, params);
if (balance !== 0) {
const entry: IBalanceSheetEntry = {
accountNumber: account.accountNumber,
accountName: account.accountName,
amount: Math.abs(balance),
};
// Classify as current or long-term based on account number
if (
account.accountNumber.startsWith('16') ||
account.accountNumber.startsWith('17')
) {
currentLiabilities.push(entry);
} else {
longTermLiabilities.push(entry);
}
totalLiabilities += Math.abs(balance);
}
}
// Process equity
const equityEntries: IBalanceSheetEntry[] = [];
let totalEquity = 0;
for (const account of equityAccounts) {
const balance = await this.getAccountBalanceForPeriod(account, params);
if (balance !== 0) {
const entry: IBalanceSheetEntry = {
accountNumber: account.accountNumber,
accountName: account.accountName,
amount: Math.abs(balance),
};
equityEntries.push(entry);
totalEquity += Math.abs(balance);
}
}
// Add current year profit/loss
const incomeStatement = await this.getIncomeStatement(params);
if (incomeStatement.netIncome !== 0) {
equityEntries.push({
accountNumber: '9999',
accountName: 'Current Year Profit/Loss',
amount: Math.abs(incomeStatement.netIncome),
});
totalEquity += Math.abs(incomeStatement.netIncome);
}
// Sort entries
currentAssets.sort((a, b) =>
a.accountNumber.localeCompare(b.accountNumber),
);
fixedAssets.sort((a, b) => a.accountNumber.localeCompare(b.accountNumber));
currentLiabilities.sort((a, b) =>
a.accountNumber.localeCompare(b.accountNumber),
);
longTermLiabilities.sort((a, b) =>
a.accountNumber.localeCompare(b.accountNumber),
);
equityEntries.sort((a, b) =>
a.accountNumber.localeCompare(b.accountNumber),
);
const report: IBalanceSheet = {
date: params?.dateTo || new Date(),
skrType: this.skrType,
assets: {
current: currentAssets,
fixed: fixedAssets,
totalAssets,
},
liabilities: {
current: currentLiabilities,
longTerm: longTermLiabilities,
totalLiabilities,
},
equity: {
entries: equityEntries,
totalEquity,
},
isBalanced:
Math.abs(totalAssets - (totalLiabilities + totalEquity)) < 0.01,
};
this.logger.log(
'info',
`Balance sheet generated: Assets ${totalAssets}, Liabilities ${totalLiabilities}, Equity ${totalEquity}`,
);
return report;
}
/**
* Get account balance for a specific period
*/
private async getAccountBalanceForPeriod(
account: Account,
params?: IReportParams,
): Promise<number> {
let transactions = await Transaction.getTransactionsByAccount(
account.accountNumber,
this.skrType,
);
// Apply date filter if provided
if (params?.dateFrom || params?.dateTo) {
transactions = transactions.filter((transaction) => {
if (params.dateFrom && transaction.date < params.dateFrom) return false;
if (params.dateTo && transaction.date > params.dateTo) return false;
return true;
});
}
let debitTotal = 0;
let creditTotal = 0;
for (const transaction of transactions) {
if (transaction.debitAccount === account.accountNumber) {
debitTotal += transaction.amount;
}
if (transaction.creditAccount === account.accountNumber) {
creditTotal += transaction.amount;
}
}
// Calculate net balance based on account type
switch (account.accountType) {
case 'asset':
case 'expense':
return debitTotal - creditTotal;
case 'liability':
case 'equity':
case 'revenue':
return creditTotal - debitTotal;
}
}
/**
* Generate General Ledger report
*/
public async getGeneralLedger(params?: IReportParams): Promise<any> {
this.logger.log('info', 'Generating general ledger');
const accounts = await Account.getInstances({
skrType: this.skrType,
isActive: true,
});
const ledgerEntries = [];
for (const account of accounts) {
const transactions = await this.getAccountTransactions(
account.accountNumber,
params,
);
if (transactions.length > 0) {
let runningBalance = 0;
const accountEntries = [];
for (const transaction of transactions) {
const isDebit = transaction.debitAccount === account.accountNumber;
const amount = transaction.amount;
// Update running balance based on account type
if (
account.accountType === 'asset' ||
account.accountType === 'expense'
) {
runningBalance += isDebit ? amount : -amount;
} else {
runningBalance += isDebit ? -amount : amount;
}
accountEntries.push({
date: transaction.date,
reference: transaction.reference,
description: transaction.description,
debit: isDebit ? amount : 0,
credit: !isDebit ? amount : 0,
balance: runningBalance,
});
}
ledgerEntries.push({
accountNumber: account.accountNumber,
accountName: account.accountName,
accountType: account.accountType,
entries: accountEntries,
finalBalance: runningBalance,
});
}
}
return {
date: params?.dateTo || new Date(),
skrType: this.skrType,
accounts: ledgerEntries,
};
}
/**
* Get account transactions for reporting
*/
private async getAccountTransactions(
accountNumber: string,
params?: IReportParams,
): Promise<Transaction[]> {
let transactions = await Transaction.getTransactionsByAccount(
accountNumber,
this.skrType,
);
// Apply date filter
if (params?.dateFrom || params?.dateTo) {
transactions = transactions.filter((transaction) => {
if (params.dateFrom && transaction.date < params.dateFrom) return false;
if (params.dateTo && transaction.date > params.dateTo) return false;
return true;
});
}
// Sort by date
transactions.sort((a, b) => a.date.getTime() - b.date.getTime());
return transactions;
}
/**
* Generate Cash Flow Statement
*/
public async getCashFlowStatement(params?: IReportParams): Promise<any> {
this.logger.log('info', 'Generating cash flow statement');
// Get cash and bank accounts
const cashAccounts = ['1000', '1100', '1200', '1210']; // Standard cash/bank accounts
let operatingCashFlow = 0;
let investingCashFlow = 0;
let financingCashFlow = 0;
for (const accountNumber of cashAccounts) {
const account = await Account.getAccountByNumber(
accountNumber,
this.skrType,
);
if (!account) continue;
const transactions = await this.getAccountTransactions(
accountNumber,
params,
);
for (const transaction of transactions) {
const otherAccount =
transaction.debitAccount === accountNumber
? transaction.creditAccount
: transaction.debitAccount;
const otherAccountObj = await Account.getAccountByNumber(
otherAccount,
this.skrType,
);
if (!otherAccountObj) continue;
const amount =
transaction.debitAccount === accountNumber
? transaction.amount
: -transaction.amount;
// Classify cash flow
if (
otherAccountObj.accountType === 'revenue' ||
otherAccountObj.accountType === 'expense'
) {
operatingCashFlow += amount;
} else if (otherAccountObj.accountClass === 0) {
// Fixed assets
investingCashFlow += amount;
} else if (
otherAccountObj.accountType === 'liability' ||
otherAccountObj.accountType === 'equity'
) {
financingCashFlow += amount;
}
}
}
return {
date: params?.dateTo || new Date(),
skrType: this.skrType,
operatingActivities: operatingCashFlow,
investingActivities: investingCashFlow,
financingActivities: financingCashFlow,
netCashFlow: operatingCashFlow + investingCashFlow + financingCashFlow,
};
}
/**
* Export report to CSV format
*/
public async exportToCSV(
reportType: 'trial_balance' | 'income_statement' | 'balance_sheet',
params?: IReportParams,
): Promise<string> {
let csvContent = '';
switch (reportType) {
case 'trial_balance':
const trialBalance = await this.getTrialBalance(params);
csvContent = this.trialBalanceToCSV(trialBalance);
break;
case 'income_statement':
const incomeStatement = await this.getIncomeStatement(params);
csvContent = this.incomeStatementToCSV(incomeStatement);
break;
case 'balance_sheet':
const balanceSheet = await this.getBalanceSheet(params);
csvContent = this.balanceSheetToCSV(balanceSheet);
break;
}
return csvContent;
}
/**
* Convert trial balance to CSV
*/
private trialBalanceToCSV(report: ITrialBalanceReport): string {
const lines: string[] = [];
lines.push('"Account Number";"Account Name";"Debit";"Credit";"Balance"');
for (const entry of report.entries) {
lines.push(
`"${entry.accountNumber}";"${entry.accountName}";${entry.debitBalance};${entry.creditBalance};${entry.netBalance}`,
);
}
lines.push(
`"TOTAL";"";"${report.totalDebits}";"${report.totalCredits}";"""`,
);
return lines.join('\n');
}
/**
* Convert income statement to CSV
*/
private incomeStatementToCSV(report: IIncomeStatement): string {
const lines: string[] = [];
lines.push('"Type";"Account Number";"Account Name";"Amount";"Percentage"');
lines.push('"REVENUE";"";"";"";""');
for (const entry of report.revenue) {
lines.push(
`"Revenue";"${entry.accountNumber}";"${entry.accountName}";${entry.amount};${entry.percentage?.toFixed(2)}%`,
);
}
lines.push(`"Total Revenue";"";"";"${report.totalRevenue}";"""`);
lines.push('"";"";"";"";""');
lines.push('"EXPENSES";"";"";"";""');
for (const entry of report.expenses) {
lines.push(
`"Expense";"${entry.accountNumber}";"${entry.accountName}";${entry.amount};${entry.percentage?.toFixed(2)}%`,
);
}
lines.push(`"Total Expenses";"";"";"${report.totalExpenses}";"""`);
lines.push('"";"";"";"";""');
lines.push(`"NET INCOME";"";"";"${report.netIncome}";"""`);
return lines.join('\n');
}
/**
* Convert balance sheet to CSV
*/
private balanceSheetToCSV(report: IBalanceSheet): string {
const lines: string[] = [];
lines.push('"Category";"Account Number";"Account Name";"Amount"');
lines.push('"ASSETS";"";"";"";');
lines.push('"Current Assets";"";"";"";');
for (const entry of report.assets.current) {
lines.push(
`"";"${entry.accountNumber}";"${entry.accountName}";${entry.amount}`,
);
}
lines.push('"Fixed Assets";"";"";"";');
for (const entry of report.assets.fixed) {
lines.push(
`"";"${entry.accountNumber}";"${entry.accountName}";${entry.amount}`,
);
}
lines.push(`"Total Assets";"";"";"${report.assets.totalAssets}"`);
lines.push('"";"";"";"";');
lines.push('"LIABILITIES";"";"";"";');
lines.push('"Current Liabilities";"";"";"";');
for (const entry of report.liabilities.current) {
lines.push(
`"";"${entry.accountNumber}";"${entry.accountName}";${entry.amount}`,
);
}
lines.push('"Long-term Liabilities";"";"";"";');
for (const entry of report.liabilities.longTerm) {
lines.push(
`"";"${entry.accountNumber}";"${entry.accountName}";${entry.amount}`,
);
}
lines.push(
`"Total Liabilities";"";"";"${report.liabilities.totalLiabilities}"`,
);
lines.push('"";"";"";"";');
lines.push('"EQUITY";"";"";"";');
for (const entry of report.equity.entries) {
lines.push(
`"";"${entry.accountNumber}";"${entry.accountName}";${entry.amount}`,
);
}
lines.push(`"Total Equity";"";"";"${report.equity.totalEquity}"`);
lines.push('"";"";"";"";');
lines.push(
`"Total Liabilities + Equity";"";"";"${report.liabilities.totalLiabilities + report.equity.totalEquity}"`,
);
return lines.join('\n');
}
/**
* Export to DATEV format
*/
public async exportToDATEV(params?: IReportParams): Promise<string> {
// DATEV format is specific to German accounting software
// This is a simplified implementation
const transactions = await Transaction.getInstances({
skrType: this.skrType,
status: 'posted',
});
const lines: string[] = [];
// DATEV header
lines.push('EXTF;510;21;"Buchungsstapel";1;;;;;;;;;;;;;;');
for (const transaction of transactions) {
const date = transaction.date
.toISOString()
.split('T')[0]
.replace(/-/g, '');
const line = [
transaction.amount.toFixed(2).replace('.', ','),
'S',
'EUR',
'',
'',
transaction.debitAccount,
transaction.creditAccount,
'',
date,
'',
transaction.description.substring(0, 60),
'',
].join(';');
lines.push(line);
}
return lines.join('\n');
}
}