diff --git a/packages/server/src/interfaces/GeneralLedgerSheet.ts b/packages/server/src/interfaces/GeneralLedgerSheet.ts index 6de1bda3b..12f1e0883 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/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..5ae2bba76 100644 --- a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts @@ -1,29 +1,31 @@ -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, IGeneralLedgerSheetAccountBalance, IGeneralLedgerSheetAccountTransaction, IAccount, - IJournalPoster, - IJournalEntry, - IContact, + ILedgerEntry, } from '@/interfaces'; import FinancialSheet from '../FinancialSheet'; -import moment from 'moment'; +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. */ -export default class GeneralLedgerSheet extends FinancialSheet { - tenantId: number; - accounts: IAccount[]; - query: IGeneralLedgerSheetQuery; - openingBalancesJournal: IJournalPoster; - transactions: IJournalPoster; - contactsMap: Map; - baseCurrency: string; - i18n: any; +export default class GeneralLedgerSheet extends R.compose( + FinancialSheetStructure +)(FinancialSheet) { + private query: IGeneralLedgerSheetQuery; + private baseCurrency: string; + private i18n: any; + private repository: GeneralLedgerRepository; /** * Constructor method. @@ -34,63 +36,59 @@ export default class GeneralLedgerSheet extends FinancialSheet { * @param {IJournalPoster} closingBalancesJournal - */ constructor( - tenantId: number, query: IGeneralLedgerSheetQuery, - accounts: IAccount[], - contactsByIdMap: Map, - transactions: IJournalPoster, - openingBalancesJournal: IJournalPoster, - baseCurrency: string, + repository: GeneralLedgerRepository, i18n ) { super(); - 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; } - /** - * Retrieve the transaction amount. - * @param {number} credit - Credit amount. - * @param {number} debit - Debit amount. - * @param {string} normal - Credit or debit. - */ - getAmount(credit: number, debit: number, normal: string) { - return normal === 'credit' ? credit - debit : debit - credit; - } - /** * Entry mapper. - * @param {IJournalEntry} entry - + * @param {ILedgerEntry} entry - * @return {IGeneralLedgerSheetAccountTransaction} */ - entryReducer( - entries: IGeneralLedgerSheetAccountTransaction[], - entry: IJournalEntry, - openingBalance: number - ): IGeneralLedgerSheetAccountTransaction[] { - const lastEntry = last(entries); + private getEntryRunningBalance( + entry: ILedgerEntry, + openingBalance: number, + runningBalance?: number + ): number { + const lastRunningBalance = runningBalance || openingBalance; - const contact = this.contactsMap.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 calculateRunningBalance(amount, lastRunningBalance); + } - const newEntry = { + /** + * Maps the given ledger entry to G/L transaction. + * @param {ILedgerEntry} entry + * @param {number} runningBalance + * @returns {IGeneralLedgerSheetAccountTransaction} + */ + private transactionMapper( + entry: ILedgerEntry, + runningBalance: number + ): IGeneralLedgerSheetAccountTransaction { + 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, @@ -109,16 +107,15 @@ export default class GeneralLedgerSheet extends FinancialSheet { 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, - }; - entries.push(newEntry); - - return entries; + } as IGeneralLedgerSheetAccountTransaction; } /** @@ -130,28 +127,48 @@ export default class GeneralLedgerSheet extends FinancialSheet { account: IAccount, openingBalance: number ): IGeneralLedgerSheetAccountTransaction[] { - const entries = this.transactions.getAccountEntries(account.id); - - return entries.reduce( - ( - entries: IGeneralLedgerSheetAccountTransaction[], - entry: IJournalEntry - ) => { - return this.entryReducer(entries, entry, openingBalance); - }, - [] - ); + const entries = this.repository.transactionsLedger + .whereAccountId(account.id) + .getEntries(); + + 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, + prevRunningBalance + ); + return [...prev, [amount, current]]; + }, []) + .map((entryPair: [number, ILedgerEntry]) => { + const [runningBalance, entry] = entryPair; + + return this.transactionMapper(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.openingBalancesJournal.getAccountBalance(account.id); + const amount = this.accountOpeningBalance(accountId); const formattedAmount = this.formatTotalNumber(amount); const currencyCode = this.baseCurrency; const date = this.query.fromDate; @@ -160,15 +177,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; @@ -176,31 +209,78 @@ 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); + + const openingBalance = this.repository.openingBalanceTransactionsLedger + .whereAccountsIds([...depsAccountsIds, accountId]) + .getClosingBalance(); + + const transactionsBalanceWithSubAccounts = + this.repository.transactionsLedger + .whereAccountsIds([...depsAccountsIds, accountId]) + .getClosingBalance(); + + const closingBalance = openingBalance + transactionsBalanceWithSubAccounts; + + return closingBalance; + }; + + /** + * Retrieves the closing balance with subaccounts total node. + * @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 }; + }; + + /** + * 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 includes transactions. + return this.repository.accountNodesIncludeTransactions.includes(accountId); + }; /** * 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 { + const initialNode = { id: account.id, name: account.name, code: account.code, @@ -210,34 +290,90 @@ export default class GeneralLedgerSheet extends FinancialSheet { transactions, closingBalance, }; - } + + return R.compose( + R.when( + () => this.isAccountNodeIncludesClosingSubaccounts(account.id), + R.assoc('closingBalanceSubaccounts', closingBalanceSubaccounts) + ) + )(initialNode); + }; + + /** + * 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 filterAccountNodesByTransactionsFilter = ( + nodes: IGeneralLedgerSheetAccount[] + ): IGeneralLedgerSheetAccount[] => { + return this.filterNodesDeep( + nodes, + (account: IGeneralLedgerSheetAccount) => + !(account.transactions.length === 0 && this.query.noneTransactions) + ); + }; /** - * Retrieve mapped accounts with general ledger transactions and opeing/closing balance. + * 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. + return this.repository.accountNodeInclude?.includes(node.id); + }); + }; + + /** + * 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( + R.defaultTo([]), + this.filterAccountNodesByTransactionsFilter, + this.accountNodesDeepMap, + R.defaultTo([]), + this.filterAccountNodesByAccountsFilter, + this.nestedAccountsNode + )(accounts); } /** - * Retrieve general ledger report data. + * Retrieves general ledger report data. * @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..875b4fefa --- /dev/null +++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerRepository.ts @@ -0,0 +1,180 @@ +import moment from 'moment'; +import * as R from 'ramda'; +import { + IAccount, + IAccountTransaction, + IContact, + IGeneralLedgerSheetQuery, + ITenant, +} from '@/interfaces'; +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; + 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; + + public accountNodesIncludeTransactions: Array = []; + public accountNodeInclude: Array = []; + + /** + * 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.initAccountsOpeningBalance(); + this.initAccountNodesIncludeTransactions(); + await this.initTransactions(); + this.initAccountNodesIncluded(); + } + + /** + * 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() + .orderBy('name', 'ASC'); + } + + /** + * 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, + }) + .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); + } + + /** + * 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 + ); + } + + /** + * 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); + } +} diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts index 53451d2d8..54066b345 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,24 @@ 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. diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts index 1820ab095..80f05f77b 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_' }, @@ -113,6 +116,36 @@ export class GeneralLedgerTable extends R.compose( ]; } + /** + * Closing balance row column accessors. + * @param {IGeneralLedgerSheetAccount} account - + * @returns {ITableColumnAccessor[]} + */ + private closingBalanceWithSubaccountsColumnAccessors( + account: IGeneralLedgerSheetAccount + ): IColumnMapperMeta[] { + return [ + { + 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_' }, + { 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[]} @@ -184,7 +217,22 @@ 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], + }; + 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(account); const meta = { rowTypes: [ROW_TYPE.CLOSING_BALANCE], }; @@ -221,8 +269,27 @@ 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); + // 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( + R.when( + isAppendClosingSubaccounts, + 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 +300,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 +317,6 @@ export class GeneralLedgerTable extends R.compose( */ public tableColumns(): ITableColumn[] { const columns = this.commonColumns(); - return R.compose(this.tableColumnsCellIndexing)(columns); } } 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; +} 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; + } } } }