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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { // 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'); } }