From 6afe1a09c673bfd0c3933d9fd02e3ec5784562e7 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Tue, 4 Jun 2024 21:26:46 +0200 Subject: [PATCH 1/8] fix: Closing balance in general ledger report does not sum the negative figures. --- .../BalanceSheet/BalanceSheetRepository.ts | 2 - .../GeneralLedger/GeneralLedger.ts | 43 +++--- .../GeneralLedger/GeneralLedgerRepository.ts | 123 ++++++++++++++++++ .../GeneralLedger/GeneralLedgerService.ts | 81 ++---------- 4 files changed, 152 insertions(+), 97 deletions(-) create mode 100644 packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerRepository.ts diff --git a/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetRepository.ts b/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetRepository.ts index 001f266be..889593516 100644 --- a/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetRepository.ts +++ b/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetRepository.ts @@ -3,7 +3,6 @@ import * as R from 'ramda'; import { Knex } from 'knex'; import { isEmpty } from 'lodash'; import { - IAccount, IAccountTransactionsGroupBy, IBalanceSheetQuery, ILedger, @@ -12,7 +11,6 @@ import { transformToMapBy } from 'utils'; import Ledger from '@/services/Accounting/Ledger'; import { BalanceSheetQuery } from './BalanceSheetQuery'; import { FinancialDatePeriods } from '../FinancialDatePeriods'; -import { ACCOUNT_PARENT_TYPE, ACCOUNT_TYPE } from '@/data/AccountTypes'; import { BalanceSheetRepositoryNetIncome } from './BalanceSheetRepositoryNetIncome'; @Service() diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts index 5e12e9078..98c7ca09c 100644 --- a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts @@ -1,29 +1,25 @@ import { isEmpty, get, last, sumBy } from 'lodash'; +import moment from 'moment'; import { IGeneralLedgerSheetQuery, IGeneralLedgerSheetAccount, IGeneralLedgerSheetAccountBalance, IGeneralLedgerSheetAccountTransaction, IAccount, - IJournalPoster, - IJournalEntry, - IContact, + ILedgerEntry, } from '@/interfaces'; import FinancialSheet from '../FinancialSheet'; -import moment from 'moment'; +import { GeneralLedgerRepository } from './GeneralLedgerRepository'; /** * General ledger sheet. */ export default class GeneralLedgerSheet extends FinancialSheet { tenantId: number; - accounts: IAccount[]; query: IGeneralLedgerSheetQuery; - openingBalancesJournal: IJournalPoster; - transactions: IJournalPoster; - contactsMap: Map; baseCurrency: string; i18n: any; + repository: GeneralLedgerRepository; /** * Constructor method. @@ -36,11 +32,7 @@ export default class GeneralLedgerSheet extends FinancialSheet { constructor( tenantId: number, query: IGeneralLedgerSheetQuery, - accounts: IAccount[], - contactsByIdMap: Map, - transactions: IJournalPoster, - openingBalancesJournal: IJournalPoster, - baseCurrency: string, + repository: GeneralLedgerRepository, i18n ) { super(); @@ -48,11 +40,8 @@ export default class GeneralLedgerSheet extends FinancialSheet { this.tenantId = tenantId; this.query = query; this.numberFormat = this.query.numberFormat; - this.accounts = accounts; - this.contactsMap = contactsByIdMap; - this.transactions = transactions; - this.openingBalancesJournal = openingBalancesJournal; - this.baseCurrency = baseCurrency; + this.repository = repository; + this.baseCurrency = this.repository.tenant.metadata.currencyCode; this.i18n = i18n; } @@ -68,17 +57,17 @@ export default class GeneralLedgerSheet extends FinancialSheet { /** * Entry mapper. - * @param {IJournalEntry} entry - + * @param {ILedgerEntry} entry - * @return {IGeneralLedgerSheetAccountTransaction} */ entryReducer( entries: IGeneralLedgerSheetAccountTransaction[], - entry: IJournalEntry, + entry: ILedgerEntry, openingBalance: number ): IGeneralLedgerSheetAccountTransaction[] { const lastEntry = last(entries); - const contact = this.contactsMap.get(entry.contactId); + const contact = this.repository.contactsById.get(entry.contactId); const amount = this.getAmount( entry.credit, entry.debit, @@ -130,12 +119,14 @@ export default class GeneralLedgerSheet extends FinancialSheet { account: IAccount, openingBalance: number ): IGeneralLedgerSheetAccountTransaction[] { - const entries = this.transactions.getAccountEntries(account.id); + const entries = this.repository.transactionsLedger + .whereAccountId(account.id) + .getEntries(); return entries.reduce( ( entries: IGeneralLedgerSheetAccountTransaction[], - entry: IJournalEntry + entry: ILedgerEntry ) => { return this.entryReducer(entries, entry, openingBalance); }, @@ -151,7 +142,9 @@ export default class GeneralLedgerSheet extends FinancialSheet { private accountOpeningBalance( account: IAccount ): IGeneralLedgerSheetAccountBalance { - const amount = this.openingBalancesJournal.getAccountBalance(account.id); + const amount = this.repository.openingBalanceTransactionsLedger + .whereAccountId(account.id) + .getClosingBalance(); const formattedAmount = this.formatTotalNumber(amount); const currencyCode = this.baseCurrency; const date = this.query.fromDate; @@ -238,6 +231,6 @@ export default class GeneralLedgerSheet extends FinancialSheet { * @return {IGeneralLedgerSheetAccount[]} */ public reportData(): IGeneralLedgerSheetAccount[] { - return this.accountsWalker(this.accounts); + return this.accountsWalker(this.repository.accounts); } } diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerRepository.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerRepository.ts new file mode 100644 index 000000000..60ecca5dd --- /dev/null +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerRepository.ts @@ -0,0 +1,123 @@ +import moment from 'moment'; +import { + IAccount, + IAccountTransaction, + IContact, + IGeneralLedgerSheetQuery, + ITenant, +} from '@/interfaces'; +import Ledger from '@/services/Accounting/Ledger'; +import { transformToMap } from '@/utils'; +import { Tenant } from '@/system/models'; + +export class GeneralLedgerRepository { + public filter: IGeneralLedgerSheetQuery; + public accounts: IAccount[]; + + public transactions: IAccountTransaction[]; + public openingBalanceTransactions: IAccountTransaction[]; + + public transactionsLedger: Ledger; + public openingBalanceTransactionsLedger: Ledger; + + public repositories: any; + public models: any; + public accountsGraph: any; + + public contacts: IContact; + public contactsById: Map; + + public tenantId: number; + public tenant: ITenant; + + /** + * Constructor method. + * @param models + * @param repositories + * @param filter + */ + constructor( + repositories: any, + filter: IGeneralLedgerSheetQuery, + tenantId: number + ) { + this.filter = filter; + this.repositories = repositories; + this.tenantId = tenantId; + } + + /** + * Initialize the G/L report. + */ + public async asyncInitialize() { + await this.initTenant(); + await this.initAccounts(); + await this.initAccountsGraph(); + await this.initContacts(); + await this.initTransactions(); + await this.initAccountsOpeningBalance(); + } + + /** + * Initialize the tenant. + */ + public async initTenant() { + this.tenant = await Tenant.query() + .findById(this.tenantId) + .withGraphFetched('metadata'); + } + + /** + * Initialize the accounts. + */ + public async initAccounts() { + this.accounts = await this.repositories.accountRepository.all(); + } + + /** + * Initialize the accounts graph. + */ + public async initAccountsGraph() { + this.accountsGraph = + await this.repositories.accountRepository.getDependencyGraph(); + } + + /** + * Initialize the contacts. + */ + public async initContacts() { + this.contacts = await this.repositories.contactRepository.all(); + this.contactsById = transformToMap(this.contacts, 'id'); + } + + /** + * Initialize the G/L transactions from/to the given date. + */ + public async initTransactions() { + this.transactions = await this.repositories.transactionsRepository.journal({ + fromDate: this.filter.fromDate, + toDate: this.filter.toDate, + branchesIds: this.filter.branchesIds, + }); + // Transform array transactions to journal collection. + this.transactionsLedger = Ledger.fromTransactions(this.transactions); + } + + /** + * Initialize the G/L accounts opening balance. + */ + public async initAccountsOpeningBalance() { + // Retreive opening balance credit/debit sumation. + this.openingBalanceTransactions = + await this.repositories.transactionsRepository.journal({ + toDate: moment(this.filter.fromDate).subtract(1, 'day'), + sumationCreditDebit: true, + branchesIds: this.filter.branchesIds, + }); + + // Accounts opening transactions. + this.openingBalanceTransactionsLedger = Ledger.fromTransactions( + this.openingBalanceTransactions + ); + } +} diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts index 53451d2d8..c2e407f5c 100644 --- a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts @@ -1,18 +1,10 @@ import { Service, Inject } from 'typedi'; import moment from 'moment'; -import { ServiceError } from '@/exceptions'; -import { difference } from 'lodash'; import { IGeneralLedgerSheetQuery, IGeneralLedgerMeta } from '@/interfaces'; import TenancyService from '@/services/Tenancy/TenancyService'; -import Journal from '@/services/Accounting/JournalPoster'; import GeneralLedgerSheet from '@/services/FinancialStatements/GeneralLedger/GeneralLedger'; -import { transformToMap } from 'utils'; -import { Tenant } from '@/system/models'; import { GeneralLedgerMeta } from './GeneralLedgerMeta'; - -const ERRORS = { - ACCOUNTS_NOT_FOUND: 'ACCOUNTS_NOT_FOUND', -}; +import { GeneralLedgerRepository } from './GeneralLedgerRepository'; @Service() export class GeneralLedgerService { @@ -40,29 +32,13 @@ export class GeneralLedgerService { }; } - /** - * Validates accounts existance on the storage. - * @param {number} tenantId - * @param {number[]} accountsIds - */ - async validateAccountsExistance(tenantId: number, accountsIds: number[]) { - const { Account } = this.tenancy.models(tenantId); - - const storedAccounts = await Account.query().whereIn('id', accountsIds); - const storedAccountsIds = storedAccounts.map((a) => a.id); - - if (difference(accountsIds, storedAccountsIds).length > 0) { - throw new ServiceError(ERRORS.ACCOUNTS_NOT_FOUND); - } - } - /** * Retrieve general ledger report statement. * @param {number} tenantId * @param {IGeneralLedgerSheetQuery} query - * @return {IGeneralLedgerStatement} + * @return {Promise} */ - async generalLedger( + public async generalLedger( tenantId: number, query: IGeneralLedgerSheetQuery ): Promise<{ @@ -70,60 +46,25 @@ export class GeneralLedgerService { query: IGeneralLedgerSheetQuery; meta: IGeneralLedgerMeta; }> { - const { accountRepository, transactionsRepository, contactRepository } = - this.tenancy.repositories(tenantId); - + const repositories = this.tenancy.repositories(tenantId); const i18n = this.tenancy.i18n(tenantId); - const tenant = await Tenant.query() - .findById(tenantId) - .withGraphFetched('metadata'); - const filter = { ...this.defaultQuery, ...query, }; - // Retrieve all accounts with associated type from the storage. - const accounts = await accountRepository.all(); - const accountsGraph = await accountRepository.getDependencyGraph(); - - // Retrieve all contacts on the storage. - const contacts = await contactRepository.all(); - const contactsByIdMap = transformToMap(contacts, 'id'); - - // Retreive journal transactions from/to the given date. - const transactions = await transactionsRepository.journal({ - fromDate: filter.fromDate, - toDate: filter.toDate, - branchesIds: filter.branchesIds, - }); - // Retreive opening balance credit/debit sumation. - const openingBalanceTrans = await transactionsRepository.journal({ - toDate: moment(filter.fromDate).subtract(1, 'day'), - sumationCreditDebit: true, - branchesIds: filter.branchesIds, - }); - // Transform array transactions to journal collection. - const transactionsJournal = Journal.fromTransactions( - transactions, - tenantId, - accountsGraph - ); - // Accounts opening transactions. - const openingTransJournal = Journal.fromTransactions( - openingBalanceTrans, - tenantId, - accountsGraph + const genealLedgerRepository = new GeneralLedgerRepository( + repositories, + query, + tenantId ); + await genealLedgerRepository.asyncInitialize(); + // General ledger report instance. const generalLedgerInstance = new GeneralLedgerSheet( tenantId, filter, - accounts, - contactsByIdMap, - transactionsJournal, - openingTransJournal, - tenant.metadata.baseCurrency, + genealLedgerRepository, i18n ); // Retrieve general ledger report data. From 044f11ff746751acf4434c6fef8c9a8a4685dc03 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 5 Jun 2024 21:45:01 +0200 Subject: [PATCH 2/8] feat: general ledger sub-accounts --- .../src/interfaces/GeneralLedgerSheet.ts | 2 + .../server/src/services/Accounting/Ledger.ts | 12 +- .../BalanceSheet/BalanceSheetAggregators.ts | 2 - .../GeneralLedger/GeneralLedger.ts | 255 +++++++++++++----- .../GeneralLedger/GeneralLedgerTable.ts | 56 +++- 5 files changed, 249 insertions(+), 78 deletions(-) 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); } } From 5dbfd36415b521f5551524a2b41a758f014505ba Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 5 Jun 2024 22:42:12 +0200 Subject: [PATCH 3/8] feat: optimize the style of general ledger sub-accounts rows --- .../GeneralLedger/GeneralLedgerTable.ts | 32 +++++++++++++------ .../GeneralLedger/GeneralLedgerTable.tsx | 16 ++++++++-- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts index 9ab77a992..4ca71578c 100644 --- a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts @@ -83,8 +83,8 @@ export class GeneralLedgerTable extends R.compose( */ private openingBalanceColumnsAccessors(): IColumnMapperMeta[] { return [ - { key: 'date', value: this.meta.fromDate }, - { key: 'account_name', value: 'Opening Balance' }, + { key: 'date', value: 'Opening Balance' }, + { key: 'account_name', value: '' }, { key: 'reference_type', accessor: '_empty_' }, { key: 'reference_number', accessor: '_empty_' }, { key: 'description', accessor: 'description' }, @@ -97,12 +97,15 @@ export class GeneralLedgerTable extends R.compose( /** * Closing balance row column accessors. + * @param {IGeneralLedgerSheetAccount} account - * @returns {ITableColumnAccessor[]} */ - private closingBalanceColumnAccessors(): IColumnMapperMeta[] { + private closingBalanceColumnAccessors( + account: IGeneralLedgerSheetAccount + ): IColumnMapperMeta[] { return [ - { key: 'date', value: this.meta.toDate }, - { key: 'account_name', value: 'Closing Balance' }, + { key: 'date', value: `Closing balance for ${account.name}` }, + { key: 'account_name', value: `` }, { key: 'reference_type', accessor: '_empty_' }, { key: 'reference_number', accessor: '_empty_' }, { key: 'description', accessor: '_empty_' }, @@ -115,12 +118,21 @@ export class GeneralLedgerTable extends R.compose( /** * Closing balance row column accessors. + * @param {IGeneralLedgerSheetAccount} account - * @returns {ITableColumnAccessor[]} */ - private closingBalanceWithSubaccountsColumnAccessors(): IColumnMapperMeta[] { + private closingBalanceWithSubaccountsColumnAccessors( + account: IGeneralLedgerSheetAccount + ): IColumnMapperMeta[] { return [ - { key: 'date', value: this.meta.toDate }, - { key: 'account_name', value: 'Closing Balance with sub-accounts' }, + { + key: 'date', + value: `Closing Balance for ${account.name} with sub-accounts`, + }, + { + key: 'account_name', + value: ``, + }, { key: 'reference_type', accessor: '_empty_' }, { key: 'reference_number', accessor: '_empty_' }, { key: 'description', accessor: '_empty_' }, @@ -205,7 +217,7 @@ export class GeneralLedgerTable extends R.compose( * @returns {ITableRow} */ private closingBalanceMapper = (account: IGeneralLedgerSheetAccount) => { - const columns = this.closingBalanceColumnAccessors(); + const columns = this.closingBalanceColumnAccessors(account); const meta = { rowTypes: [ROW_TYPE.CLOSING_BALANCE], }; @@ -220,7 +232,7 @@ export class GeneralLedgerTable extends R.compose( private closingBalanceWithSubaccountsMapper = ( account: IGeneralLedgerSheetAccount ): ITableRow => { - const columns = this.closingBalanceWithSubaccountsColumnAccessors(); + const columns = this.closingBalanceWithSubaccountsColumnAccessors(account); const meta = { rowTypes: [ROW_TYPE.CLOSING_BALANCE], }; diff --git a/packages/webapp/src/containers/FinancialStatements/GeneralLedger/GeneralLedgerTable.tsx b/packages/webapp/src/containers/FinancialStatements/GeneralLedger/GeneralLedgerTable.tsx index 13a012be0..6566a2773 100644 --- a/packages/webapp/src/containers/FinancialStatements/GeneralLedger/GeneralLedgerTable.tsx +++ b/packages/webapp/src/containers/FinancialStatements/GeneralLedger/GeneralLedgerTable.tsx @@ -96,12 +96,19 @@ const GeneralLedgerDataTable = styled(ReportDataTable)` } } } - &:not(:first-child).is-expanded .td { - border-top: 1px solid #ddd; - } } &--OPENING_BALANCE, &--CLOSING_BALANCE { + .td { + color: #000; + } + .date { + font-weight: 500; + + .cell-inner { + position: absolute; + } + } .amount { font-weight: 500; } @@ -110,6 +117,9 @@ const GeneralLedgerDataTable = styled(ReportDataTable)` .name { font-weight: 500; } + .td { + border-top: 1px solid #ddd; + } } } } From 10fcf94c92d9776c518464c04d34aa2424f46db6 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 6 Jun 2024 18:42:07 +0200 Subject: [PATCH 4/8] feat: general ledger closing balance with accounts row --- .../src/interfaces/GeneralLedgerSheet.ts | 2 +- .../GeneralLedger/GeneralLedger.ts | 104 ++++++++++++++---- .../GeneralLedger/GeneralLedgerRepository.ts | 16 ++- .../GeneralLedger/GeneralLedgerTable.ts | 8 +- 4 files changed, 99 insertions(+), 31 deletions(-) diff --git a/packages/server/src/interfaces/GeneralLedgerSheet.ts b/packages/server/src/interfaces/GeneralLedgerSheet.ts index 9f0a82296..12f1e0883 100644 --- a/packages/server/src/interfaces/GeneralLedgerSheet.ts +++ b/packages/server/src/interfaces/GeneralLedgerSheet.ts @@ -56,7 +56,7 @@ export interface IGeneralLedgerSheetAccount { transactions: IGeneralLedgerSheetAccountTransaction[]; openingBalance: IGeneralLedgerSheetAccountBalance; closingBalance: IGeneralLedgerSheetAccountBalance; - closingBalanceSubaccounts: IGeneralLedgerSheetAccountBalance; + closingBalanceSubaccounts?: IGeneralLedgerSheetAccountBalance; children?: IGeneralLedgerSheetAccount[]; } diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts index 1fbd8eec0..4ee378a9d 100644 --- a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts @@ -117,10 +117,12 @@ export default class GeneralLedgerSheet extends R.compose( amount, runningBalance, - formattedAmount: this.formatNumber(amount), - formattedCredit: this.formatNumber(entry.credit), - formattedDebit: this.formatNumber(entry.debit), - formattedRunningBalance: this.formatNumber(runningBalance), + formattedAmount: this.formatNumber(amount, { excerptZero: false }), + formattedCredit: this.formatNumber(entry.credit, { excerptZero: false }), + formattedDebit: this.formatNumber(entry.debit, { excerptZero: false }), + formattedRunningBalance: this.formatNumber(runningBalance, { + excerptZero: false, + }), currencyCode: this.baseCurrency, } as IGeneralLedgerSheetAccountTransaction; @@ -141,16 +143,20 @@ export default class GeneralLedgerSheet extends R.compose( return entries .reduce((prev: Array<[number, ILedgerEntry]>, current: ILedgerEntry) => { + const prevEntry = last(prev); + const prevRunningBalance = head(prevEntry) as number; const amount = this.getEntryRunningBalance( current, openingBalance, - head(last(prev)) as number + prevRunningBalance ); - return new Array([amount, current]); + return [...prev, [amount, current]]; }, []) - .map(([runningBalance, entry]: [number, ILedgerEntry]) => - this.entryMapper(entry, runningBalance) - ); + .map((entryPair: [number, ILedgerEntry]) => { + const [runningBalance, entry] = entryPair; + + return this.entryMapper(entry, runningBalance); + }); } /** @@ -224,8 +230,6 @@ export default class GeneralLedgerSheet extends R.compose( const depsAccountsIds = this.repository.accountsGraph.dependenciesOf(accountId); - console.log([...depsAccountsIds, accountId]); - const openingBalance = this.repository.openingBalanceTransactionsLedger .whereAccountsIds([...depsAccountsIds, accountId]) .getClosingBalance(); @@ -241,7 +245,7 @@ export default class GeneralLedgerSheet extends R.compose( }; /** - * + * Retrieves the closing balance with subaccounts total node. * @param {number} accountId * @returns {IGeneralLedgerSheetAccountBalance} */ @@ -256,6 +260,31 @@ export default class GeneralLedgerSheet extends R.compose( return { amount, formattedAmount, currencyCode, date }; }; + /** + * Detarmines whether the closing balance subaccounts node should be exist. + * @param {number} accountId + * @returns {boolean} + */ + private isAccountNodeIncludesClosingSubaccounts = (accountId: number) => { + // Retrun early if there is no accounts in the filter so + // return closing subaccounts in all cases. + if (isEmpty(this.query.accountsIds)) { + return true; + } + // Returns true if the given account id included in the filter. + const isIncluded = this.query.accountsIds.includes(accountId); + + const parentAccountIds = + this.repository.accountsGraph.dependantsOf(accountId); + + // Returns true if one of the parent account id exists in the filter. + const accountIdInChildren = R.any( + (parentAccountId) => R.includes(parentAccountId, this.query.accountsIds), + parentAccountIds + ); + return isIncluded || accountIdInChildren; + }; + /** * Retreive general ledger accounts sections. * @param {IAccount} account @@ -271,7 +300,12 @@ export default class GeneralLedgerSheet extends R.compose( const closingBalanceSubaccounts = this.accountClosingBalanceWithSubaccountsTotal(account.id); - return { + return R.compose( + R.when( + () => this.isAccountNodeIncludesClosingSubaccounts(account.id), + R.assoc('closingBalanceSubaccounts', closingBalanceSubaccounts) + ) + )({ id: account.id, name: account.name, code: account.code, @@ -280,8 +314,7 @@ export default class GeneralLedgerSheet extends R.compose( openingBalance, transactions, closingBalance, - closingBalanceSubaccounts, - }; + }); }; /** @@ -310,19 +343,42 @@ export default class GeneralLedgerSheet extends R.compose( * @param {IGeneralLedgerSheetAccount[]} nodes * @returns {IGeneralLedgerSheetAccount[]} */ - private filterAccountNodes = ( + private filterAccountNodesByTransactionsFilter = ( nodes: IGeneralLedgerSheetAccount[] ): IGeneralLedgerSheetAccount[] => { return this.filterNodesDeep( nodes, - (generalLedgerAccount: IGeneralLedgerSheetAccount) => - !( - generalLedgerAccount.transactions.length === 0 && - this.query.noneTransactions - ) + (account: IGeneralLedgerSheetAccount) => + !(account.transactions.length === 0 && this.query.noneTransactions) ); }; + /** + * Filters account nodes by the acounts filter. + * @param {IAccount[]} nodes + * @returns {IAccount[]} + */ + private filterAccountNodesByAccountsFilter = ( + nodes: IAccount[] + ): IAccount[] => { + return this.filterNodesDeep(nodes, (node: IGeneralLedgerSheetAccount) => { + if (R.isEmpty(this.query.accountsIds)) { + return true; + } + // Returns true if the given account id exists in the filter. + const isIncluded = this.query.accountsIds?.includes(node.id); + + const parentAccountIds = this.repository.accountsGraph.dependantsOf( + node.id + ); + // Returns true if one of th parent account ids exist in the filter. + const oneParentAccountIdExistInFilter = parentAccountIds.some((id) => + this.query.accountsIds?.includes(id) + ); + return isIncluded || oneParentAccountIdExistInFilter; + }); + }; + /** * Retrieves mapped accounts with general ledger transactions and * opeing/closing balance. @@ -331,8 +387,11 @@ export default class GeneralLedgerSheet extends R.compose( */ private accountsWalker(accounts: IAccount[]): IGeneralLedgerSheetAccount[] { return R.compose( - this.filterAccountNodes, + R.defaultTo([]), + this.filterAccountNodesByTransactionsFilter, this.accountNodesDeepMap, + R.defaultTo([]), + this.filterAccountNodesByAccountsFilter, this.nestedAccountsNode )(accounts); } @@ -342,6 +401,7 @@ export default class GeneralLedgerSheet extends R.compose( * @return {IGeneralLedgerSheetAccount[]} */ public reportData(): IGeneralLedgerSheetAccount[] { + console.log(this.repository.accounts); return this.accountsWalker(this.repository.accounts); } } diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerRepository.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerRepository.ts index 60ecca5dd..9d51a9806 100644 --- a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerRepository.ts +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerRepository.ts @@ -71,7 +71,9 @@ export class GeneralLedgerRepository { * Initialize the accounts. */ public async initAccounts() { - this.accounts = await this.repositories.accountRepository.all(); + this.accounts = await this.repositories.accountRepository + .all() + .orderBy('name', 'ASC'); } /** @@ -94,11 +96,13 @@ export class GeneralLedgerRepository { * Initialize the G/L transactions from/to the given date. */ public async initTransactions() { - this.transactions = await this.repositories.transactionsRepository.journal({ - fromDate: this.filter.fromDate, - toDate: this.filter.toDate, - branchesIds: this.filter.branchesIds, - }); + this.transactions = await this.repositories.transactionsRepository + .journal({ + fromDate: this.filter.fromDate, + toDate: this.filter.toDate, + branchesIds: this.filter.branchesIds, + }) + .orderBy('date', 'ASC'); // Transform array transactions to journal collection. this.transactionsLedger = Ledger.fromTransactions(this.transactions); } diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts index 4ca71578c..80f05f77b 100644 --- a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts @@ -272,10 +272,14 @@ export class GeneralLedgerTable extends R.compose( const closingBalanceWithSubaccounts = this.closingBalanceWithSubaccountsMapper(account); + // Appends the closing balance with sub-accounts row if the account + // has children accounts and the node is define. + const isAppendClosingSubaccounts = () => + account.children?.length > 0 && !!account.closingBalanceSubaccounts; + const children = R.compose( - // Appends the closing balance with sub-accounts row if the account has children accounts. R.when( - () => account.children?.length > 0, + isAppendClosingSubaccounts, R.append(closingBalanceWithSubaccounts) ), R.concat(R.defaultTo([], transactions)), From 708a4dda9eaa4ecfb0132bab8e2cd26c6a866fd0 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 6 Jun 2024 18:44:19 +0200 Subject: [PATCH 5/8] chore: remove the console.log --- .../services/FinancialStatements/GeneralLedger/GeneralLedger.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts index 4ee378a9d..d61fe9a50 100644 --- a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts @@ -401,7 +401,6 @@ export default class GeneralLedgerSheet extends R.compose( * @return {IGeneralLedgerSheetAccount[]} */ public reportData(): IGeneralLedgerSheetAccount[] { - console.log(this.repository.accounts); return this.accountsWalker(this.repository.accounts); } } From 94192bfc294fa2caa8bb6c34ce4a2d2c494b88e7 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 6 Jun 2024 18:48:33 +0200 Subject: [PATCH 6/8] fix: doctype general ledger --- .../GeneralLedger/GeneralLedger.ts | 38 ++++++++----------- .../GeneralLedger/_utils.ts | 13 +++++++ 2 files changed, 28 insertions(+), 23 deletions(-) create mode 100644 packages/server/src/services/FinancialStatements/GeneralLedger/_utils.ts diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts index d61fe9a50..e5ec99fb3 100644 --- a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts @@ -14,6 +14,7 @@ import { GeneralLedgerRepository } from './GeneralLedgerRepository'; import { FinancialSheetStructure } from '../FinancialSheetStructure'; import { flatToNestedArray } from '@/utils'; import Ledger from '@/services/Accounting/Ledger'; +import { calculateRunningBalance } from './_utils'; /** * General ledger sheet. @@ -21,11 +22,10 @@ import Ledger from '@/services/Accounting/Ledger'; export default class GeneralLedgerSheet extends R.compose( FinancialSheetStructure )(FinancialSheet) { - tenantId: number; - query: IGeneralLedgerSheetQuery; - baseCurrency: string; - i18n: any; - repository: GeneralLedgerRepository; + private query: IGeneralLedgerSheetQuery; + private baseCurrency: string; + private i18n: any; + private repository: GeneralLedgerRepository; /** * Constructor method. @@ -51,17 +51,6 @@ export default class GeneralLedgerSheet extends R.compose( this.i18n = i18n; } - /** - * Calculate the running balance. - * @param {number} amount - Transaction amount. - * @param {number} lastRunningBalance - Last running balance. - * @param {number} openingBalance - Opening balance. - * @return {number} Running balance. - */ - calculateRunningBalance(amount: number, lastRunningBalance: number): number { - return amount + lastRunningBalance; - } - /** * Entry mapper. * @param {ILedgerEntry} entry - @@ -79,16 +68,19 @@ export default class GeneralLedgerSheet extends R.compose( entry.debit, entry.accountNormal ); - return this.calculateRunningBalance(amount, lastRunningBalance); + return calculateRunningBalance(amount, lastRunningBalance); } /** - * - * @param entry - * @param runningBalance - * @returns + * Maps the given ledger entry to G/L transaction. + * @param {ILedgerEntry} entry + * @param {number} runningBalance + * @returns {IGeneralLedgerSheetAccountTransaction} */ - private entryMapper(entry: ILedgerEntry, runningBalance: number) { + private transactionMapper( + entry: ILedgerEntry, + runningBalance: number + ): IGeneralLedgerSheetAccountTransaction { const contact = this.repository.contactsById.get(entry.contactId); const amount = Ledger.getAmount( entry.credit, @@ -155,7 +147,7 @@ export default class GeneralLedgerSheet extends R.compose( .map((entryPair: [number, ILedgerEntry]) => { const [runningBalance, entry] = entryPair; - return this.entryMapper(entry, runningBalance); + return this.transactionMapper(entry, runningBalance); }); } diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/_utils.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/_utils.ts new file mode 100644 index 000000000..916b895c9 --- /dev/null +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/_utils.ts @@ -0,0 +1,13 @@ +/** + * Calculate the running balance. + * @param {number} amount - Transaction amount. + * @param {number} lastRunningBalance - Last running balance. + * @param {number} openingBalance - Opening balance. + * @return {number} Running balance. + */ +export function calculateRunningBalance( + amount: number, + lastRunningBalance: number +): number { + return amount + lastRunningBalance; +} From 8b99e0938d4652c7072a19de0d7042ce3c8b8b77 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 6 Jun 2024 18:50:24 +0200 Subject: [PATCH 7/8] fix: remove un-used code --- .../services/FinancialStatements/GeneralLedger/GeneralLedger.ts | 2 -- .../FinancialStatements/GeneralLedger/GeneralLedgerService.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts index e5ec99fb3..e797085a4 100644 --- a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts @@ -36,14 +36,12 @@ export default class GeneralLedgerSheet extends R.compose( * @param {IJournalPoster} closingBalancesJournal - */ constructor( - tenantId: number, query: IGeneralLedgerSheetQuery, repository: GeneralLedgerRepository, i18n ) { super(); - this.tenantId = tenantId; this.query = query; this.numberFormat = this.query.numberFormat; this.repository = repository; diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts index c2e407f5c..54066b345 100644 --- a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts @@ -62,7 +62,6 @@ export class GeneralLedgerService { // General ledger report instance. const generalLedgerInstance = new GeneralLedgerSheet( - tenantId, filter, genealLedgerRepository, i18n From 1cbc1c056ff28120a48fb158b619cde38e8c4f99 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 10 Jun 2024 08:08:47 +0200 Subject: [PATCH 8/8] feat: general ledger filter nodes --- .../GeneralLedger/GeneralLedger.ts | 41 ++++--------- .../GeneralLedger/GeneralLedgerRepository.ts | 57 ++++++++++++++++++- 2 files changed, 67 insertions(+), 31 deletions(-) diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts index e797085a4..5ae2bba76 100644 --- a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts @@ -261,18 +261,8 @@ export default class GeneralLedgerSheet extends R.compose( if (isEmpty(this.query.accountsIds)) { return true; } - // Returns true if the given account id included in the filter. - const isIncluded = this.query.accountsIds.includes(accountId); - - const parentAccountIds = - this.repository.accountsGraph.dependantsOf(accountId); - - // Returns true if one of the parent account id exists in the filter. - const accountIdInChildren = R.any( - (parentAccountId) => R.includes(parentAccountId, this.query.accountsIds), - parentAccountIds - ); - return isIncluded || accountIdInChildren; + // Returns true if the given account id includes transactions. + return this.repository.accountNodesIncludeTransactions.includes(accountId); }; /** @@ -290,12 +280,7 @@ export default class GeneralLedgerSheet extends R.compose( const closingBalanceSubaccounts = this.accountClosingBalanceWithSubaccountsTotal(account.id); - return R.compose( - R.when( - () => this.isAccountNodeIncludesClosingSubaccounts(account.id), - R.assoc('closingBalanceSubaccounts', closingBalanceSubaccounts) - ) - )({ + const initialNode = { id: account.id, name: account.name, code: account.code, @@ -304,7 +289,14 @@ export default class GeneralLedgerSheet extends R.compose( openingBalance, transactions, closingBalance, - }); + }; + + return R.compose( + R.when( + () => this.isAccountNodeIncludesClosingSubaccounts(account.id), + R.assoc('closingBalanceSubaccounts', closingBalanceSubaccounts) + ) + )(initialNode); }; /** @@ -356,16 +348,7 @@ export default class GeneralLedgerSheet extends R.compose( return true; } // Returns true if the given account id exists in the filter. - const isIncluded = this.query.accountsIds?.includes(node.id); - - const parentAccountIds = this.repository.accountsGraph.dependantsOf( - node.id - ); - // Returns true if one of th parent account ids exist in the filter. - const oneParentAccountIdExistInFilter = parentAccountIds.some((id) => - this.query.accountsIds?.includes(id) - ); - return isIncluded || oneParentAccountIdExistInFilter; + return this.repository.accountNodeInclude?.includes(node.id); }); }; diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerRepository.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerRepository.ts index 9d51a9806..875b4fefa 100644 --- a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerRepository.ts +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerRepository.ts @@ -1,4 +1,5 @@ import moment from 'moment'; +import * as R from 'ramda'; import { IAccount, IAccountTransaction, @@ -9,6 +10,7 @@ import { import Ledger from '@/services/Accounting/Ledger'; import { transformToMap } from '@/utils'; import { Tenant } from '@/system/models'; +import { flatten, isEmpty, uniq } from 'lodash'; export class GeneralLedgerRepository { public filter: IGeneralLedgerSheetQuery; @@ -30,6 +32,9 @@ export class GeneralLedgerRepository { public tenantId: number; public tenant: ITenant; + public accountNodesIncludeTransactions: Array = []; + public accountNodeInclude: Array = []; + /** * Constructor method. * @param models @@ -54,8 +59,10 @@ export class GeneralLedgerRepository { await this.initAccounts(); await this.initAccountsGraph(); await this.initContacts(); - await this.initTransactions(); await this.initAccountsOpeningBalance(); + this.initAccountNodesIncludeTransactions(); + await this.initTransactions(); + this.initAccountNodesIncluded(); } /** @@ -102,7 +109,12 @@ export class GeneralLedgerRepository { toDate: this.filter.toDate, branchesIds: this.filter.branchesIds, }) - .orderBy('date', 'ASC'); + .orderBy('date', 'ASC') + .onBuild((query) => { + if (this.filter.accountsIds?.length > 0) { + query.whereIn('accountId', this.accountNodesIncludeTransactions); + } + }); // Transform array transactions to journal collection. this.transactionsLedger = Ledger.fromTransactions(this.transactions); } @@ -124,4 +136,45 @@ export class GeneralLedgerRepository { this.openingBalanceTransactions ); } + + /** + * Initialize the account nodes that should include transactions. + * @returns {void} + */ + public initAccountNodesIncludeTransactions() { + if (isEmpty(this.filter.accountsIds)) { + return; + } + const childrenNodeIds = this.filter.accountsIds?.map( + (accountId: number) => { + return this.accountsGraph.dependenciesOf(accountId); + } + ); + const nodeIds = R.concat(this.filter.accountsIds, childrenNodeIds); + + this.accountNodesIncludeTransactions = uniq(flatten(nodeIds)); + } + + /** + * Initialize the account node ids should be included, + * if the filter by acounts is presented. + * @returns {void} + */ + public initAccountNodesIncluded() { + if (isEmpty(this.filter.accountsIds)) { + return; + } + const nodeIds = this.filter.accountsIds.map((accountId) => { + const childrenIds = this.accountsGraph.dependenciesOf(accountId); + const parentIds = this.accountsGraph.dependantsOf(accountId); + + return R.concat(childrenIds, parentIds); + }); + + this.accountNodeInclude = R.compose( + R.uniq, + R.flatten, + R.concat(this.filter.accountsIds) + )(nodeIds); + } }