From ba00e8baeb0b234175fc23b30df8054d80fa36c1 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 19 Oct 2023 19:32:27 +0200 Subject: [PATCH] fix: trial balance sheet adjusted balance --- .../FinancialStatements/TrialBalanceSheet.ts | 33 ++-- packages/server/src/interfaces/Ledger.ts | 2 + .../server/src/services/Accounting/Ledger.ts | 18 ++- .../TrialBalanceSheet/TrialBalanceSheet.ts | 74 ++++++--- .../TrialBalanceSheetRepository.ts | 104 +++++++++++++ .../TrialBalanceSheetService.ts | 72 +++++---- .../TrialBalanceSheetTable.ts | 146 ++++++++++++++++++ .../TrialBalanceSheet/_constants.ts | 5 + packages/webapp/src/lang/en/index.json | 1 + 9 files changed, 388 insertions(+), 67 deletions(-) create mode 100644 packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetRepository.ts create mode 100644 packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetTable.ts create mode 100644 packages/server/src/services/FinancialStatements/TrialBalanceSheet/_constants.ts diff --git a/packages/server/src/api/controllers/FinancialStatements/TrialBalanceSheet.ts b/packages/server/src/api/controllers/FinancialStatements/TrialBalanceSheet.ts index 4fa298fa0..da59287e4 100644 --- a/packages/server/src/api/controllers/FinancialStatements/TrialBalanceSheet.ts +++ b/packages/server/src/api/controllers/FinancialStatements/TrialBalanceSheet.ts @@ -16,7 +16,7 @@ export default class TrialBalanceSheetController extends BaseFinancialReportCont /** * Router constructor. */ - router() { + public router() { const router = Router(); router.get( @@ -36,7 +36,7 @@ export default class TrialBalanceSheetController extends BaseFinancialReportCont * Validation schema. * @return {ValidationChain[]} */ - get trialBalanceSheetValidationSchema(): ValidationChain[] { + private get trialBalanceSheetValidationSchema(): ValidationChain[] { return [ ...this.sheetNumberFormatValidationSchema, query('basis').optional(), @@ -59,28 +59,37 @@ export default class TrialBalanceSheetController extends BaseFinancialReportCont /** * Retrieve the trial balance sheet. */ - public async trialBalanceSheet( + private async trialBalanceSheet( req: Request, res: Response, next: NextFunction ) { - const { tenantId, settings } = req; + const { tenantId } = req; let filter = this.matchedQueryData(req); filter = { ...filter, accountsIds: castArray(filter.accountsIds), }; - try { - const { data, query, meta } = - await this.trialBalanceSheetService.trialBalanceSheet(tenantId, filter); + const accept = this.accepts(req); + const acceptType = accept.types(['json', 'application/json+table']); - return res.status(200).send({ - data: this.transfromToResponse(data), - query: this.transfromToResponse(query), - meta: this.transfromToResponse(meta), - }); + if (acceptType === 'application/json+table') { + const { table, meta, query } = + await this.trialBalanceSheetService.trialBalanceSheetTable( + tenantId, + filter + ); + return res.status(200).send({ table, meta, query }); + } else { + const { data, query, meta } = + await this.trialBalanceSheetService.trialBalanceSheet( + tenantId, + filter + ); + return res.status(200).send({ data, query, meta }); + } } catch (error) { next(error); } diff --git a/packages/server/src/interfaces/Ledger.ts b/packages/server/src/interfaces/Ledger.ts index 0f6379676..2305d1ed3 100644 --- a/packages/server/src/interfaces/Ledger.ts +++ b/packages/server/src/interfaces/Ledger.ts @@ -16,6 +16,8 @@ export interface ILedger { getClosingBalance(): number; getForeignClosingBalance(): number; + getClosingDebit(): number; + getClosingCredit(): number; getContactsIds(): number[]; getAccountsIds(): number[]; diff --git a/packages/server/src/services/Accounting/Ledger.ts b/packages/server/src/services/Accounting/Ledger.ts index 7cb71bed8..ab241ebe2 100644 --- a/packages/server/src/services/Accounting/Ledger.ts +++ b/packages/server/src/services/Accounting/Ledger.ts @@ -1,5 +1,5 @@ import moment from 'moment'; -import { defaultTo, uniqBy } from 'lodash'; +import { defaultTo, sumBy, uniqBy } from 'lodash'; import { IAccountTransaction, ILedger, ILedgerEntry } from '@/interfaces'; export default class Ledger implements ILedger { @@ -130,6 +130,22 @@ export default class Ledger implements ILedger { return closingBalance; } + /** + * Retrieves the closing credit of the entries. + * @returns {number} + */ + public getClosingCredit(): number { + return sumBy(this.entries, 'credit'); + } + + /** + * Retrieves the closing debit of the entries. + * @returns {number} + */ + public getClosingDebit(): number { + return sumBy(this.entries, 'debit'); + } + /** * Retrieve the closing balance of the entries. * @returns {number} diff --git a/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheet.ts b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheet.ts index 980bd1dd4..4a517650c 100644 --- a/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheet.ts +++ b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheet.ts @@ -10,13 +10,12 @@ import { } from '@/interfaces'; import FinancialSheet from '../FinancialSheet'; import { allPassedConditionsPass, flatToNestedArray } from 'utils'; +import { TrialBalanceSheetRepository } from './TrialBalanceSheetRepository'; export default class TrialBalanceSheet extends FinancialSheet { - tenantId: number; - query: ITrialBalanceSheetQuery; - accounts: IAccount & { type: IAccountType }[]; - journalFinancial: any; - baseCurrency: string; + private query: ITrialBalanceSheetQuery; + private repository: TrialBalanceSheetRepository; + private baseCurrency: string; /** * Constructor method. @@ -28,20 +27,52 @@ export default class TrialBalanceSheet extends FinancialSheet { constructor( tenantId: number, query: ITrialBalanceSheetQuery, - accounts: IAccount & { type: IAccountType }[], - journalFinancial: any, + repository: TrialBalanceSheetRepository, baseCurrency: string ) { super(); this.tenantId = tenantId; this.query = query; - this.accounts = accounts; - this.journalFinancial = journalFinancial; + this.repository = repository; this.numberFormat = this.query.numberFormat; this.baseCurrency = baseCurrency; } + /** + * Retrieves the closing credit of the given account. + * @param {number} accountId + * @returns {number} + */ + public getClosingAccountCredit(accountId: number) { + return this.repository.totalAccountsLedger + .whereAccountId(accountId) + .getClosingCredit(); + } + + /** + * Retrieves the closing debit of the given account. + * @param {number} accountId + * @returns {number} + */ + public getClosingAccountDebit(accountId: number) { + return this.repository.totalAccountsLedger + .whereAccountId(accountId) + .getClosingDebit(); + } + + /** + * Retrieves the closing total of the given account. + * @param {number} accountId + * @returns {number} + */ + public getClosingAccountTotal(accountId: number) { + const credit = this.getClosingAccountCredit(accountId); + const debit = this.getClosingAccountDebit(accountId); + + return debit - credit; + } + /** * Account mapper. * @param {IAccount} account @@ -50,7 +81,9 @@ export default class TrialBalanceSheet extends FinancialSheet { private accountTransformer = ( account: IAccount & { type: IAccountType } ): ITrialBalanceAccount => { - const trial = this.journalFinancial.getTrialBalanceWithDepands(account.id); + const debit = this.getClosingAccountDebit(account.id); + const credit = this.getClosingAccountCredit(account.id); + const balance = this.getClosingAccountTotal(account.id); return { id: account.id, @@ -59,14 +92,14 @@ export default class TrialBalanceSheet extends FinancialSheet { code: account.code, accountNormal: account.accountNormal, - credit: trial.credit, - debit: trial.debit, - balance: trial.balance, + credit, + debit, + balance, currencyCode: this.baseCurrency, - formattedCredit: this.formatNumber(trial.credit), - formattedDebit: this.formatNumber(trial.debit), - formattedBalance: this.formatNumber(trial.balance), + formattedCredit: this.formatNumber(credit), + formattedDebit: this.formatNumber(debit), + formattedBalance: this.formatNumber(balance), }; }; @@ -117,10 +150,7 @@ export default class TrialBalanceSheet extends FinancialSheet { private filterNoneTransactions = ( accountNode: ITrialBalanceAccount ): boolean => { - const entries = this.journalFinancial.getAccountEntriesWithDepents( - accountNode.id - ); - return entries.length > 0; + return false === this.repository.totalAccountsLedger.isEmpty(); }; /** @@ -200,11 +230,11 @@ export default class TrialBalanceSheet extends FinancialSheet { */ public reportData(): ITrialBalanceSheetData { // Don't return noting if the journal has no transactions. - if (this.journalFinancial.isEmpty()) { + if (this.repository.totalAccountsLedger.isEmpty()) { return null; } // Retrieve accounts nodes. - const accounts = this.accountsSection(this.accounts); + const accounts = this.accountsSection(this.repository.accounts); // Retrieve account node. const total = this.tatalSection(accounts); diff --git a/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetRepository.ts b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetRepository.ts new file mode 100644 index 000000000..cffc71c7a --- /dev/null +++ b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetRepository.ts @@ -0,0 +1,104 @@ +import { ITrialBalanceSheetQuery } from '@/interfaces'; +import Ledger from '@/services/Accounting/Ledger'; +import { Knex } from 'knex'; +import { isEmpty } from 'lodash'; +import { Service } from 'typedi'; + +@Service() +export class TrialBalanceSheetRepository { + private query: ITrialBalanceSheetQuery; + private models: any; + + /** + * + */ + public accounts: any; + + /** + * Total closing accounts ledger. + * @param {Ledger} + */ + public totalAccountsLedger: Ledger; + + /** + * Constructor method. + * @param {number} tenantId + * @param {IBalanceSheetQuery} query + */ + constructor(models: any, query: ITrialBalanceSheetQuery) { + this.query = query; + this.models = models; + } + + /** + * Async initialize. + * @returns {Promise} + */ + public asyncInitialize = async () => { + await this.initAccounts(); + await this.initAccountsClosingTotalLedger(); + }; + + // ---------------------------- + // # Accounts + // ---------------------------- + /** + * Initialize accounts. + * @returns {Promise} + */ + public initAccounts = async () => { + const accounts = await this.getAccounts(); + + this.accounts = accounts; + }; + + /** + * Initialize all accounts closing total ledger. + * @return {Promise} + */ + public initAccountsClosingTotalLedger = async (): Promise => { + const totalByAccounts = await this.closingAccountsTotal(this.query.toDate); + + this.totalAccountsLedger = Ledger.fromTransactions(totalByAccounts); + }; + + /** + * Retrieve accounts of the report. + * @return {Promise} + */ + private getAccounts = () => { + const { Account } = this.models; + + return Account.query(); + }; + + /** + * Retrieve the opening balance transactions of the report. + * @param {Date|string} openingDate - + */ + public closingAccountsTotal = async (openingDate: Date | string) => { + const { AccountTransaction } = this.models; + + return AccountTransaction.query().onBuild((query) => { + query.sum('credit as credit'); + query.sum('debit as debit'); + query.groupBy('accountId'); + query.select(['accountId']); + + query.modify('filterDateRange', null, openingDate); + query.withGraphFetched('account'); + + this.commonFilterBranchesQuery(query); + }); + }; + + /** + * Common branches filter query. + * @param {Knex.QueryBuilder} query + */ + private commonFilterBranchesQuery = (query: Knex.QueryBuilder) => { + if (!isEmpty(this.query.branchesIds)) { + query.modify('filterByBranches', this.query.branchesIds); + } + }; +} diff --git a/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetService.ts b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetService.ts index f22624a3e..1b7789d62 100644 --- a/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetService.ts +++ b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetService.ts @@ -2,12 +2,18 @@ import { Service, Inject } from 'typedi'; import moment from 'moment'; import TenancyService from '@/services/Tenancy/TenancyService'; import Journal from '@/services/Accounting/JournalPoster'; -import { ITrialBalanceSheetMeta, ITrialBalanceSheetQuery, ITrialBalanceStatement } from '@/interfaces'; +import { + ITrialBalanceSheetMeta, + ITrialBalanceSheetQuery, + ITrialBalanceStatement, +} from '@/interfaces'; import TrialBalanceSheet from './TrialBalanceSheet'; import FinancialSheet from '../FinancialSheet'; import InventoryService from '@/services/Inventory/Inventory'; import { parseBoolean } from 'utils'; import { Tenant } from '@/system/models'; +import { TrialBalanceSheetRepository } from './TrialBalanceSheetRepository'; +import { TrialBalanceSheetTable } from './TrialBalanceSheetTable'; @Service() export default class TrialBalanceSheetService extends FinancialSheet { @@ -51,9 +57,8 @@ export default class TrialBalanceSheetService extends FinancialSheet { reportMetadata(tenantId: number): ITrialBalanceSheetMeta { const settings = this.tenancy.settings(tenantId); - const isCostComputeRunning = this.inventoryService.isItemsCostComputeRunning( - tenantId - ); + const isCostComputeRunning = + this.inventoryService.isItemsCostComputeRunning(tenantId); const organizationName = settings.get({ group: 'organization', key: 'name', @@ -72,10 +77,8 @@ export default class TrialBalanceSheetService extends FinancialSheet { /** * Retrieve trial balance sheet statement. - * ------------- * @param {number} tenantId * @param {IBalanceSheetQuery} query - * * @return {IBalanceSheetStatement} */ public async trialBalanceSheet( @@ -86,43 +89,25 @@ export default class TrialBalanceSheetService extends FinancialSheet { ...this.defaultQuery, ...query, }; - const { - accountRepository, - transactionsRepository, - } = this.tenancy.repositories(tenantId); const tenant = await Tenant.query() .findById(tenantId) .withGraphFetched('metadata'); - this.logger.info('[trial_balance_sheet] trying to calcualte the report.', { - tenantId, - filter, - }); - // Retrieve all accounts on the storage. - const accounts = await accountRepository.all(); - const accountsGraph = await accountRepository.getDependencyGraph(); + const models = this.tenancy.models(tenantId); - // Retrieve all journal transactions based on the given query. - const transactions = await transactionsRepository.journal({ - fromDate: query.fromDate, - toDate: query.toDate, - sumationCreditDebit: true, - branchesIds: query.branchesIds - }); - // Transform transactions array to journal collection. - const transactionsJournal = Journal.fromTransactions( - transactions, - tenantId, - accountsGraph + const trialBalanceSheetRepos = new TrialBalanceSheetRepository( + models, + filter ); + await trialBalanceSheetRepos.asyncInitialize(); + // Trial balance report instance. const trialBalanceInstance = new TrialBalanceSheet( tenantId, filter, - accounts, - transactionsJournal, - tenant.metadata.baseCurrency, + trialBalanceSheetRepos, + tenant.metadata.baseCurrency ); // Trial balance sheet data. const trialBalanceSheetData = trialBalanceInstance.reportData(); @@ -133,4 +118,27 @@ export default class TrialBalanceSheetService extends FinancialSheet { meta: this.reportMetadata(tenantId), }; } + + /** + * Retrieves the trial balance sheet table. + * @param {number} tenantId + * @param {ITrialBalanceSheetQuery} query + * @returns {Promise} + */ + public async trialBalanceSheetTable( + tenantId: number, + query: ITrialBalanceSheetQuery + ) { + const trialBalance = await this.trialBalanceSheet(tenantId, query); + const table = new TrialBalanceSheetTable(trialBalance.data, query, {}); + + return { + table: { + columns: table.tableColumns(), + rows: table.tableRows(), + }, + meta: trialBalance.meta, + query: trialBalance.query, + }; + } } diff --git a/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetTable.ts b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetTable.ts new file mode 100644 index 000000000..b00d379c8 --- /dev/null +++ b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetTable.ts @@ -0,0 +1,146 @@ +import * as R from 'ramda'; +import FinancialSheet from '../FinancialSheet'; +import { FinancialTable } from '../FinancialTable'; +import { + IBalanceSheetStatementData, + ITableColumn, + ITableColumnAccessor, + ITableRow, + ITrialBalanceAccount, + ITrialBalanceSheetData, + ITrialBalanceSheetQuery, + ITrialBalanceTotal, +} from '@/interfaces'; +import { tableRowMapper } from '@/utils'; +import { IROW_TYPE } from '../BalanceSheet/constants'; +import { FinancialSheetStructure } from '../FinancialSheetStructure'; + +export class TrialBalanceSheetTable extends R.compose( + FinancialTable, + FinancialSheetStructure +)(FinancialSheet) { + /** + * @param {ITrialBalanceSheetData} + */ + public data: ITrialBalanceSheetData; + + /** + * Balance sheet query. + * @param {ITrialBalanceSheetQuery} + */ + public query: ITrialBalanceSheetQuery; + + /** + * Constructor method. + * @param {IBalanceSheetStatementData} reportData - + * @param {ITrialBalanceSheetQuery} query - + */ + constructor( + data: ITrialBalanceSheetData, + query: ITrialBalanceSheetQuery, + i18n: any + ) { + super(); + + this.data = data; + this.query = query; + this.i18n = i18n; + } + + /** + * Retrieve the common columns for all report nodes. + * @param {ITableColumnAccessor[]} + */ + private commonColumnsAccessors = (): ITableColumnAccessor[] => { + return [ + { key: 'account', accessor: 'name' }, + { key: 'debit', accessor: 'formattedDebit' }, + { key: 'credit', accessor: 'formattedCredit' }, + { key: 'total', accessor: 'formattedBalance' }, + ]; + }; + + /** + * Maps the account node to table row. + * @param {ITrialBalanceAccount} node - + * @returns {ITableRow} + */ + private accountNodeTableRowsMapper = ( + node: ITrialBalanceAccount + ): ITableRow => { + const columns = this.commonColumnsAccessors(); + const meta = { + rowTypes: [IROW_TYPE.ACCOUNT], + id: node.id, + }; + return tableRowMapper(node, columns, meta); + }; + + /** + * Maps the total node to table row. + * @param {ITrialBalanceTotal} node - + * @returns {ITableRow} + */ + private totalNodeTableRowsMapper = (node: ITrialBalanceTotal): ITableRow => { + const columns = this.commonColumnsAccessors(); + const meta = { + rowTypes: [IROW_TYPE.TOTAL], + id: node.id, + }; + return tableRowMapper(node, columns, meta); + }; + + /** + * Mappes the given report sections to table rows. + * @param {IBalanceSheetDataNode[]} nodes - + * @return {ITableRow} + */ + private accountsToTableRowsMap = ( + nodes: ITrialBalanceAccount[] + ): ITableRow[] => { + return this.mapNodesDeep(nodes, this.accountNodeTableRowsMapper); + }; + + /** + * Retrieves the accounts table rows of the given report data. + * @returns {ITableRow[]} + */ + private accountsTableRows = (): ITableRow[] => { + return this.accountsToTableRowsMap(this.data.accounts); + }; + + /** + * Maps the given total node to table row. + * @returns {ITableRow} + */ + private totalTableRow = (): ITableRow => { + return this.totalNodeTableRowsMapper(this.data.total); + }; + + /** + * Retrieves the table rows. + * @returns {ITableRow[]} + */ + public tableRows = (): ITableRow[] => { + return R.compose( + R.append(this.totalTableRow()), + R.concat(this.accountsTableRows()) + )([]); + }; + + /** + * Retrrieves the table columns. + * @returns {ITableColumn[]} + */ + public tableColumns = (): ITableColumn[] => { + return R.compose( + this.tableColumnsCellIndexing, + R.concat([ + { key: 'account_name', label: 'Account' }, + { key: 'debit', label: 'Debit' }, + { key: 'credit', label: 'Credit' }, + { key: 'total', label: 'Total' }, + ]) + )([]); + }; +} diff --git a/packages/server/src/services/FinancialStatements/TrialBalanceSheet/_constants.ts b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/_constants.ts new file mode 100644 index 000000000..91e8c595f --- /dev/null +++ b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/_constants.ts @@ -0,0 +1,5 @@ + +export enum IROW_TYPE { + ACCOUNT = 'ACCOUNT', + TOTAL = 'TOTAL', +} \ No newline at end of file diff --git a/packages/webapp/src/lang/en/index.json b/packages/webapp/src/lang/en/index.json index 1b2e25f1f..f4a2d91cf 100644 --- a/packages/webapp/src/lang/en/index.json +++ b/packages/webapp/src/lang/en/index.json @@ -1804,6 +1804,7 @@ "balance_sheet.total_change": "Total Change", "balance_sheet.change": "% Change", "balance_sheet.previous_period": "Previous Period (PP)", + "balance_sheet.net_income": "Net Income", "profit_loss_sheet.comparisons": "Comparisons", "profit_loss_sheet.dimensions": "Dimensions", "profit_loss_sheet.previous_year": "Previous Year",