diff --git a/packages/server/src/interfaces/GeneralLedgerSheet.ts b/packages/server/src/interfaces/GeneralLedgerSheet.ts index 6de1bda3b..9f0a82296 100644 --- a/packages/server/src/interfaces/GeneralLedgerSheet.ts +++ b/packages/server/src/interfaces/GeneralLedgerSheet.ts @@ -56,6 +56,8 @@ export interface IGeneralLedgerSheetAccount { transactions: IGeneralLedgerSheetAccountTransaction[]; openingBalance: IGeneralLedgerSheetAccountBalance; closingBalance: IGeneralLedgerSheetAccountBalance; + closingBalanceSubaccounts: IGeneralLedgerSheetAccountBalance; + children?: IGeneralLedgerSheetAccount[]; } export type IGeneralLedgerSheetData = IGeneralLedgerSheetAccount[]; diff --git a/packages/server/src/services/Accounting/Ledger.ts b/packages/server/src/services/Accounting/Ledger.ts index 0a3ecd41e..3926ebe8f 100644 --- a/packages/server/src/services/Accounting/Ledger.ts +++ b/packages/server/src/services/Accounting/Ledger.ts @@ -51,7 +51,7 @@ export default class Ledger implements ILedger { /** * Filters entries by the given accounts ids then returns a new ledger. - * @param {number[]} accountIds + * @param {number[]} accountIds * @returns {ILedger} */ public whereAccountsIds(accountIds: number[]): ILedger { @@ -274,4 +274,14 @@ export default class Ledger implements ILedger { const entries = Ledger.mappingTransactions(transactions); return new Ledger(entries); } + + /** + * Retrieve the transaction amount. + * @param {number} credit - Credit amount. + * @param {number} debit - Debit amount. + * @param {string} normal - Credit or debit. + */ + static getAmount(credit: number, debit: number, normal: string) { + return normal === 'credit' ? credit - debit : debit - credit; + } } diff --git a/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetAggregators.ts b/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetAggregators.ts index 2ed4ebbd2..c818ef3ab 100644 --- a/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetAggregators.ts +++ b/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetAggregators.ts @@ -1,6 +1,4 @@ import * as R from 'ramda'; -import { FinancialPreviousPeriod } from '../FinancialPreviousPeriod'; -import { FinancialHorizTotals } from '../FinancialHorizTotals'; import { FinancialSheetStructure } from '../FinancialSheetStructure'; import { BALANCE_SHEET_SCHEMA_NODE_TYPE, diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts index 98c7ca09c..1fbd8eec0 100644 --- a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts @@ -1,5 +1,6 @@ -import { isEmpty, get, last, sumBy } from 'lodash'; +import { isEmpty, get, last, sumBy, first, head } from 'lodash'; import moment from 'moment'; +import * as R from 'ramda'; import { IGeneralLedgerSheetQuery, IGeneralLedgerSheetAccount, @@ -10,11 +11,16 @@ import { } from '@/interfaces'; import FinancialSheet from '../FinancialSheet'; import { GeneralLedgerRepository } from './GeneralLedgerRepository'; +import { FinancialSheetStructure } from '../FinancialSheetStructure'; +import { flatToNestedArray } from '@/utils'; +import Ledger from '@/services/Accounting/Ledger'; /** * General ledger sheet. */ -export default class GeneralLedgerSheet extends FinancialSheet { +export default class GeneralLedgerSheet extends R.compose( + FinancialSheetStructure +)(FinancialSheet) { tenantId: number; query: IGeneralLedgerSheetQuery; baseCurrency: string; @@ -46,13 +52,14 @@ export default class GeneralLedgerSheet extends FinancialSheet { } /** - * Retrieve the transaction amount. - * @param {number} credit - Credit amount. - * @param {number} debit - Debit amount. - * @param {string} normal - Credit or debit. + * Calculate the running balance. + * @param {number} amount - Transaction amount. + * @param {number} lastRunningBalance - Last running balance. + * @param {number} openingBalance - Opening balance. + * @return {number} Running balance. */ - getAmount(credit: number, debit: number, normal: string) { - return normal === 'credit' ? credit - debit : debit - credit; + calculateRunningBalance(amount: number, lastRunningBalance: number): number { + return amount + lastRunningBalance; } /** @@ -60,26 +67,38 @@ export default class GeneralLedgerSheet extends FinancialSheet { * @param {ILedgerEntry} entry - * @return {IGeneralLedgerSheetAccountTransaction} */ - entryReducer( - entries: IGeneralLedgerSheetAccountTransaction[], + private getEntryRunningBalance( entry: ILedgerEntry, - openingBalance: number - ): IGeneralLedgerSheetAccountTransaction[] { - const lastEntry = last(entries); + openingBalance: number, + runningBalance?: number + ): number { + const lastRunningBalance = runningBalance || openingBalance; - const contact = this.repository.contactsById.get(entry.contactId); - const amount = this.getAmount( + const amount = Ledger.getAmount( entry.credit, entry.debit, entry.accountNormal ); - const runningBalance = - amount + (!isEmpty(entries) ? lastEntry.runningBalance : openingBalance); + return this.calculateRunningBalance(amount, lastRunningBalance); + } - const newEntry = { + /** + * + * @param entry + * @param runningBalance + * @returns + */ + private entryMapper(entry: ILedgerEntry, runningBalance: number) { + const contact = this.repository.contactsById.get(entry.contactId); + const amount = Ledger.getAmount( + entry.credit, + entry.debit, + entry.accountNormal + ); + return { + id: entry.id, date: entry.date, dateFormatted: moment(entry.date).format('YYYY MMM DD'), - entryId: entry.id, transactionNumber: entry.transactionNumber, referenceType: entry.referenceType, @@ -104,10 +123,7 @@ export default class GeneralLedgerSheet extends FinancialSheet { formattedRunningBalance: this.formatNumber(runningBalance), currencyCode: this.baseCurrency, - }; - entries.push(newEntry); - - return entries; + } as IGeneralLedgerSheetAccountTransaction; } /** @@ -123,28 +139,40 @@ export default class GeneralLedgerSheet extends FinancialSheet { .whereAccountId(account.id) .getEntries(); - return entries.reduce( - ( - entries: IGeneralLedgerSheetAccountTransaction[], - entry: ILedgerEntry - ) => { - return this.entryReducer(entries, entry, openingBalance); - }, - [] - ); + return entries + .reduce((prev: Array<[number, ILedgerEntry]>, current: ILedgerEntry) => { + const amount = this.getEntryRunningBalance( + current, + openingBalance, + head(last(prev)) as number + ); + return new Array([amount, current]); + }, []) + .map(([runningBalance, entry]: [number, ILedgerEntry]) => + this.entryMapper(entry, runningBalance) + ); } /** - * Retrieve account opening balance. + * Retrieves the given account opening balance. + * @param {number} accountId + * @returns {number} + */ + private accountOpeningBalance(accountId: number): number { + return this.repository.openingBalanceTransactionsLedger + .whereAccountId(accountId) + .getClosingBalance(); + } + + /** + * Retrieve the given account opening balance. * @param {IAccount} account * @return {IGeneralLedgerSheetAccountBalance} */ - private accountOpeningBalance( - account: IAccount + private accountOpeningBalanceTotal( + accountId: number ): IGeneralLedgerSheetAccountBalance { - const amount = this.repository.openingBalanceTransactionsLedger - .whereAccountId(account.id) - .getClosingBalance(); + const amount = this.accountOpeningBalance(accountId); const formattedAmount = this.formatTotalNumber(amount); const currencyCode = this.baseCurrency; const date = this.query.fromDate; @@ -153,15 +181,31 @@ export default class GeneralLedgerSheet extends FinancialSheet { } /** - * Retrieve account closing balance. + * Retrieves the given account closing balance. + * @param {number} accountId + * @returns {number} + */ + private accountClosingBalance(accountId: number): number { + const openingBalance = this.repository.openingBalanceTransactionsLedger + .whereAccountId(accountId) + .getClosingBalance(); + + const transactionsBalance = this.repository.transactionsLedger + .whereAccountId(accountId) + .getClosingBalance(); + + return openingBalance + transactionsBalance; + } + + /** + * Retrieves the given account closing balance. * @param {IAccount} account * @return {IGeneralLedgerSheetAccountBalance} */ - private accountClosingBalance( - openingBalance: number, - transactions: IGeneralLedgerSheetAccountTransaction[] + private accountClosingBalanceTotal( + accountId: number ): IGeneralLedgerSheetAccountBalance { - const amount = this.calcClosingBalance(openingBalance, transactions); + const amount = this.accountClosingBalance(accountId); const formattedAmount = this.formatTotalNumber(amount); const currencyCode = this.baseCurrency; const date = this.query.toDate; @@ -169,29 +213,63 @@ export default class GeneralLedgerSheet extends FinancialSheet { return { amount, formattedAmount, currencyCode, date }; } - private calcClosingBalance( - openingBalance: number, - transactions: IGeneralLedgerSheetAccountTransaction[] - ) { - return openingBalance + sumBy(transactions, (trans) => trans.amount); - } + /** + * Retrieves the given account closing balance with subaccounts. + * @param {number} accountId + * @returns {number} + */ + private accountClosingBalanceWithSubaccounts = ( + accountId: number + ): number => { + const depsAccountsIds = + this.repository.accountsGraph.dependenciesOf(accountId); + + console.log([...depsAccountsIds, accountId]); + + const openingBalance = this.repository.openingBalanceTransactionsLedger + .whereAccountsIds([...depsAccountsIds, accountId]) + .getClosingBalance(); + + const transactionsBalanceWithSubAccounts = + this.repository.transactionsLedger + .whereAccountsIds([...depsAccountsIds, accountId]) + .getClosingBalance(); + + const closingBalance = openingBalance + transactionsBalanceWithSubAccounts; + + return closingBalance; + }; + + /** + * + * @param {number} accountId + * @returns {IGeneralLedgerSheetAccountBalance} + */ + private accountClosingBalanceWithSubaccountsTotal = ( + accountId: number + ): IGeneralLedgerSheetAccountBalance => { + const amount = this.accountClosingBalanceWithSubaccounts(accountId); + const formattedAmount = this.formatTotalNumber(amount); + const currencyCode = this.baseCurrency; + const date = this.query.toDate; + + return { amount, formattedAmount, currencyCode, date }; + }; /** * Retreive general ledger accounts sections. * @param {IAccount} account * @return {IGeneralLedgerSheetAccount} */ - private accountMapper(account: IAccount): IGeneralLedgerSheetAccount { - const openingBalance = this.accountOpeningBalance(account); - + private accountMapper = (account: IAccount): IGeneralLedgerSheetAccount => { + const openingBalance = this.accountOpeningBalanceTotal(account.id); const transactions = this.accountTransactionsMapper( account, openingBalance.amount ); - const closingBalance = this.accountClosingBalance( - openingBalance.amount, - transactions - ); + const closingBalance = this.accountClosingBalanceTotal(account.id); + const closingBalanceSubaccounts = + this.accountClosingBalanceWithSubaccountsTotal(account.id); return { id: account.id, @@ -202,32 +280,65 @@ export default class GeneralLedgerSheet extends FinancialSheet { openingBalance, transactions, closingBalance, + closingBalanceSubaccounts, }; - } + }; /** - * Retrieve mapped accounts with general ledger transactions and opeing/closing balance. + * Maps over deep nodes to retrieve the G/L account node. + * @param {IAccount[]} accounts + * @returns {IGeneralLedgerSheetAccount[]} + */ + private accountNodesDeepMap = ( + accounts: IAccount[] + ): IGeneralLedgerSheetAccount[] => { + return this.mapNodesDeep(accounts, this.accountMapper); + }; + + /** + * Transformes the flatten nodes to nested nodes. + */ + private nestedAccountsNode = (flattenAccounts: IAccount[]): IAccount[] => { + return flatToNestedArray(flattenAccounts, { + id: 'id', + parentId: 'parentAccountId', + }); + }; + + /** + * Filters account nodes. + * @param {IGeneralLedgerSheetAccount[]} nodes + * @returns {IGeneralLedgerSheetAccount[]} + */ + private filterAccountNodes = ( + nodes: IGeneralLedgerSheetAccount[] + ): IGeneralLedgerSheetAccount[] => { + return this.filterNodesDeep( + nodes, + (generalLedgerAccount: IGeneralLedgerSheetAccount) => + !( + generalLedgerAccount.transactions.length === 0 && + this.query.noneTransactions + ) + ); + }; + + /** + * Retrieves mapped accounts with general ledger transactions and + * opeing/closing balance. * @param {IAccount[]} accounts - * @return {IGeneralLedgerSheetAccount[]} */ private accountsWalker(accounts: IAccount[]): IGeneralLedgerSheetAccount[] { - return ( - accounts - .map((account: IAccount) => this.accountMapper(account)) - // Filter general ledger accounts that have no transactions - // when`noneTransactions` is on. - .filter( - (generalLedgerAccount: IGeneralLedgerSheetAccount) => - !( - generalLedgerAccount.transactions.length === 0 && - this.query.noneTransactions - ) - ) - ); + return R.compose( + this.filterAccountNodes, + this.accountNodesDeepMap, + this.nestedAccountsNode + )(accounts); } /** - * Retrieve general ledger report data. + * Retrieves general ledger report data. * @return {IGeneralLedgerSheetAccount[]} */ public reportData(): IGeneralLedgerSheetAccount[] { diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts index 1820ab095..9ab77a992 100644 --- a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts @@ -113,6 +113,27 @@ export class GeneralLedgerTable extends R.compose( ]; } + /** + * Closing balance row column accessors. + * @returns {ITableColumnAccessor[]} + */ + private closingBalanceWithSubaccountsColumnAccessors(): IColumnMapperMeta[] { + return [ + { key: 'date', value: this.meta.toDate }, + { key: 'account_name', value: 'Closing Balance with sub-accounts' }, + { key: 'reference_type', accessor: '_empty_' }, + { key: 'reference_number', accessor: '_empty_' }, + { key: 'description', accessor: '_empty_' }, + { key: 'credit', accessor: '_empty_' }, + { key: 'debit', accessor: '_empty_' }, + { key: 'amount', accessor: 'closingBalanceSubaccounts.formattedAmount' }, + { + key: 'running_balance', + accessor: 'closingBalanceSubaccounts.formattedAmount', + }, + ]; + } + /** * Retrieves the common table columns. * @returns {ITableColumn[]} @@ -191,6 +212,21 @@ export class GeneralLedgerTable extends R.compose( return tableRowMapper(account, columns, meta); }; + /** + * Maps the given account node to opening balance table row. + * @param {IGeneralLedgerSheetAccount} account + * @returns {ITableRow} + */ + private closingBalanceWithSubaccountsMapper = ( + account: IGeneralLedgerSheetAccount + ): ITableRow => { + const columns = this.closingBalanceWithSubaccountsColumnAccessors(); + const meta = { + rowTypes: [ROW_TYPE.CLOSING_BALANCE], + }; + return tableRowMapper(account, columns, meta); + }; + /** * Maps the given account node to transactions table rows. * @param {IGeneralLedgerSheetAccount} account @@ -221,8 +257,23 @@ export class GeneralLedgerTable extends R.compose( rowTypes: [ROW_TYPE.ACCOUNT], }; const row = tableRowMapper(account, columns, meta); + const closingBalanceWithSubaccounts = + this.closingBalanceWithSubaccountsMapper(account); - return R.assoc('children', transactions)(row); + const children = R.compose( + // Appends the closing balance with sub-accounts row if the account has children accounts. + R.when( + () => account.children?.length > 0, + R.append(closingBalanceWithSubaccounts) + ), + R.concat(R.defaultTo([], transactions)), + R.when( + () => account?.children?.length > 0, + R.concat(R.defaultTo([], account.children)) + ) + )([]); + + return R.assoc('children', children)(row); }; /** @@ -233,7 +284,7 @@ export class GeneralLedgerTable extends R.compose( private accountsMapper = ( accounts: IGeneralLedgerSheetAccount[] ): ITableRow[] => { - return this.mapNodesDeep(accounts, this.accountMapper); + return this.mapNodesDeepReverse(accounts, this.accountMapper); }; /** @@ -250,7 +301,6 @@ export class GeneralLedgerTable extends R.compose( */ public tableColumns(): ITableColumn[] { const columns = this.commonColumns(); - return R.compose(this.tableColumnsCellIndexing)(columns); } }