UNPKG

@debito/hippo-lib

Version:

Double-entry accounting library for CouchDB

243 lines (212 loc) 9.97 kB
import Account from '../Account.js'; import { ACCOUNT_TYPES } from '../constants.js'; /** * Trial Balance report generator * Provides current trial balance and utility methods for display */ class TrialBalance { /** * Generates trial balance as of a specific date (current balances) * @param {Date} asOfDate - Date for trial balance (default: current date) * @returns {Promise<Object>} Trial balance data in JSON format */ static async asOfDate(asOfDate = new Date()) { try { // Get all accounts const accounts = await Account.list(); // Group accounts by type const accountGroups = { [ACCOUNT_TYPES.ASSET]: [], [ACCOUNT_TYPES.LIABILITY]: [], [ACCOUNT_TYPES.EQUITY]: [], [ACCOUNT_TYPES.REVENUE]: [], [ACCOUNT_TYPES.EXPENSE]: [] }; // Organize accounts by type for (const account of accounts) { if (accountGroups[account.accountType]) { accountGroups[account.accountType].push({ key: account.key, label: account.label, balance: account.balance, debit: (account.isDebitAccount() && account.balance > 0) ? account.balance : (!account.isDebitAccount() && account.balance < 0) ? Math.abs(account.balance) : 0, credit: (!account.isDebitAccount() && account.balance > 0) ? account.balance : (account.isDebitAccount() && account.balance < 0) ? Math.abs(account.balance) : 0 }); } } // Calculate totals for each group const groupTotals = {}; let totalDebits = 0; let totalCredits = 0; for (const [accountType, accountList] of Object.entries(accountGroups)) { const groupDebitTotal = Math.round(accountList.reduce((sum, acc) => sum + acc.debit, 0) * 100) / 100; const groupCreditTotal = Math.round(accountList.reduce((sum, acc) => sum + acc.credit, 0) * 100) / 100; groupTotals[accountType] = { debit: groupDebitTotal, credit: groupCreditTotal, count: accountList.length }; totalDebits = Math.round((totalDebits + groupDebitTotal) * 100) / 100; totalCredits = Math.round((totalCredits + groupCreditTotal) * 100) / 100; } return { metadata: { asOfDate: asOfDate.toISOString(), generatedAt: new Date().toISOString(), totalAccounts: accounts.length, isBalanced: Math.abs(totalDebits - totalCredits) < 0.01 }, accounts: accountGroups, totals: { byGroup: groupTotals, overall: { debit: totalDebits, credit: totalCredits, difference: totalDebits - totalCredits } } }; } catch (error) { throw new Error(`Failed to generate trial balance: ${error.message}`); } } /** * TODO: Generates trial balance for a specific period (activity-based) * @param {Date} startDate - Period start date * @param {Date} endDate - Period end date * @returns {Promise<Object>} Period trial balance data */ static async forPeriod(startDate, endDate) { // TODO: Implement period trial balance // This will require querying LedgerEntry by date range // and calculating net activity for each account during the period throw new Error('Period trial balance not yet implemented'); } /** * TODO: Generates comparative trial balance for multiple periods * @param {Array} periods - Array of period objects {startDate, endDate, label} * @returns {Promise<Object>} Comparative trial balance data */ static async comparative(periods) { // TODO: Implement comparative trial balance // This will show side-by-side comparison of multiple periods throw new Error('Comparative trial balance not yet implemented'); } /** * Utility method to print trial balance in tabular format to console * @param {Object} trialBalanceData - Trial balance JSON data * @param {Object} options - Display options {showZeroBalances: boolean} */ static printToConsole(trialBalanceData, options = {}) { const { showZeroBalances = false } = options; console.log('\n' + '='.repeat(80)); console.log('TRIAL BALANCE'); console.log(`As of: ${new Date(trialBalanceData.metadata.asOfDate).toLocaleDateString()}`); console.log(`Generated: ${new Date(trialBalanceData.metadata.generatedAt).toLocaleString()}`); console.log('='.repeat(80)); // Helper function to format currency const formatAmount = (amount) => { return amount.toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2 }); }; // Helper function to pad text const pad = (text, length, align = 'left') => { const str = String(text); if (align === 'right') { return str.padStart(length); } return str.padEnd(length); }; // Print header console.log(pad('Account', 35) + pad('Debit', 15, 'right') + pad('Credit', 15, 'right')); console.log('-'.repeat(65)); // Account type display order const typeOrder = [ ACCOUNT_TYPES.ASSET, ACCOUNT_TYPES.LIABILITY, ACCOUNT_TYPES.EQUITY, ACCOUNT_TYPES.REVENUE, ACCOUNT_TYPES.EXPENSE ]; const typeLabels = { [ACCOUNT_TYPES.ASSET]: 'ASSETS', [ACCOUNT_TYPES.LIABILITY]: 'LIABILITIES', [ACCOUNT_TYPES.EQUITY]: 'EQUITY', [ACCOUNT_TYPES.REVENUE]: 'REVENUE', [ACCOUNT_TYPES.EXPENSE]: 'EXPENSES' }; // Print accounts by type for (const accountType of typeOrder) { const accounts = trialBalanceData.accounts[accountType]; const groupTotal = trialBalanceData.totals.byGroup[accountType]; if (accounts.length === 0 && !showZeroBalances) continue; // Print account type header console.log('\n' + typeLabels[accountType] + ':'); // Print accounts in this group for (const account of accounts) { if (!showZeroBalances && account.balance === 0) continue; const debitStr = account.debit > 0 ? formatAmount(account.debit) : ''; const creditStr = account.credit > 0 ? formatAmount(account.credit) : ''; console.log( ' ' + pad(account.label, 33) + pad(debitStr, 15, 'right') + pad(creditStr, 15, 'right') ); } // Print group subtotal if there are accounts if (accounts.length > 0) { const groupDebitStr = groupTotal.debit > 0 ? formatAmount(groupTotal.debit) : ''; const groupCreditStr = groupTotal.credit > 0 ? formatAmount(groupTotal.credit) : ''; console.log(' ' + '-'.repeat(63)); console.log( ' ' + pad(`Total ${typeLabels[accountType]}`, 33) + pad(groupDebitStr, 15, 'right') + pad(groupCreditStr, 15, 'right') ); } } // Print grand totals console.log('\n' + '='.repeat(65)); console.log( pad('TOTAL', 35) + pad(formatAmount(trialBalanceData.totals.overall.debit), 15, 'right') + pad(formatAmount(trialBalanceData.totals.overall.credit), 15, 'right') ); console.log('='.repeat(65)); // Show balance status const isBalanced = trialBalanceData.metadata.isBalanced; const difference = trialBalanceData.totals.overall.difference; console.log(`\nBalance Status: ${isBalanced ? '✅ BALANCED' : '❌ OUT OF BALANCE'}`); if (!isBalanced) { console.log(`Difference: ${formatAmount(Math.abs(difference))} ${difference > 0 ? '(Debit Heavy)' : '(Credit Heavy)'}`); } console.log(`Total Accounts: ${trialBalanceData.metadata.totalAccounts}`); console.log('='.repeat(80) + '\n'); } /** * Utility method to get trial balance summary * @param {Object} trialBalanceData - Trial balance JSON data * @returns {Object} Summary statistics */ static getSummary(trialBalanceData) { return { totalAccounts: trialBalanceData.metadata.totalAccounts, totalDebits: trialBalanceData.totals.overall.debit, totalCredits: trialBalanceData.totals.overall.credit, isBalanced: trialBalanceData.metadata.isBalanced, difference: trialBalanceData.totals.overall.difference, accountsByType: Object.keys(trialBalanceData.totals.byGroup).reduce((acc, type) => { acc[type] = trialBalanceData.totals.byGroup[type].count; return acc; }, {}) }; } } export default TrialBalance;