diff --git a/packages/server-nest/package.json b/packages/server-nest/package.json index 52f7e15ca..c88928ff8 100644 --- a/packages/server-nest/package.json +++ b/packages/server-nest/package.json @@ -80,6 +80,7 @@ "strategy": "^1.1.1", "uniqid": "^5.2.0", "uuid": "^10.0.0", + "xlsx": "^0.18.5", "yup": "^0.28.1", "zod": "^3.23.8" }, diff --git a/packages/server-nest/src/common/types/Constructor.ts b/packages/server-nest/src/common/types/Constructor.ts new file mode 100644 index 000000000..c9a49e5d2 --- /dev/null +++ b/packages/server-nest/src/common/types/Constructor.ts @@ -0,0 +1,2 @@ + +export type Constructor = new (...args: any[]) => {}; diff --git a/packages/server-nest/src/constants/accept-type.ts b/packages/server-nest/src/constants/accept-type.ts new file mode 100644 index 000000000..bd89e8fb0 --- /dev/null +++ b/packages/server-nest/src/constants/accept-type.ts @@ -0,0 +1,8 @@ +export const AcceptType = { + ApplicationPdf: 'application/pdf', + ApplicationJson: 'application/json', + ApplicationJsonTable: 'application/json+table', + ApplicationXlsx: 'application/xlsx', + ApplicationCsv: 'application/csv', + ApplicationTextHtml: 'application/json+html', +}; diff --git a/packages/server-nest/src/modules/App/App.module.ts b/packages/server-nest/src/modules/App/App.module.ts index c8ef58b44..81c4f8631 100644 --- a/packages/server-nest/src/modules/App/App.module.ts +++ b/packages/server-nest/src/modules/App/App.module.ts @@ -66,6 +66,7 @@ import { InventoryAdjustmentsModule } from '../InventoryAdjutments/InventoryAdju import { PostHogModule } from '../EventsTracker/postHog.module'; import { EventTrackerModule } from '../EventsTracker/EventTracker.module'; import { MailModule } from '../Mail/Mail.module'; +import { FinancialStatementsModule } from '../FinancialStatements/FinancialStatements.module'; @Module({ imports: [ @@ -156,6 +157,7 @@ import { MailModule } from '../Mail/Mail.module'; InventoryAdjustmentsModule, PostHogModule, EventTrackerModule, + FinancialStatementsModule, ], controllers: [AppController], providers: [ diff --git a/packages/server-nest/src/modules/FinancialStatements/FinancialStatements.module.ts b/packages/server-nest/src/modules/FinancialStatements/FinancialStatements.module.ts new file mode 100644 index 000000000..117caf95b --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/FinancialStatements.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { TableSheetPdf } from './TableSheetPdf'; +import { PurchasesByItemsModule } from './modules/PurchasesByItems/PurchasesByItems.module'; + +@Module({ + providers: [TableSheetPdf], + imports: [PurchasesByItemsModule], +}) +export class FinancialStatementsModule {} diff --git a/packages/server-nest/src/modules/FinancialStatements/TableSheetPdf.ts b/packages/server-nest/src/modules/FinancialStatements/TableSheetPdf.ts new file mode 100644 index 000000000..4385c0625 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/TableSheetPdf.ts @@ -0,0 +1,75 @@ +import * as R from 'ramda'; +import { ITableColumn, ITableData, ITableRow } from './types/Table.types'; +import { FinancialTableStructure } from './common/FinancialTableStructure'; +import { tableClassNames } from './utils'; +import { Injectable } from '@nestjs/common'; +import { TemplateInjectable } from '../TemplateInjectable/TemplateInjectable.service'; +import { ChromiumlyTenancy } from '../ChromiumlyTenancy/ChromiumlyTenancy.service'; + +@Injectable() +export class TableSheetPdf { + constructor( + private readonly templateInjectable: TemplateInjectable, + private readonly chromiumlyTenancy: ChromiumlyTenancy, + ) {} + + /** + * Converts the table data into a PDF format. + * @param {ITableData} table - The table data to be converted. + * @param {string} sheetName - The name of the sheet. + * @param {string} sheetDate - The date of the sheet. + * @returns A promise that resolves with the PDF conversion result. + */ + public async convertToPdf( + table: ITableData, + sheetName: string, + sheetDate: string, + customCSS?: string, + ): Promise { + // Prepare columns and rows for PDF conversion + const columns = this.tablePdfColumns(table.columns); + const rows = this.tablePdfRows(table.rows); + + const landscape = columns.length > 4; + + // Generate HTML content from the template + const htmlContent = await this.templateInjectable.render( + 'modules/financial-sheet', + { + table: { rows, columns }, + sheetName, + sheetDate, + customCSS, + }, + ); + // Convert the HTML content to PDF + return this.chromiumlyTenancy.convertHtmlContent(htmlContent, { + margins: { top: 0, bottom: 0, left: 0, right: 0 }, + landscape, + }); + } + + /** + * Converts the table columns to pdf columns. + * @param {ITableColumn[]} columns + * @returns {ITableColumn[]} + */ + private tablePdfColumns = (columns: ITableColumn[]): ITableColumn[] => { + return columns; + }; + + /** + * Converts the table rows to pdf rows. + * @param {ITableRow[]} rows - + * @returns {ITableRow[]} + */ + private tablePdfRows = (rows: ITableRow[]): ITableRow[] => { + const curriedFlatNestedTree = R.curry( + FinancialTableStructure.flatNestedTree, + ); + const flatNestedTree = curriedFlatNestedTree(R.__, { + nestedPrefix: '', + }); + return R.compose(tableClassNames, flatNestedTree)(rows); + }; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/common/FinancialDatePeriods.ts b/packages/server-nest/src/modules/FinancialStatements/common/FinancialDatePeriods.ts new file mode 100644 index 000000000..c848e0f7a --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/common/FinancialDatePeriods.ts @@ -0,0 +1,110 @@ +import * as R from 'ramda'; +import { memoize } from 'lodash'; +import { + IAccountTransactionsGroupBy, + IFinancialDatePeriodsUnit, + IFinancialSheetTotalPeriod, + IFormatNumberSettings, +} from '../types/Report.types'; +import { dateRangeFromToCollection } from '@/utils/date-range-collection'; +import { FinancialDateRanges } from './FinancialDateRanges'; +import { Constructor } from '@/common/types/Constructor'; + +export const FinancialDatePeriods = (Base: T) => + class extends R.compose(FinancialDateRanges)(Base) { + /** + * Retrieves the date ranges from the given from date to the given to date. + * @param {Date} fromDate - + * @param {Date} toDate + * @param {string} unit + */ + public getDateRanges = memoize( + (fromDate: Date, toDate: Date, unit: string) => { + return dateRangeFromToCollection(fromDate, toDate, unit); + } + ); + + /** + * Retrieves the date period meta. + * @param {number} total - Total amount. + * @param {Date} fromDate - From date. + * @param {Date} toDate - To date. + * @return {ICashFlowDatePeriod} + */ + public getDatePeriodMeta = ( + total: number, + fromDate: Date, + toDate: Date, + overrideSettings?: IFormatNumberSettings + ): IFinancialSheetTotalPeriod => { + return { + fromDate: this.getDateMeta(fromDate), + toDate: this.getDateMeta(toDate), + total: this.getAmountMeta(total, overrideSettings), + }; + }; + + /** + * Retrieve the date period meta. + * @param {number} total - Total amount. + * @param {Date} fromDate - From date. + * @param {Date} toDate - To date. + * @return {ICashFlowDatePeriod} + */ + public getDatePeriodTotalMeta = ( + total: number, + fromDate: Date, + toDate: Date, + overrideSettings: IFormatNumberSettings = {} + ) => { + return this.getDatePeriodMeta(total, fromDate, toDate, { + money: true, + ...overrideSettings, + }); + }; + + /** + * Retrieve the date preioods of the given node and accumulated function. + * @param {IBalanceSheetAccountNode} node + * @param {(fromDate: Date, toDate: Date, index: number) => any} + * @return {} + */ + public getNodeDatePeriods = R.curry( + ( + fromDate: Date, + toDate: Date, + periodsUnit: string, + node: any, + callback: ( + node: any, + fromDate: Date, + toDate: Date, + index: number + ) => any + ) => { + const curriedCallback = R.curry(callback)(node); + // Retrieves memorized date ranges. + const dateRanges = this.getDateRanges(fromDate, toDate, periodsUnit); + return dateRanges.map((dateRange, index) => { + return curriedCallback(dateRange.fromDate, dateRange.toDate, index); + }); + } + ); + /** + * Retrieve the accounts transactions group type from display columns by. + * @param {IAccountTransactionsGroupBy} columnsBy + * @returns {IAccountTransactionsGroupBy} + */ + public getGroupByFromDisplayColumnsBy = ( + columnsBy: IFinancialDatePeriodsUnit + ): IAccountTransactionsGroupBy => { + const paris = { + week: IAccountTransactionsGroupBy.Day, + quarter: IAccountTransactionsGroupBy.Month, + year: IAccountTransactionsGroupBy.Year, + month: IAccountTransactionsGroupBy.Month, + day: IAccountTransactionsGroupBy.Day, + }; + return paris[columnsBy]; + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/common/FinancialDateRanges.ts b/packages/server-nest/src/modules/FinancialStatements/common/FinancialDateRanges.ts new file mode 100644 index 000000000..438337f17 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/common/FinancialDateRanges.ts @@ -0,0 +1,108 @@ +import moment from 'moment'; +import { IDateRange, IFinancialDatePeriodsUnit } from '../types/Report.types'; +import { Constructor } from '@/common/types/Constructor'; + +export const FinancialDateRanges = (Base: T) => + class extends Base { + /** + * Retrieve previous period (PP) date of the given date. + * @param {Date} fromDate - + * @param {Date} toDate - + * @param {IFinancialDatePeriodsUnit} unit - + * @returns {Date} + */ + public getPreviousPeriodDate = ( + date: Date, + value: number = 1, + unit: IFinancialDatePeriodsUnit = IFinancialDatePeriodsUnit.Day, + ): Date => { + return moment(date).subtract(value, unit).toDate(); + }; + + /** + * Retrieves the different + * @param {Date} fromDate + * @param {Date} toDate + * @returns + */ + public getPreviousPeriodDiff = (fromDate: Date, toDate: Date) => { + return moment(toDate).diff(fromDate, 'days') + 1; + }; + + /** + * Retrieves the periods period dates. + * @param {Date} fromDate - + * @param {Date} toDate - + */ + public getPreviousPeriodDateRange = ( + fromDate: Date, + toDate: Date, + unit: IFinancialDatePeriodsUnit, + amount: number = 1, + ): IDateRange => { + const PPToDate = this.getPreviousPeriodDate(toDate, amount, unit); + const PPFromDate = this.getPreviousPeriodDate(fromDate, amount, unit); + + return { toDate: PPToDate, fromDate: PPFromDate }; + }; + + /** + * Retrieves the previous period (PP) date range of total column. + * @param {Date} fromDate + * @param {Date} toDate + * @returns {IDateRange} + */ + public getPPTotalDateRange = (fromDate: Date, toDate: Date): IDateRange => { + const unit = this.getPreviousPeriodDiff(fromDate, toDate); + + return this.getPreviousPeriodDateRange( + fromDate, + toDate, + IFinancialDatePeriodsUnit.Day, + unit, + ); + }; + + /** + * Retrieves the previous period (PP) date range of date periods columns. + * @param {Date} fromDate - + * @param {Date} toDate - + * @param {IFinancialDatePeriodsUnit} + * @returns {IDateRange} + */ + public getPPDatePeriodDateRange = ( + fromDate: Date, + toDate: Date, + unit: IFinancialDatePeriodsUnit, + ): IDateRange => { + return this.getPreviousPeriodDateRange(fromDate, toDate, unit, 1); + }; + + // ------------------------ + // Previous Year (PY). + // ------------------------ + /** + * Retrieve the previous year of the given date. + * @params {Date} date + * @returns {Date} + */ + getPreviousYearDate = (date: Date) => { + return moment(date).subtract(1, 'years').toDate(); + }; + + /** + * Retrieves previous year date range. + * @param {Date} fromDate + * @param {Date} toDate + * @returns {IDateRange} + */ + public getPreviousYearDateRange = ( + fromDate: Date, + toDate: Date, + ): IDateRange => { + const PYFromDate = this.getPreviousYearDate(fromDate); + const PYToDate = this.getPreviousYearDate(toDate); + + return { fromDate: PYFromDate, toDate: PYToDate }; + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/common/FinancialEvaluateEquation.ts b/packages/server-nest/src/modules/FinancialStatements/common/FinancialEvaluateEquation.ts new file mode 100644 index 000000000..253f488c2 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/common/FinancialEvaluateEquation.ts @@ -0,0 +1,63 @@ +import * as mathjs from 'mathjs'; +import * as R from 'ramda'; +import { compose } from 'lodash/fp'; +import { omit, get, mapValues } from 'lodash'; +import { FinancialSheetStructure } from './FinancialSheetStructure'; +import { Constructor } from '@/common/types/Constructor'; + +export const FinancialEvaluateEquation = (Base: T) => + class extends compose(FinancialSheetStructure)(Base) { + /** + * Evauluate equaation string with the given scope table. + * @param {string} equation - + * @param {{ [key: string]: number }} scope - + * @return {number} + */ + public evaluateEquation = ( + equation: string, + scope: { [key: string | number]: number } + ): number => { + return mathjs.evaluate(equation, scope); + }; + + /** + * Transformes the given nodes nested array to object key/value by id. + * @param nodes + * @returns + */ + public transformNodesToMap = (nodes: any[]) => { + return this.mapAccNodesDeep( + nodes, + (node, key, parentValue, acc, context) => { + if (node.id) { + acc[`${node.id}`] = omit(node, ['children']); + } + return acc; + }, + {} + ); + }; + + /** + * + * @param nodesById + * @returns + */ + public mapNodesToTotal = R.curry( + (path: string, nodesById: { [key: number]: any }) => { + return mapValues(nodesById, (node) => get(node, path, 0)); + } + ); + + /** + * + */ + public getNodesTableForEvaluating = R.curry( + (path = 'total.amount', nodes) => { + return R.compose( + this.mapNodesToTotal(path), + this.transformNodesToMap + )(nodes); + } + ); + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/common/FinancialFilter.ts b/packages/server-nest/src/modules/FinancialStatements/common/FinancialFilter.ts new file mode 100644 index 000000000..169356283 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/common/FinancialFilter.ts @@ -0,0 +1,23 @@ + +import { Constructor } from '@/common/types/Constructor'; +import { isEmpty } from 'lodash'; + +export const FinancialFilter = (Base: T) => + class extends Base { + /** + * Detarmines whether the given node has children. + * @param {IBalanceSheetCommonNode} node + * @returns {boolean} + */ + public isNodeHasChildren = (node: IFinancialCommonNode): boolean => + !isEmpty(node.children); + + /** + * Detarmines whether the given node has no zero amount. + * @param {IBalanceSheetCommonNode} node + * @returns {boolean} + */ + public isNodeNoneZero = (node) =>{ + return node.total.amount !== 0; + } + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/common/FinancialHorizTotals.ts b/packages/server-nest/src/modules/FinancialStatements/common/FinancialHorizTotals.ts new file mode 100644 index 000000000..2202a893e --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/common/FinancialHorizTotals.ts @@ -0,0 +1,97 @@ +import * as R from 'ramda'; +import { get, isEmpty } from 'lodash'; +import { Constructor } from '@/common/types/Constructor'; + +export const FinancialHorizTotals = (Base: T) => + class extends Base { + /** + * + */ + public assocNodePercentage = R.curry( + (assocPath, parentTotal: number, node: any) => { + const percentage = this.getPercentageBasis( + parentTotal, + node.total.amount, + ); + return R.assoc( + assocPath, + this.getPercentageAmountMeta(percentage), + node, + ); + }, + ); + + /** + * + * @param {} parentNode - + * @param {} horTotalNode - + * @param {number} index - + */ + public assocPercentageHorizTotal = R.curry( + (assocPercentagePath: string, parentNode, horTotalNode, index) => { + const parentTotal = get( + parentNode, + `horizontalTotals[${index}].total.amount`, + 0, + ); + return this.assocNodePercentage( + assocPercentagePath, + parentTotal, + horTotalNode, + ); + }, + ); + + /** + * + * @param assocPercentagePath + * @param parentNode + * @param node + * @returns + */ + public assocPercentageHorizTotals = R.curry( + (assocPercentagePath: string, parentNode, node) => { + const assocColPerc = this.assocPercentageHorizTotal( + assocPercentagePath, + parentNode, + ); + return R.addIndex(R.map)(assocColPerc)(node.horizontalTotals); + }, + ); + + /** + * + */ + assocRowPercentageHorizTotal = R.curry( + (assocPercentagePath: string, node, horizTotalNode) => { + return this.assocNodePercentage( + assocPercentagePath, + node.total.amount, + horizTotalNode, + ); + }, + ); + + /** + * + */ + public assocHorizontalPercentageTotals = R.curry( + (assocPercentagePath: string, node) => { + const assocColPerc = this.assocRowPercentageHorizTotal( + assocPercentagePath, + node, + ); + + return R.map(assocColPerc)(node.horizontalTotals); + }, + ); + + /** + * + * @param node + * @returns + */ + public isNodeHasHorizTotals = (node) => { + return !isEmpty(node.horizontalTotals); + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/common/FinancialPreviousPeriod.ts b/packages/server-nest/src/modules/FinancialStatements/common/FinancialPreviousPeriod.ts new file mode 100644 index 000000000..ccad76ccf --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/common/FinancialPreviousPeriod.ts @@ -0,0 +1,127 @@ +import { sumBy } from 'lodash'; +import { + IFinancialDatePeriodsUnit, + IFinancialNodeWithPreviousPeriod, +} from '../types/Report.types'; +import * as R from 'ramda'; + +export const FinancialPreviousPeriod = (Base) => + class extends Base { + // --------------------------- + // # Common Node. + // --------------------------- + /** + * Assoc previous period percentage attribute to account node. + * @param {IProfitLossSheetAccountNode} accountNode + * @returns {IFinancialNodeWithPreviousPeriod} + */ + public assocPreviousPeriodPercentageNode = ( + accountNode: IProfitLossSheetAccountNode + ): IFinancialNodeWithPreviousPeriod => { + const percentage = this.getPercentageBasis( + accountNode.previousPeriod.amount, + accountNode.previousPeriodChange.amount + ); + return R.assoc( + 'previousPeriodPercentage', + this.getPercentageAmountMeta(percentage), + accountNode + ); + }; + + /** + * Assoc previous period total attribute to account node. + * @param {IProfitLossSheetAccountNode} accountNode + * @returns {IFinancialNodeWithPreviousPeriod} + */ + public assocPreviousPeriodChangeNode = ( + accountNode: IProfitLossSheetAccountNode + ): IFinancialNodeWithPreviousPeriod => { + const change = this.getAmountChange( + accountNode.total.amount, + accountNode.previousPeriod.amount + ); + return R.assoc( + 'previousPeriodChange', + this.getAmountMeta(change), + accountNode + ); + }; + + /** + * Assoc previous period percentage attribute to account node. + * + * % change = Change ÷ Original Number × 100. + * + * @param {IProfitLossSheetAccountNode} accountNode + * @returns {IFinancialNodeWithPreviousPeriod} + */ + public assocPreviousPeriodTotalPercentageNode = ( + accountNode: IProfitLossSheetAccountNode + ): IFinancialNodeWithPreviousPeriod => { + const percentage = this.getPercentageBasis( + accountNode.previousPeriod.amount, + accountNode.previousPeriodChange.amount + ); + return R.assoc( + 'previousPeriodPercentage', + this.getPercentageTotalAmountMeta(percentage), + accountNode + ); + }; + + /** + * Assoc previous period total attribute to account node. + * @param {IProfitLossSheetAccountNode} accountNode + * @returns {IFinancialNodeWithPreviousPeriod} + */ + public assocPreviousPeriodTotalChangeNode = ( + accountNode: any + ): IFinancialNodeWithPreviousPeriod => { + const change = this.getAmountChange( + accountNode.total.amount, + accountNode.previousPeriod.amount + ); + return R.assoc( + 'previousPeriodChange', + this.getTotalAmountMeta(change), + accountNode + ); + }; + + /** + * Assoc previous year from/to date to horizontal nodes. + * @param horizNode + * @returns {IFinancialNodeWithPreviousPeriod} + */ + public assocPreviousPeriodHorizNodeFromToDates = R.curry( + ( + periodUnit: IFinancialDatePeriodsUnit, + horizNode: any + ): IFinancialNodeWithPreviousPeriod => { + const { fromDate: PPFromDate, toDate: PPToDate } = + this.getPreviousPeriodDateRange( + horizNode.fromDate.date, + horizNode.toDate.date, + periodUnit + ); + return R.compose( + R.assoc('previousPeriodToDate', this.getDateMeta(PPToDate)), + R.assoc('previousPeriodFromDate', this.getDateMeta(PPFromDate)) + )(horizNode); + } + ); + + /** + * Retrieves PP total sumation of the given horiz index node. + * @param {number} index + * @param node + * @returns {number} + */ + public getPPHorizNodesTotalSumation = (index: number, node): number => { + return sumBy( + node.children, + `horizontalTotals[${index}].previousPeriod.amount` + ); + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/common/FinancialPreviousYear.ts b/packages/server-nest/src/modules/FinancialStatements/common/FinancialPreviousYear.ts new file mode 100644 index 000000000..4453112da --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/common/FinancialPreviousYear.ts @@ -0,0 +1,118 @@ +import * as R from 'ramda'; +import { sumBy } from 'lodash' +import { + IFinancialCommonHorizDatePeriodNode, + IFinancialCommonNode, + IFinancialNodeWithPreviousYear, +} from '../types/Report.types'; + +export const FinancialPreviousYear = (Base) => + class extends Base { + // --------------------------- + // # Common Node + // --------------------------- + /** + * Assoc previous year change attribute to account node. + * @param {IProfitLossSheetAccountNode} accountNode + * @returns {IProfitLossSheetAccountNode} + */ + public assocPreviousYearChangetNode = ( + node: IFinancialCommonNode & IFinancialNodeWithPreviousYear + ): IFinancialNodeWithPreviousYear => { + const change = this.getAmountChange( + node.total.amount, + node.previousYear.amount + ); + return R.assoc('previousYearChange', this.getAmountMeta(change), node); + }; + + /** + * Assoc previous year percentage attribute to account node. + * + * % increase = Increase ÷ Original Number × 100. + * + * @param {IProfitLossSheetAccountNode} accountNode + * @returns {IProfitLossSheetAccountNode} + */ + public assocPreviousYearPercentageNode = ( + node: IFinancialCommonNode & IFinancialNodeWithPreviousYear + ): IFinancialNodeWithPreviousYear => { + const percentage = this.getPercentageBasis( + node.previousYear.amount, + node.previousYearChange.amount + ); + return R.assoc( + 'previousYearPercentage', + this.getPercentageAmountMeta(percentage), + node + ); + }; + + /** + * Assoc previous year change attribute to account node. + * @param {IProfitLossSheetAccountNode} accountNode + * @returns {IProfitLossSheetAccountNode} + */ + public assocPreviousYearTotalChangeNode = ( + node: IFinancialCommonNode & IFinancialNodeWithPreviousYear + ): IFinancialNodeWithPreviousYear => { + const change = this.getAmountChange( + node.total.amount, + node.previousYear.amount + ); + return R.assoc( + 'previousYearChange', + this.getTotalAmountMeta(change), + node + ); + }; + + /** + * Assoc previous year percentage attribute to account node. + * @param {IProfitLossSheetAccountNode} accountNode + * @returns {IProfitLossSheetAccountNode} + */ + public assocPreviousYearTotalPercentageNode = ( + node: IFinancialCommonNode & IFinancialNodeWithPreviousYear + ): IFinancialNodeWithPreviousYear => { + const percentage = this.getPercentageBasis( + node.previousYear.amount, + node.previousYearChange.amount + ); + return R.assoc( + 'previousYearPercentage', + this.getPercentageTotalAmountMeta(percentage), + node + ); + }; + + /** + * Assoc previous year from/to date to horizontal nodes. + * @param horizNode + * @returns + */ + public assocPreviousYearHorizNodeFromToDates = ( + horizNode: IFinancialCommonHorizDatePeriodNode + ) => { + const PYFromDate = this.getPreviousYearDate(horizNode.fromDate.date); + const PYToDate = this.getPreviousYearDate(horizNode.toDate.date); + + return R.compose( + R.assoc('previousYearToDate', this.getDateMeta(PYToDate)), + R.assoc('previousYearFromDate', this.getDateMeta(PYFromDate)) + )(horizNode); + }; + + /** + * Retrieves PP total sumation of the given horiz index node. + * @param {number} index + * @param {} node + * @returns {number} + */ + public getPYHorizNodesTotalSumation = (index: number, node): number => { + return sumBy( + node.children, + `horizontalTotals[${index}].previousYear.amount` + ) + } + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/common/FinancialReportService.ts b/packages/server-nest/src/modules/FinancialStatements/common/FinancialReportService.ts new file mode 100644 index 000000000..ad05715ff --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/common/FinancialReportService.ts @@ -0,0 +1,8 @@ +export default class FinancialReportService { + transformOrganizationMeta(tenant) { + return { + organizationName: tenant.metadata?.name, + baseCurrency: tenant.metadata?.baseCurrency, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/common/FinancialSchema.ts b/packages/server-nest/src/modules/FinancialStatements/common/FinancialSchema.ts new file mode 100644 index 000000000..f134b1083 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/common/FinancialSchema.ts @@ -0,0 +1,25 @@ +import * as R from 'ramda'; +import { FinancialSheetStructure } from './FinancialSheetStructure'; +import { Constructor } from '@/common/types/Constructor'; + +export const FinancialSchema = (Base: T) => + class extends R.compose(FinancialSheetStructure)(Base) { + /** + * + * @returns + */ + getSchema() { + return []; + } + + /** + * + * @param {string|number} id + * @returns + */ + publicgetSchemaNodeById = (id: string | number) => { + const schema = this.getSchema(); + + return this.findNodeDeep(schema, (node) => node.id === id); + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/common/FinancialSheet.ts b/packages/server-nest/src/modules/FinancialStatements/common/FinancialSheet.ts new file mode 100644 index 000000000..22f1e98b9 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/common/FinancialSheet.ts @@ -0,0 +1,177 @@ +import moment from 'moment'; +import { + ICashFlowStatementTotal, + IFormatNumberSettings, + INumberFormatQuery, +} from '../types/Report.types'; +import { formatNumber } from '@/utils/format-number'; + +export default class FinancialSheet { + readonly numberFormat: INumberFormatQuery = { + precision: 2, + divideOn1000: false, + showZero: false, + formatMoney: 'total', + negativeFormat: 'mines', + }; + readonly baseCurrency: string; + + /** + * Transformes the number format query to settings + */ + protected transfromFormatQueryToSettings(): IFormatNumberSettings { + const { numberFormat } = this; + + return { + precision: numberFormat.precision, + divideOn1000: numberFormat.divideOn1000, + excerptZero: !numberFormat.showZero, + negativeFormat: numberFormat.negativeFormat, + money: numberFormat.formatMoney === 'always', + currencyCode: this.baseCurrency, + }; + } + + /** + * Formating amount based on the given report query. + * @param {number} number - + * @param {IFormatNumberSettings} overrideSettings - + * @return {string} + */ + protected formatNumber( + number, + overrideSettings: IFormatNumberSettings = {} + ): string { + const settings = { + ...this.transfromFormatQueryToSettings(), + ...overrideSettings, + }; + return formatNumber(number, settings); + } + + /** + * Formatting full amount with different format settings. + * @param {number} amount - + * @param {IFormatNumberSettings} settings - + */ + protected formatTotalNumber = ( + amount: number, + settings: IFormatNumberSettings = {} + ): string => { + const { numberFormat } = this; + + return this.formatNumber(amount, { + money: numberFormat.formatMoney === 'none' ? false : true, + excerptZero: false, + ...settings, + }); + }; + + /** + * Formates the amount to the percentage string. + * @param {number} amount + * @returns {string} + */ + protected formatPercentage = ( + amount: number, + overrideSettings: IFormatNumberSettings = {} + ): string => { + const percentage = amount * 100; + const settings = { + excerptZero: true, + ...overrideSettings, + symbol: '%', + money: false, + }; + return formatNumber(percentage, settings); + }; + + /** + * Format the given total percentage. + * @param {number} amount - + * @param {IFormatNumberSettings} settings - + */ + protected formatTotalPercentage = ( + amount: number, + settings: IFormatNumberSettings = {} + ): string => { + return this.formatPercentage(amount, { + ...settings, + excerptZero: false, + }); + }; + + /** + * Retrieve the amount meta object. + * @param {number} amount + * @returns {ICashFlowStatementTotal} + */ + protected getAmountMeta( + amount: number, + overrideSettings?: IFormatNumberSettings + ): ICashFlowStatementTotal { + return { + amount, + formattedAmount: this.formatNumber(amount, overrideSettings), + currencyCode: this.baseCurrency, + }; + } + + /** + * Retrieve the total amount meta object. + * @param {number} amount + * @returns {ICashFlowStatementTotal} + */ + protected getTotalAmountMeta( + amount: number, + title?: string + ): ICashFlowStatementTotal { + return { + ...(title ? { title } : {}), + amount, + formattedAmount: this.formatTotalNumber(amount), + currencyCode: this.baseCurrency, + }; + } + + /** + * Retrieve the date meta. + * @param {Date} date + * @param {string} format + * @returns + */ + protected getDateMeta(date: Date, format = 'YYYY-MM-DD') { + return { + formattedDate: moment(date).format(format), + date: moment(date).toDate(), + }; + } + + getPercentageBasis = (base, amount) => { + return base ? amount / base : 0; + }; + + getAmountChange = (base, amount) => { + return base - amount; + }; + + protected getPercentageAmountMeta = (amount) => { + const formattedAmount = this.formatPercentage(amount); + + return { + amount, + formattedAmount, + }; + }; + + /** + * Re + * @param {number} amount + * @returns + */ + protected getPercentageTotalAmountMeta = (amount: number) => { + const formattedAmount = this.formatTotalPercentage(amount); + + return { amount, formattedAmount }; + }; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/common/FinancialSheetMeta.ts b/packages/server-nest/src/modules/FinancialStatements/common/FinancialSheetMeta.ts new file mode 100644 index 000000000..f90dc7568 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/common/FinancialSheetMeta.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common'; +import { IFinancialSheetCommonMeta } from '../types/Report.types'; +import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; + +@Injectable() +export class FinancialSheetMeta { + constructor( + private readonly inventoryService: InventoryService, + private readonly tenancyContext: TenancyContext, + ) {} + + /** + * Retrieves the common meta data of the financial sheet. + * @returns {Promise} + */ + async meta(): Promise { + const tenantMetadata = await this.tenancyContext.getTenantMetadata(); + + const organizationName = tenantMetadata.name; + const baseCurrency = tenantMetadata.baseCurrency; + const dateFormat = tenantMetadata.dateFormat; + + const isCostComputeRunning = + this.inventoryService.isItemsCostComputeRunning(tenantId); + + return { + organizationName, + baseCurrency, + dateFormat, + isCostComputeRunning, + sheetName: '', + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/common/FinancialSheetStructure.ts b/packages/server-nest/src/modules/FinancialStatements/common/FinancialSheetStructure.ts new file mode 100644 index 000000000..fdeb7852b --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/common/FinancialSheetStructure.ts @@ -0,0 +1,108 @@ +import * as R from 'ramda'; +import { set, sumBy } from 'lodash'; +import { + mapValuesDeepReverse, + mapValuesDeep, + mapValues, + condense, + filterDeep, + reduceDeep, + findValueDeep, + filterNodesDeep, +} from 'utils/deepdash'; +import { Constructor } from '@/common/types/Constructor'; + +export const FinancialSheetStructure = (Base: T) => + class extends Base { + /** + * + * @param nodes + * @param callback + * @returns + */ + public mapNodesDeepReverse = (nodes, callback) => { + return mapValuesDeepReverse(nodes, callback, { + childrenPath: 'children', + pathFormat: 'array', + }); + }; + + /** + * + * @param nodes + * @param callback + * @returns + */ + public mapNodesDeep = (nodes, callback) => { + return mapValuesDeep(nodes, callback, { + childrenPath: 'children', + pathFormat: 'array', + }); + }; + + public mapNodes = (nodes, callback) => { + return mapValues(nodes, callback, { + childrenPath: 'children', + pathFormat: 'array', + }); + }; + + public filterNodesDeep2 = R.curry((predicate, nodes) => { + return filterNodesDeep(predicate, nodes); + }); + + /** + * + * @param + */ + public filterNodesDeep = (nodes, callback) => { + return filterDeep(nodes, callback, { + childrenPath: 'children', + pathFormat: 'array', + }); + }; + + public findNodeDeep = (nodes, callback) => { + return findValueDeep(nodes, callback, { + childrenPath: 'children', + pathFormat: 'array', + }); + }; + + public mapAccNodesDeep = (nodes, callback) => { + return reduceDeep( + nodes, + (acc, value, key, parentValue, context) => { + set( + acc, + context.path, + callback(value, key, parentValue, acc, context) + ); + return acc; + }, + [], + { + childrenPath: 'children', + pathFormat: 'array', + } + ); + }; + + /** + * + */ + public reduceNodesDeep = (nodes, iteratee, accumulator) => { + return reduceDeep(nodes, iteratee, accumulator, { + childrenPath: 'children', + pathFormat: 'array', + }); + }; + + public getTotalOfChildrenNodes = (node) => { + return this.getTotalOfNodes(node.children); + }; + + public getTotalOfNodes = (nodes) => { + return sumBy(nodes, 'total.amount'); + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/common/FinancialTable.ts b/packages/server-nest/src/modules/FinancialStatements/common/FinancialTable.ts new file mode 100644 index 000000000..72f029183 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/common/FinancialTable.ts @@ -0,0 +1,47 @@ +import * as R from 'ramda'; +import { isEmpty, clone, cloneDeep, omit } from 'lodash'; +import { increment } from '@/utils/increment'; +import { ITableRow, ITableColumn } from '../types/Table.types'; +import { IROW_TYPE } from './BalanceSheet/constants'; + +export const FinancialTable = (Base) => + class extends Base { + /** + * Table columns cell indexing. + * @param {ITableColumn[]} columns + * @returns {ITableColumn[]} + */ + public tableColumnsCellIndexing = ( + columns: ITableColumn[], + ): ITableColumn[] => { + const cellIndex = increment(-1); + + return this.mapNodesDeep(columns, (column) => { + return isEmpty(column.children) + ? R.assoc('cellIndex', cellIndex(), column) + : column; + }); + }; + + addTotalRow = (node: ITableRow) => { + const clonedNode = clone(node); + + if (clonedNode.children) { + const cells = cloneDeep(node.cells); + cells[0].value = this.i18n.__('financial_sheet.total_row', { + value: cells[0].value, + }); + + clonedNode.children.push({ + ...omit(clonedNode, 'children'), + cells, + rowTypes: [IROW_TYPE.TOTAL], + }); + } + return clonedNode; + }; + + public addTotalRows = (nodes: ITableRow[]) => { + return this.mapNodesDeep(nodes, this.addTotalRow); + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/common/FinancialTablePreviousPeriod.ts b/packages/server-nest/src/modules/FinancialStatements/common/FinancialTablePreviousPeriod.ts new file mode 100644 index 000000000..d2a6200e4 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/common/FinancialTablePreviousPeriod.ts @@ -0,0 +1,130 @@ +import moment from 'moment'; +import { ITableColumn, IDateRange, ITableColumnAccessor } from '../types/Table.types'; + +export const FinancialTablePreviousPeriod = (Base) => + class extends Base { + getTotalPreviousPeriod = () => { + return this.query.PPToDate; + }; + // ---------------------------- + // # Columns + // ---------------------------- + /** + * Retrive previous period total column. + * @param {IDateRange} dateRange - + * @returns {ITableColumn} + */ + public getPreviousPeriodTotalColumn = ( + dateRange?: IDateRange + ): ITableColumn => { + const PPDate = dateRange + ? dateRange.toDate + : this.getTotalPreviousPeriod(); + const PPFormatted = moment(PPDate).format('YYYY-MM-DD'); + + return { + key: 'previous_period', + label: this.i18n.__(`financial_sheet.previoud_period_date`, { + date: PPFormatted, + }), + }; + }; + + /** + * Retrieve previous period change column. + * @returns {ITableColumn} + */ + public getPreviousPeriodChangeColumn = (): ITableColumn => { + return { + key: 'previous_period_change', + label: this.i18n.__('fianncial_sheet.previous_period_change'), + }; + }; + + /** + * Retrieve previous period percentage column. + * @returns {ITableColumn} + */ + public getPreviousPeriodPercentageColumn = (): ITableColumn => { + return { + key: 'previous_period_percentage', + label: this.i18n.__('financial_sheet.previous_period_percentage'), + }; + }; + + /** + * Retrieves previous period total accessor. + * @returns {ITableColumnAccessor} + */ + public getPreviousPeriodTotalAccessor = (): ITableColumnAccessor => { + return { + key: 'previous_period', + accessor: 'previousPeriod.formattedAmount', + }; + }; + + /** + * Retrieves previous period change accessor. + * @returns + */ + public getPreviousPeriodChangeAccessor = () => { + return { + key: 'previous_period_change', + accessor: 'previousPeriodChange.formattedAmount', + }; + }; + + /** + * Retrieves previous period percentage accessor. + * @returns {ITableColumnAccessor} + */ + public getPreviousPeriodPercentageAccessor = + (): ITableColumnAccessor => { + return { + key: 'previous_period_percentage', + accessor: 'previousPeriodPercentage.formattedAmount', + }; + }; + + /** + * Retrieves previous period total horizontal column accessor. + * @param {number} index + * @returns {ITableColumnAccessor} + */ + public getPreviousPeriodTotalHorizAccessor = ( + index: number + ): ITableColumnAccessor => { + return { + key: 'previous_period', + accessor: `horizontalTotals[${index}].previousPeriod.formattedAmount`, + }; + }; + + /** + * Retrieves previous period change horizontal column accessor. + * @param {number} index + * @returns {ITableColumnAccessor} + */ + public getPreviousPeriodChangeHorizAccessor = ( + index: number + ): ITableColumnAccessor => { + return { + key: 'previous_period_change', + accessor: `horizontalTotals[${index}].previousPeriodChange.formattedAmount`, + }; + }; + + /** + * Retrieves pervious period percentage horizontal column accessor. + * @param {number} index + * @returns {ITableColumnAccessor} + */ + public getPreviousPeriodPercentageHorizAccessor = ( + index: number + ): ITableColumnAccessor => { + return { + key: 'previous_period_percentage', + accessor: `horizontalTotals[${index}].previousPeriodPercentage.formattedAmount`, + }; + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/common/FinancialTablePreviousYear.ts b/packages/server-nest/src/modules/FinancialStatements/common/FinancialTablePreviousYear.ts new file mode 100644 index 000000000..59766ac4b --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/common/FinancialTablePreviousYear.ts @@ -0,0 +1,131 @@ +import moment from 'moment'; +import { ITableColumn, ITableColumnAccessor } from '../types/Table.types'; +import { IDateRange } from '../types/Report.types'; + +export const FinancialTablePreviousYear = (Base) => + class extends Base { + getTotalPreviousYear = () => { + return this.query.PYToDate; + }; + // ------------------------------------ + // # Columns. + // ------------------------------------ + /** + * Retrive previous year total column. + * @param {DateRange} previousYear - + * @returns {ITableColumn} + */ + public getPreviousYearTotalColumn = ( + dateRange?: IDateRange, + ): ITableColumn => { + const PYDate = dateRange ? dateRange.toDate : this.getTotalPreviousYear(); + const PYFormatted = moment(PYDate).format('YYYY-MM-DD'); + + return { + key: 'previous_year', + label: this.i18n.__('financial_sheet.previous_year_date', { + date: PYFormatted, + }), + }; + }; + + /** + * Retrieve previous year change column. + * @returns {ITableColumn} + */ + public getPreviousYearChangeColumn = (): ITableColumn => { + return { + key: 'previous_year_change', + label: this.i18n.__('financial_sheet.previous_year_change'), + }; + }; + + /** + * Retrieve previous year percentage column. + * @returns {ITableColumn} + */ + public getPreviousYearPercentageColumn = (): ITableColumn => { + return { + key: 'previous_year_percentage', + label: this.i18n.__('financial_sheet.previous_year_percentage'), + }; + }; + + // ------------------------------------ + // # Accessors. + // ------------------------------------ + /** + * Retrieves previous year total column accessor. + * @returns {ITableColumnAccessor} + */ + public getPreviousYearTotalAccessor = (): ITableColumnAccessor => { + return { + key: 'previous_year', + accessor: 'previousYear.formattedAmount', + }; + }; + + /** + * Retrieves previous year change column accessor. + * @returns {ITableColumnAccessor} + */ + public getPreviousYearChangeAccessor = (): ITableColumnAccessor => { + return { + key: 'previous_year_change', + accessor: 'previousYearChange.formattedAmount', + }; + }; + + /** + * Retrieves previous year percentage column accessor. + * @returns {ITableColumnAccessor} + */ + public getPreviousYearPercentageAccessor = (): ITableColumnAccessor => { + return { + key: 'previous_year_percentage', + accessor: 'previousYearPercentage.formattedAmount', + }; + }; + + /** + * Retrieves previous year total horizontal column accessor. + * @param {number} index + * @returns {ITableColumnAccessor} + */ + public getPreviousYearTotalHorizAccessor = ( + index: number, + ): ITableColumnAccessor => { + return { + key: 'previous_year', + accessor: `horizontalTotals[${index}].previousYear.formattedAmount`, + }; + }; + + /** + * Retrieves previous previous year change horizontal column accessor. + * @param {number} index + * @returns {ITableColumnAccessor} + */ + public getPreviousYearChangeHorizAccessor = ( + index: number, + ): ITableColumnAccessor => { + return { + key: 'previous_year_change', + accessor: `horizontalTotals[${index}].previousYearChange.formattedAmount`, + }; + }; + + /** + * Retrieves previous year percentage horizontal column accessor. + * @param {number} index + * @returns {ITableColumnAccessor} + */ + public getPreviousYearPercentageHorizAccessor = ( + index: number, + ): ITableColumnAccessor => { + return { + key: 'previous_year_percentage', + accessor: `horizontalTotals[${index}].previousYearPercentage.formattedAmount`, + }; + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/common/FinancialTableStructure.ts b/packages/server-nest/src/modules/FinancialStatements/common/FinancialTableStructure.ts new file mode 100644 index 000000000..cde90e955 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/common/FinancialTableStructure.ts @@ -0,0 +1,48 @@ +import { ITableRow } from '../types/Table.types'; +import { flatNestedTree } from '@/utils/deepdash'; +import { repeat } from 'lodash'; + +interface FlatNestTreeOpts { + nestedPrefix?: string; + nestedPrefixIndex?: number; +} + +export class FinancialTableStructure { + /** + * Converts the given table object with nested rows in flat rows. + * @param {ITableRow[]} + * @param {FlatNestTreeOpts} + * @returns {ITableRow[]} + */ + public static flatNestedTree = ( + obj: ITableRow[], + options?: FlatNestTreeOpts + ): ITableRow[] => { + const parsedOptions = { + nestedPrefix: ' ', + nestedPrefixIndex: 0, + ...options, + }; + const { nestedPrefixIndex, nestedPrefix } = parsedOptions; + + return flatNestedTree( + obj, + (item, key, context) => { + const cells = item.cells.map((cell, index) => { + return { + ...cell, + value: + (context.depth > 1 && nestedPrefixIndex === index + ? repeat(nestedPrefix, context.depth) + : '') + cell.value, + }; + }); + return { + ...item, + cells, + }; + }, + parsedOptions + ); + }; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/common/TableSheet.ts b/packages/server-nest/src/modules/FinancialStatements/common/TableSheet.ts new file mode 100644 index 000000000..4de3b0800 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/common/TableSheet.ts @@ -0,0 +1,137 @@ +import * as xlsx from 'xlsx'; +import { ITableData } from '../types/Table.types'; +import { FinancialTableStructure } from './FinancialTableStructure'; + +interface ITableSheet { + convertToXLSX(): xlsx.WorkBook; + convertToCSV(): string; + convertToBuffer(workbook: xlsx.WorkBook, fileType: string): Buffer; +} + +export class TableSheet implements ITableSheet { + private table: ITableData; + + constructor(table: ITableData) { + this.table = table; + } + + /** + * Retrieves the columns labels. + * @returns {string[]} + */ + private get columns() { + return this.table.columns.map((col) => col.label); + } + + /** + * Retrieves the columns accessors. + * @returns {string[]} + */ + private get columnsAccessors() { + return this.table.columns.map((col, index) => { + return `${index}`; + }); + } + + /** + * Retrieves the rows data cellIndex/Value. + * @returns {Record} + */ + private get rows() { + const computedRows = FinancialTableStructure.flatNestedTree( + this.table.rows, + ); + return computedRows.map((row) => { + const entries = row.cells.map((cell, index) => { + return [`${index}`, cell.value]; + }); + return Object.fromEntries(entries); + }); + } + + /** + * Converts the table to a CSV string. + * @returns {string} + */ + public convertToCSV(): string { + // Define custom headers + const headers = this.columns; + + // Convert data to worksheet with headers + const worksheet = xlsx.utils.json_to_sheet(this.rows, { + header: this.columnsAccessors, + }); + // Add custom headers to the worksheet + xlsx.utils.sheet_add_aoa(worksheet, [headers], { origin: 'A1' }); + + // Convert worksheet to CSV format + const csvOutput = xlsx.utils.sheet_to_csv(worksheet); + + return csvOutput; + } + + /** + * Convert the array of objects to an XLSX file with styled headers + * @returns {xlsx.WorkBook} + */ + public convertToXLSX(): xlsx.WorkBook { + // Create a new workbook and a worksheet + const workbook = xlsx.utils.book_new(); + const worksheet = xlsx.utils.json_to_sheet(this.rows, { + header: this.columnsAccessors, + }); + // Add custom headers to the worksheet + xlsx.utils.sheet_add_aoa(worksheet, [this.columns], { + origin: 'A1', + }); + // Adjust column width. + worksheet['!cols'] = this.computeXlsxColumnsWidths(this.rows); + + // Append the worksheet to the workbook + xlsx.utils.book_append_sheet(workbook, worksheet, 'Sheet1'); + + return workbook; + } + + /** + * Converts the given workbook to buffer of the given file type + * @param {xlsx.WorkBook} workbook + * @param {string} fileType + * @returns {Promise} + */ + public convertToBuffer( + workbook: xlsx.WorkBook, + fileType: 'xlsx' | 'csv', + ): Buffer { + return xlsx.write(workbook, { + type: 'buffer', + bookType: fileType, + cellStyles: true, + }); + } + + /** + * Adjusts and computes the columns width. + * @param {} rows + * @returns {{wch: number}[]} + */ + private computeXlsxColumnsWidths = (rows): { wch: number }[] => { + const cols = [{ wch: 60 }]; + + this.columns.map((column) => { + cols.push({ wch: column.length }); + }); + rows.forEach((row) => { + const entries = Object.entries(row); + + entries.forEach(([key, value]) => { + if (cols[key]) { + cols[key].wch = Math.max(cols[key].wch, String(value).length); + } else { + cols[key] = { wch: String(value).length }; + } + }); + }); + return cols; + }; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheet.controller.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheet.controller.ts new file mode 100644 index 000000000..92c00db46 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheet.controller.ts @@ -0,0 +1,66 @@ +import { Response } from 'express'; +import { castArray } from 'lodash'; +import { Headers, Injectable, Query, Res } from '@nestjs/common'; +import { BalanceSheetInjectable } from './BalanceSheetInjectable'; +import { IBalanceSheetQuery } from './BalanceSheet.types'; +import { AcceptType } from '@/constants/accept-type'; +import { BalanceSheetApplication } from './BalanceSheetApplication'; + +@Injectable() +export class BalanceSheetStatementController { + constructor(private readonly balanceSheetApp: BalanceSheetApplication) {} + + /** + * Retrieve the balance sheet. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + public async balanceSheet( + @Query() query: IBalanceSheetQuery, + @Res() res: Response, + @Headers('accept') acceptHeader: string, + ) { + const filter = { + ...query, + accountsIds: castArray(query.accountsIds), + }; + // Retrieves the json table format. + if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) { + const table = await this.balanceSheetApp.table(tenantId, filter); + + return res.status(200).send(table); + // Retrieves the csv format. + } else if (acceptHeader.includes(AcceptType.ApplicationCsv)) { + const buffer = await this.balanceSheetApp.csv(tenantId, filter); + + res.setHeader('Content-Disposition', 'attachment; filename=output.csv'); + res.setHeader('Content-Type', 'text/csv'); + + return res.send(buffer); + // Retrieves the xlsx format. + } else if (acceptHeader.includes(AcceptType.ApplicationXlsx)) { + const buffer = await this.balanceSheetApp.xlsx(tenantId, filter); + + res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx'); + res.setHeader( + 'Content-Type', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ); + return res.send(buffer); + // Retrieves the pdf format. + } else if (acceptHeader.includes(AcceptType.ApplicationPdf)) { + const pdfContent = await this.balanceSheetApp.pdf(tenantId, filter); + + res.set({ + 'Content-Type': 'application/pdf', + 'Content-Length': pdfContent.length, + }); + res.send(pdfContent); + } else { + const sheet = await this.balanceSheetApp.sheet(tenantId, filter); + + return res.status(200).send(sheet); + } + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheet.module.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheet.module.ts new file mode 100644 index 000000000..39c02289e --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheet.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { BalanceSheetInjectable } from './BalanceSheetInjectable'; +import { BalanceSheetApplication } from './BalanceSheetApplication'; + +@Module({ + providers: [BalanceSheetInjectable, BalanceSheetApplication], + exports: [BalanceSheetInjectable], +}) +export class BalanceSheetModule {} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheet.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheet.ts new file mode 100644 index 000000000..6738ca5c3 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheet.ts @@ -0,0 +1,108 @@ +import * as R from 'ramda'; +import { + IBalanceSheetQuery, + IBalanceSheetSchemaNode, + IBalanceSheetDataNode, +} from './BalanceSheet.types'; +import { BalanceSheetSchema } from './BalanceSheetSchema'; +import { BalanceSheetPercentage } from './BalanceSheetPercentage'; +import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod'; +import { BalanceSheetComparsionPreviousYear } from './BalanceSheetComparsionPreviousYear'; +import { BalanceSheetDatePeriods } from './BalanceSheetDatePeriods'; +import { BalanceSheetBase } from './BalanceSheetBase'; +import { FinancialSheetStructure } from '../../common/FinancialSheetStructure'; +import BalanceSheetRepository from './BalanceSheetRepository'; +import { BalanceSheetQuery } from './BalanceSheetQuery'; +import { BalanceSheetFiltering } from './BalanceSheetFiltering'; +import { BalanceSheetNetIncome } from './BalanceSheetNetIncome'; +import { BalanceSheetAggregators } from './BalanceSheetAggregators'; +import { BalanceSheetAccounts } from './BalanceSheetAccounts'; +import FinancialSheet from '../../common/FinancialSheet'; +import { INumberFormatQuery } from '../../types/Report.types'; + +export class BalanceSheet extends R.compose( + BalanceSheetAggregators, + BalanceSheetAccounts, + BalanceSheetNetIncome, + BalanceSheetFiltering, + BalanceSheetDatePeriods, + BalanceSheetComparsionPreviousPeriod, + BalanceSheetComparsionPreviousYear, + BalanceSheetPercentage, + BalanceSheetSchema, + BalanceSheetBase, + FinancialSheetStructure, +)(FinancialSheet) { + /** + * Balance sheet query. + * @param {BalanceSheetQuery} + */ + readonly query: BalanceSheetQuery; + + /** + * Balance sheet number format query. + * @param {INumberFormatQuery} + */ + readonly numberFormat: INumberFormatQuery; + + /** + * Base currency of the organization. + * @param {string} + */ + readonly baseCurrency: string; + + /** + * Localization. + */ + readonly i18n: any; + + /** + * Constructor method. + * @param {IBalanceSheetQuery} query - + * @param {IAccount[]} accounts - + * @param {string} baseCurrency - + */ + constructor( + query: IBalanceSheetQuery, + repository: BalanceSheetRepository, + baseCurrency: string, + i18n, + ) { + super(); + + this.query = new BalanceSheetQuery(query); + this.repository = repository; + this.baseCurrency = baseCurrency; + this.numberFormat = this.query.query.numberFormat; + this.i18n = i18n; + } + + /** + * Parses report schema nodes. + * @param {IBalanceSheetSchemaNode[]} schema + * @returns {IBalanceSheetDataNode[]} + */ + public parseSchemaNodes = ( + schema: IBalanceSheetSchemaNode[], + ): IBalanceSheetDataNode[] => { + return R.compose( + this.aggregatesSchemaParser, + this.netIncomeSchemaParser, + this.accountsSchemaParser, + )(schema) as IBalanceSheetDataNode[]; + }; + + /** + * Retrieve the report statement data. + * @returns {IBalanceSheetDataNode[]} + */ + public reportData = () => { + const balanceSheetSchema = this.getSchema(); + + return R.compose( + this.reportFilterPlugin, + this.reportPercentageCompose, + this.parseSchemaNodes, + )(balanceSheetSchema); + }; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheet.types.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheet.types.ts new file mode 100644 index 000000000..96cff4985 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheet.types.ts @@ -0,0 +1,223 @@ +import { + IFinancialSheetBranchesQuery, + IFinancialSheetCommonMeta, + IFormatNumberSettings, + INumberFormatQuery, +} from '../../types/Report.types'; +import { IFinancialTable } from '../../types/Table.types'; + +// Balance sheet schema nodes types. +export enum BALANCE_SHEET_SCHEMA_NODE_TYPE { + AGGREGATE = 'AGGREGATE', + ACCOUNTS = 'ACCOUNTS', + ACCOUNT = 'ACCOUNT', + NET_INCOME = 'NET_INCOME', +} + +export enum BALANCE_SHEET_NODE_TYPE { + AGGREGATE = 'AGGREGATE', + ACCOUNTS = 'ACCOUNTS', + ACCOUNT = 'ACCOUNT', +} + +// Balance sheet schema nodes ids. +export enum BALANCE_SHEET_SCHEMA_NODE_ID { + ASSETS = 'ASSETS', + CURRENT_ASSETS = 'CURRENT_ASSETS', + CASH_EQUIVALENTS = 'CASH_EQUIVALENTS', + ACCOUNTS_RECEIVABLE = 'ACCOUNTS_RECEIVABLE', + NON_CURRENT_ASSET = 'NON_CURRENT_ASSET', + FIXED_ASSET = 'FIXED_ASSET', + OTHER_CURRENT_ASSET = 'OTHER_CURRENT_ASSET', + INVENTORY = 'INVENTORY', + LIABILITY_EQUITY = 'LIABILITY_EQUITY', + LIABILITY = 'LIABILITY', + CURRENT_LIABILITY = 'CURRENT_LIABILITY', + LOGN_TERM_LIABILITY = 'LOGN_TERM_LIABILITY', + NON_CURRENT_LIABILITY = 'NON_CURRENT_LIABILITY', + EQUITY = 'EQUITY', + NET_INCOME = 'NET_INCOME', +} + +// Balance sheet query. +export interface IBalanceSheetQuery extends IFinancialSheetBranchesQuery { + displayColumnsType: 'total' | 'date_periods'; + displayColumnsBy: string; + fromDate: Date; + toDate: Date; + numberFormat: INumberFormatQuery; + noneTransactions: boolean; + noneZero: boolean; + basis: 'cash' | 'accrual'; + accountIds: number[]; + + percentageOfColumn: boolean; + percentageOfRow: boolean; + + previousPeriod: boolean; + previousPeriodAmountChange: boolean; + previousPeriodPercentageChange: boolean; + + previousYear: boolean; + previousYearAmountChange: boolean; + previousYearPercentageChange: boolean; +} + +// Balance sheet meta. +export interface IBalanceSheetMeta extends IFinancialSheetCommonMeta { + formattedAsDate: string; + formattedDateRange: string; +} + +export interface IBalanceSheetFormatNumberSettings + extends IFormatNumberSettings { + type: string; +} + +// Balance sheet service. +export interface IBalanceSheetStatementService { + balanceSheet( + tenantId: number, + query: IBalanceSheetQuery, + ): Promise; +} + +export type IBalanceSheetStatementData = IBalanceSheetDataNode[]; + +export interface IBalanceSheetDOO { + query: IBalanceSheetQuery; + data: IBalanceSheetStatementData; + meta: IBalanceSheetMeta; +} + +export interface IBalanceSheetCommonNode { + total: IBalanceSheetTotal; + horizontalTotals?: IBalanceSheetTotal[]; + + percentageRow?: IBalanceSheetPercentageAmount; + percentageColumn?: IBalanceSheetPercentageAmount; + + previousPeriod?: IBalanceSheetTotal; + previousPeriodChange?: IBalanceSheetTotal; + previousPeriodPercentage?: IBalanceSheetPercentageAmount; + + previousYear?: IBalanceSheetTotal; + previousYearChange?: IBalanceSheetTotal; + previousYearPercentage?: IBalanceSheetPercentageAmount; +} + +export interface IBalanceSheetAggregateNode extends IBalanceSheetCommonNode { + id: string; + name: string; + nodeType: BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE; + children?: IBalanceSheetDataNode[]; +} + +export interface IBalanceSheetTotal { + amount: number; + formattedAmount: string; + currencyCode: string; + date?: string | Date; +} + +export interface IBalanceSheetAccountsNode extends IBalanceSheetCommonNode { + id: number | string; + name: string; + nodeType: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS; + children: IBalanceSheetAccountNode[]; +} + +export interface IBalanceSheetAccountNode extends IBalanceSheetCommonNode { + id: number; + index: number; + name: string; + code: string; + parentAccountId?: number; + nodeType: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNT; + children?: IBalanceSheetAccountNode[]; +} + +export interface IBalanceSheetNetIncomeNode extends IBalanceSheetCommonNode { + id: number; + name: string; + nodeType: BALANCE_SHEET_SCHEMA_NODE_TYPE.NET_INCOME; +} + +export type IBalanceSheetDataNode = + | IBalanceSheetAggregateNode + | IBalanceSheetAccountNode + | IBalanceSheetAccountsNode + | IBalanceSheetNetIncomeNode; + +export interface IBalanceSheetPercentageAmount { + amount: number; + formattedAmount: string; +} + +export interface IBalanceSheetSchemaAggregateNode { + name: string; + id: string; + type: BALANCE_SHEET_SCHEMA_NODE_TYPE; + children: IBalanceSheetSchemaNode[]; + alwaysShow: boolean; +} + +export interface IBalanceSheetSchemaAccountNode { + name: string; + id: string; + type: BALANCE_SHEET_SCHEMA_NODE_TYPE; + accountsTypes: string[]; +} + +export interface IBalanceSheetSchemaNetIncomeNode { + id: string; + name: string; + type: BALANCE_SHEET_SCHEMA_NODE_TYPE; +} + +export type IBalanceSheetSchemaNode = + | IBalanceSheetSchemaAccountNode + | IBalanceSheetSchemaAggregateNode + | IBalanceSheetSchemaNetIncomeNode; + +export interface IBalanceSheetDatePeriods { + assocAccountNodeDatePeriods(node): any; + initDateRangeCollection(): void; +} + +export interface IBalanceSheetComparsions { + assocPreviousYearAccountNode(node); + hasPreviousPeriod(): boolean; + hasPreviousYear(): boolean; + assocPreviousPeriodAccountNode(node); +} + +export interface IBalanceSheetTotalPeriod extends IFinancialSheetTotalPeriod { + percentageRow?: IBalanceSheetPercentageAmount; + percentageColumn?: IBalanceSheetPercentageAmount; +} + +export interface IFinancialSheetTotalPeriod { + fromDate: any; + toDate: any; + total: any; +} + +export enum IFinancialDatePeriodsUnit { + Day = 'day', + Month = 'month', + Year = 'year', +} + +export enum IAccountTransactionsGroupBy { + Quarter = 'quarter', + Year = 'year', + Day = 'day', + Month = 'month', + Week = 'week', +} + +export interface IBalanceSheetTable extends IFinancialTable { + meta: IBalanceSheetMeta; + query: IBalanceSheetQuery; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetAccounts.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetAccounts.ts new file mode 100644 index 000000000..687931212 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetAccounts.ts @@ -0,0 +1,205 @@ +import * as R from 'ramda'; +import { defaultTo, toArray } from 'lodash'; +import { FinancialSheetStructure } from '../../common/FinancialSheetStructure'; +import { + BALANCE_SHEET_SCHEMA_NODE_TYPE, + IBalanceSheetAccountNode, + IBalanceSheetAccountsNode, + IBalanceSheetDataNode, + IBalanceSheetSchemaAccountNode, + IBalanceSheetSchemaNode, +} from './BalanceSheet.types'; +import { BalanceSheetNetIncome } from './BalanceSheetNetIncome'; +import { BalanceSheetFiltering } from './BalanceSheetFiltering'; +import { BalanceSheetDatePeriods } from './BalanceSheetDatePeriods'; +import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod'; +import { BalanceSheetComparsionPreviousYear } from './BalanceSheetComparsionPreviousYear'; +import { BalanceSheetPercentage } from './BalanceSheetPercentage'; +import { BalanceSheetSchema } from './BalanceSheetSchema'; +import { BalanceSheetBase } from './BalanceSheetBase'; +import { BalanceSheetQuery } from './BalanceSheetQuery'; +import { flatToNestedArray } from '@/utils'; +import BalanceSheetRepository from './BalanceSheetRepository'; +import { Constructor } from '@/common/types/Constructor'; +import { INumberFormatQuery } from '../../types/Report.types'; +import { Account } from '@/modules/Accounts/models/Account.model'; + +export const BalanceSheetAccounts = (Base: T) => + class extends R.compose( + BalanceSheetNetIncome, + BalanceSheetFiltering, + BalanceSheetDatePeriods, + BalanceSheetComparsionPreviousPeriod, + BalanceSheetComparsionPreviousYear, + BalanceSheetPercentage, + BalanceSheetSchema, + BalanceSheetBase, + FinancialSheetStructure + )(Base) { + /** + * Balance sheet query. + * @param {BalanceSheetQuery} + */ + readonly query: BalanceSheetQuery; + + /** + * Balance sheet number format query. + * @param {INumberFormatQuery} + */ + readonly numberFormat: INumberFormatQuery; + + /** + * Base currency of the organization. + * @param {string} + */ + readonly baseCurrency: string; + + /** + * Localization. + */ + readonly i18n: any; + + /** + * Balance sheet repository. + */ + readonly repository: BalanceSheetRepository; + + /** + * Retrieve the accounts node of accounts types. + * @param {string} accountsTypes + * @returns {IAccount[]} + */ + private getAccountsByAccountTypes = ( + accountsTypes: string[] + ): Account[] => { + const mapAccountsByTypes = R.map((accountType) => + defaultTo(this.repository.accountsByType.get(accountType), []) + ); + return R.compose(R.flatten, mapAccountsByTypes)(accountsTypes); + }; + + /** + * Mappes the account model to report account node. + * @param {Account} account + * @returns {IBalanceSheetAccountNode} + */ + private reportSchemaAccountNodeMapper = ( + account: Account + ): IBalanceSheetAccountNode => { + const childrenAccountsIds = this.repository.accountsGraph.dependenciesOf( + account.id + ); + const accountIds = R.uniq(R.append(account.id, childrenAccountsIds)); + const total = this.repository.totalAccountsLedger + .whereAccountsIds(accountIds) + .getClosingBalance(); + + return { + id: account.id, + index: account.index, + name: account.name, + code: account.code, + total: this.getAmountMeta(total), + nodeType: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNT, + }; + }; + + /** + * Mappes the given account model to the balance sheet account node. + * @param {IAccount} account + * @returns {IBalanceSheetAccountNode} + */ + private reportSchemaAccountNodeComposer = ( + account: Account + ): IBalanceSheetAccountNode => { + return R.compose( + R.when( + this.query.isPreviousYearActive, + this.previousYearAccountNodeComposer + ), + R.when( + this.query.isPreviousPeriodActive, + this.previousPeriodAccountNodeComposer + ), + R.when( + this.query.isDatePeriodsColumnsType, + this.assocAccountNodeDatePeriods + ), + this.reportSchemaAccountNodeMapper + )(account); + }; + + // ----------------------------- + // - Accounts Node Praser + // ----------------------------- + /** + * Retrieve the report accounts node by the given accounts types. + * @param {string[]} accountsTypes + * @returns {IBalanceSheetAccountNode[]} + */ + private getAccountsNodesByAccountTypes = ( + accountsTypes: string[] + ): IBalanceSheetAccountNode[] => { + // Retrieves accounts from the given defined node account types. + const accounts = this.getAccountsByAccountTypes(accountsTypes); + + // Converts the flatten accounts to tree. + const accountsTree = flatToNestedArray(accounts, { + id: 'id', + parentId: 'parentAccountId', + }); + // Maps over the accounts tree. + return this.mapNodesDeep( + accountsTree, + this.reportSchemaAccountNodeComposer + ); + }; + + /** + * Mappes the accounts schema node type. + * @param {IBalanceSheetSchemaNode} node - Schema node. + * @returns {IBalanceSheetAccountNode} + */ + private reportSchemaAccountsNodeMapper = ( + node: IBalanceSheetSchemaAccountNode + ): IBalanceSheetAccountsNode => { + const accounts = this.getAccountsNodesByAccountTypes(node.accountsTypes); + const children = toArray(node?.children); + + return { + id: node.id, + name: this.i18n.__(node.name), + nodeType: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS, + type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS, + children: [...accounts, ...children], + total: this.getTotalAmountMeta(0), + }; + }; + + /** + * Mappes the given report schema node. + * @param {IBalanceSheetSchemaNode | IBalanceSheetDataNode} node - Schema node. + * @return {IBalanceSheetSchemaNode | IBalanceSheetDataNode} + */ + private reportAccountSchemaParser = ( + node: IBalanceSheetSchemaNode | IBalanceSheetDataNode + ): IBalanceSheetSchemaNode | IBalanceSheetDataNode => { + return R.compose( + R.when( + this.isSchemaNodeType(BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS), + this.reportSchemaAccountsNodeMapper + ) + )(node); + }; + + /** + * Parses the report accounts schema nodes. + * @param {IBalanceSheetSchemaNode[]} nodes - + * @return {IBalanceSheetStructureSection[]} + */ + public accountsSchemaParser = ( + nodes: (IBalanceSheetSchemaNode | IBalanceSheetDataNode)[] + ): (IBalanceSheetDataNode | IBalanceSheetSchemaNode)[] => { + return this.mapNodesDeepReverse(nodes, this.reportAccountSchemaParser); + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetAggregators.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetAggregators.ts new file mode 100644 index 000000000..53218b739 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetAggregators.ts @@ -0,0 +1,141 @@ +import * as R from 'ramda'; +import { + BALANCE_SHEET_SCHEMA_NODE_TYPE, + IBalanceSheetAggregateNode, + IBalanceSheetDataNode, + IBalanceSheetSchemaAggregateNode, + IBalanceSheetSchemaNode, + INumberFormatQuery, +} from '@/interfaces'; +import { BalanceSheetDatePeriods } from './BalanceSheetDatePeriods'; +import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod'; +import { BalanceSheetComparsionPreviousYear } from './BalanceSheetComparsionPreviousYear'; +import { BalanceSheetPercentage } from './BalanceSheetPercentage'; +import { BalanceSheetSchema } from './BalanceSheetSchema'; +import { BalanceSheetBase } from './BalanceSheetBase'; +import { BalanceSheetQuery } from './BalanceSheetQuery'; +import { FinancialSheetStructure } from '../../common/FinancialSheetStructure'; +import { Constructor } from '@/common/types/Constructor'; + +export const BalanceSheetAggregators = (Base: T) => + class extends R.compose( + BalanceSheetDatePeriods, + BalanceSheetComparsionPreviousPeriod, + BalanceSheetComparsionPreviousYear, + BalanceSheetPercentage, + BalanceSheetSchema, + BalanceSheetBase, + FinancialSheetStructure + )(Base) { + /** + * Balance sheet query. + * @param {BalanceSheetQuery} + */ + readonly query: BalanceSheetQuery; + + /** + * Balance sheet number format query. + * @param {INumberFormatQuery} + */ + readonly numberFormat: INumberFormatQuery; + + /** + * Base currency of the organization. + * @param {string} + */ + readonly baseCurrency: string; + + /** + * Localization. + */ + readonly i18n: any; + + /** + * Sets total amount that calculated from node children. + * @param {IBalanceSheetSection} node + * @returns {IBalanceSheetDataNode} + */ + public aggregateNodeTotalMapper = ( + node: IBalanceSheetDataNode + ): IBalanceSheetDataNode => { + return R.compose( + R.when( + this.query.isPreviousYearActive, + this.previousYearAggregateNodeComposer + ), + R.when( + this.query.isPreviousPeriodActive, + this.previousPeriodAggregateNodeComposer + ), + R.when( + this.query.isDatePeriodsColumnsType, + this.assocAggregateNodeDatePeriods + ) + )(node); + }; + + /** + * Mappes the aggregate schema node type. + * @param {IBalanceSheetSchemaAggregateNode} node - Schema node. + * @return {IBalanceSheetAggregateNode} + */ + public reportSchemaAggregateNodeMapper = ( + node: IBalanceSheetSchemaAggregateNode + ): IBalanceSheetAggregateNode => { + const total = this.getTotalOfNodes(node.children); + + return { + name: this.i18n.__(node.name), + id: node.id, + nodeType: BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE, + type: BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE, + total: this.getTotalAmountMeta(total), + children: node.children, + }; + }; + + /** + * Compose shema aggregate node of balance sheet schema. + * @param {IBalanceSheetSchemaAggregateNode} node + * @returns {IBalanceSheetSchemaAggregateNode} + */ + public schemaAggregateNodeCompose = ( + node: IBalanceSheetSchemaAggregateNode + ) => { + return R.compose( + this.aggregateNodeTotalMapper, + this.reportSchemaAggregateNodeMapper + )(node); + }; + + /** + * Mappes the given report schema node. + * @param {IBalanceSheetSchemaNode} node - Schema node. + * @return {IBalanceSheetDataNode} + */ + public reportAggregateSchemaParser = ( + node: IBalanceSheetSchemaNode + ): IBalanceSheetDataNode => { + return R.compose( + R.when( + this.isSchemaNodeType(BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE), + this.schemaAggregateNodeCompose + ), + R.when( + this.isSchemaNodeType(BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS), + this.schemaAggregateNodeCompose + ) + )(node); + }; + + /** + * Mappes the report schema nodes. + * @param {IBalanceSheetSchemaNode[]} nodes - + * @return {IBalanceSheetStructureSection[]} + */ + public aggregatesSchemaParser = ( + nodes: (IBalanceSheetSchemaNode | IBalanceSheetDataNode)[] + ): (IBalanceSheetDataNode | IBalanceSheetSchemaNode)[] => { + return this.mapNodesDeepReverse(nodes, this.reportAggregateSchemaParser); + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetApplication.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetApplication.ts new file mode 100644 index 000000000..8813d05d5 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetApplication.ts @@ -0,0 +1,60 @@ +import { Injectable } from '@nestjs/common'; +import { IBalanceSheetQuery } from '@/interfaces'; +import { BalanceSheetExportInjectable } from './BalanceSheetExportInjectable'; +import { BalanceSheetTableInjectable } from './BalanceSheetTableInjectable'; +import { BalanceSheetStatementService } from './BalanceSheetInjectable'; + +@Injectable() +export class BalanceSheetApplication { + + constructor( + private readonly balanceSheetExportService: BalanceSheetExportInjectable, + private readonly balanceSheetTableService: BalanceSheetTableInjectable, + private readonly balanceSheetService: BalanceSheetStatementService, + ) {} + + /** + * Retrieves the balnace sheet in json format. + * @param {IBalanceSheetQuery} query + * @returns {Promise} + */ + public sheet(query: IBalanceSheetQuery) { + return this.balanceSheetService.balanceSheet(query); + } + + /** + * Retrieves the balance sheet in table format. + * @param {IBalanceSheetQuery} query + * @returns {Promise} + */ + public table(query: IBalanceSheetQuery) { + return this.balanceSheetTableService.table(query); + } + + /** + * Retrieves the balance sheet in XLSX format. + * @param {IBalanceSheetQuery} query + * @returns {Promise} + */ + public xlsx(query: IBalanceSheetQuery) { + return this.balanceSheetExportService.xlsx(query); + } + + /** + * Retrieves the balance sheet in CSV format. + * @param {IBalanceSheetQuery} query + * @returns {Promise} + */ + public csv(query: IBalanceSheetQuery): Promise { + return this.balanceSheetExportService.csv(query); + } + + /** + * Retrieves the balance sheet in pdf format. + * @param {IBalanceSheetQuery} query + * @returns {Promise} + */ + public pdf(query: IBalanceSheetQuery) { + return this.balanceSheetExportService.pdf(query); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetBase.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetBase.ts new file mode 100644 index 000000000..c89f5ee7c --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetBase.ts @@ -0,0 +1,33 @@ +import * as R from 'ramda'; +import { IBalanceSheetDataNode, IBalanceSheetSchemaNode } from '@/interfaces'; +import { Constructor } from '@/common/types/Constructor'; + +export const BalanceSheetBase = (Base: T) => + class extends Base { + /** + * Detarmines the node type of the given schema node. + * @param {IBalanceSheetStructureSection} node - + * @param {string} type - + * @return {boolean} + */ + public isSchemaNodeType = R.curry( + (type: string, node: IBalanceSheetSchemaNode): boolean => { + return node.type === type; + } + ); + + isNodeType = R.curry( + (type: string, node: IBalanceSheetDataNode): boolean => { + return node.nodeType === type; + } + ); + + /** + * Detarmines the given display columns by type. + * @param {string} displayColumnsBy + * @returns {boolean} + */ + public isDisplayColumnsBy = (displayColumnsBy: string): boolean => { + return this.query.displayColumnsType === displayColumnsBy; + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetComparsionPreviousPeriod.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetComparsionPreviousPeriod.ts new file mode 100644 index 000000000..918b11359 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetComparsionPreviousPeriod.ts @@ -0,0 +1,271 @@ +import * as R from 'ramda'; +import { sumBy } from 'lodash'; +import { + IBalanceSheetAccountNode, + IBalanceSheetDataNode, + IBalanceSheetAggregateNode, + IBalanceSheetTotal, + IBalanceSheetCommonNode, + IBalanceSheetComparsions, +} from '@/interfaces'; +import { FinancialPreviousPeriod } from '../../common/FinancialPreviousPeriod'; +import { FinancialHorizTotals } from '../../common/FinancialHorizTotals'; +import { Constructor } from '@/common/types/Constructor'; + +export const BalanceSheetComparsionPreviousPeriod = ( + Base: T, +) => + class + extends R.compose(FinancialPreviousPeriod, FinancialHorizTotals)(Base) + implements IBalanceSheetComparsions + { + // ------------------------------ + // # Account + // ------------------------------ + /** + * Associates the previous period to account node. + * @param {IBalanceSheetDataNode} node + * @returns {IBalanceSheetDataNode} + */ + public assocPreviousPeriodAccountNode = ( + node: IBalanceSheetDataNode, + ): IBalanceSheetDataNode => { + const total = this.repository.PPTotalAccountsLedger.whereAccountId( + node.id, + ).getClosingBalance(); + + return R.assoc('previousPeriod', this.getAmountMeta(total), node); + }; + + /** + * Previous period account node composer. + * @param {IBalanceSheetAccountNode} node + * @returns {IBalanceSheetAccountNode} + */ + public previousPeriodAccountNodeComposer = ( + node: IBalanceSheetAccountNode, + ): IBalanceSheetAccountNode => { + return R.compose( + R.when( + this.isNodeHasHorizTotals, + this.assocPreivousPeriodAccountHorizNodeComposer, + ), + R.when( + this.query.isPreviousPeriodPercentageActive, + this.assocPreviousPeriodPercentageNode, + ), + R.when( + this.query.isPreviousPeriodChangeActive, + this.assocPreviousPeriodChangeNode, + ), + R.when( + this.query.isPreviousPeriodActive, + this.assocPreviousPeriodAccountNode, + ), + )(node); + }; + + // ------------------------------ + // # Aggregate + // ------------------------------ + /** + * Assoc previous period total to aggregate node. + * @param {IBalanceSheetAggregateNode} node + * @returns {IBalanceSheetAggregateNode} + */ + public assocPreviousPeriodAggregateNode = ( + node: IBalanceSheetAggregateNode, + ): IBalanceSheetAggregateNode => { + const total = sumBy(node.children, 'previousYear.amount'); + + return R.assoc('previousPeriod', this.getTotalAmountMeta(total), node); + }; + + /** + * Previous period aggregate node composer. + * @param {IBalanceSheetAccountNode} node + * @returns {IBalanceSheetAccountNode} + */ + public previousPeriodAggregateNodeComposer = ( + node: IBalanceSheetAccountNode, + ): IBalanceSheetAccountNode => { + return R.compose( + R.when( + this.isNodeHasHorizTotals, + this.assocPreviousPeriodAggregateHorizNode, + ), + R.when( + this.query.isPreviousPeriodPercentageActive, + this.assocPreviousPeriodTotalPercentageNode, + ), + R.when( + this.query.isPreviousPeriodChangeActive, + this.assocPreviousPeriodTotalChangeNode, + ), + R.when( + this.query.isPreviousPeriodActive, + this.assocPreviousPeriodAggregateNode, + ), + )(node); + }; + + // ------------------------------ + // # Horizontal Nodes - Account. + // ------------------------------ + /** + * Retrieve the given account total in the given period. + * @param {number} accountId - Account id. + * @param {Date} fromDate - From date. + * @param {Date} toDate - To date. + * @returns {number} + */ + private getAccountPPDatePeriodTotal = R.curry( + (accountId: number, fromDate: Date, toDate: Date): number => { + const PPPeriodsTotal = + this.repository.PPPeriodsAccountsLedger.whereAccountId(accountId) + .whereToDate(toDate) + .getClosingBalance(); + + const PPPeriodsOpeningTotal = + this.repository.PPPeriodsOpeningAccountLedger.whereAccountId( + accountId, + ).getClosingBalance(); + + return PPPeriodsOpeningTotal + PPPeriodsTotal; + }, + ); + + /** + * Assoc preivous period to account horizontal total node. + * @param {IBalanceSheetAccountNode} node + * @returns {} + */ + private assocPreviousPeriodAccountHorizTotal = R.curry( + (node: IBalanceSheetAccountNode, totalNode) => { + const total = this.getAccountPPDatePeriodTotal( + node.id, + totalNode.previousPeriodFromDate.date, + totalNode.previousPeriodToDate.date, + ); + return R.assoc('previousPeriod', this.getAmountMeta(total), totalNode); + }, + ); + + /** + * Previous year account horizontal node composer. + * @param {IBalanceSheetAccountNode} node - + * @param {IBalanceSheetTotal} + * @returns {IBalanceSheetTotal} + */ + private previousPeriodAccountHorizNodeCompose = R.curry( + ( + node: IBalanceSheetAccountNode, + horizontalTotalNode: IBalanceSheetTotal, + ): IBalanceSheetTotal => { + return R.compose( + R.when( + this.query.isPreviousPeriodPercentageActive, + this.assocPreviousPeriodPercentageNode, + ), + R.when( + this.query.isPreviousPeriodChangeActive, + this.assocPreviousPeriodChangeNode, + ), + R.when( + this.query.isPreviousPeriodActive, + this.assocPreviousPeriodAccountHorizTotal(node), + ), + R.when( + this.query.isPreviousPeriodActive, + this.assocPreviousPeriodHorizNodeFromToDates( + this.query.displayColumnsBy, + ), + ), + )(horizontalTotalNode); + }, + ); + + /** + * + * @param {IBalanceSheetAccountNode} node + * @returns + */ + private assocPreivousPeriodAccountHorizNodeComposer = ( + node: IBalanceSheetAccountNode, + ) => { + const horizontalTotals = R.map( + this.previousPeriodAccountHorizNodeCompose(node), + node.horizontalTotals, + ); + return R.assoc('horizontalTotals', horizontalTotals, node); + }; + + // ------------------------------ + // # Horizontal Nodes - Aggregate + // ------------------------------ + /** + * Assoc previous year total to horizontal node. + * @param node + * @returns + */ + private assocPreviousPeriodAggregateHorizTotalNode = R.curry( + (node, index: number, totalNode) => { + const total = this.getPPHorizNodesTotalSumation(index, node); + + return R.assoc( + 'previousPeriod', + this.getTotalAmountMeta(total), + totalNode, + ); + }, + ); + + /** + * Compose previous period to aggregate horizontal nodes. + * @param {IBalanceSheetTotal} node + * @returns {IBalanceSheetTotal} + */ + private previousPeriodAggregateHorizNodeComposer = R.curry( + ( + node: IBalanceSheetCommonNode, + horiontalTotalNode: IBalanceSheetTotal, + index: number, + ): IBalanceSheetTotal => { + return R.compose( + R.when( + this.query.isPreviousPeriodPercentageActive, + this.assocPreviousPeriodTotalPercentageNode, + ), + R.when( + this.query.isPreviousPeriodChangeActive, + this.assocPreviousPeriodTotalChangeNode, + ), + R.when( + this.query.isPreviousPeriodActive, + this.assocPreviousPeriodAggregateHorizTotalNode(node, index), + ), + R.when( + this.query.isPreviousPeriodActive, + this.assocPreviousPeriodHorizNodeFromToDates( + this.query.displayColumnsBy, + ), + ), + )(horiontalTotalNode); + }, + ); + + /** + * Assoc + * @param {IBalanceSheetCommonNode} node + * @returns {IBalanceSheetCommonNode} + */ + private assocPreviousPeriodAggregateHorizNode = ( + node: IBalanceSheetCommonNode, + ) => { + const horizontalTotals = R.addIndex(R.map)( + this.previousPeriodAggregateHorizNodeComposer(node), + node.horizontalTotals, + ); + return R.assoc('horizontalTotals', horizontalTotals, node); + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetComparsionPreviousYear.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetComparsionPreviousYear.ts new file mode 100644 index 000000000..d5d048039 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetComparsionPreviousYear.ts @@ -0,0 +1,269 @@ +import * as R from 'ramda'; +import { sumBy, isEmpty } from 'lodash'; +import { + IBalanceSheetAccountNode, + IBalanceSheetCommonNode, + IBalanceSheetDataNode, + IBalanceSheetTotal, + ITableColumn, +} from '@/interfaces'; +import { FinancialPreviousYear } from '../FinancialPreviousYear'; + +export const BalanceSheetComparsionPreviousYear = (Base: any) => + class + extends R.compose(FinancialPreviousYear)(Base) + implements IBalanceSheetComparsions + { + // ------------------------------ + // # Account + // ------------------------------ + /** + * Associates the previous year to account node. + * @param {IBalanceSheetDataNode} node + * @returns {IBalanceSheetDataNode} + */ + protected assocPreviousYearAccountNode = ( + node: IBalanceSheetDataNode + ): IBalanceSheetDataNode => { + const closingBalance = + this.repository.PYTotalAccountsLedger.whereAccountId( + node.id + ).getClosingBalance(); + + return R.assoc('previousYear', this.getAmountMeta(closingBalance), node); + }; + + /** + * Assoc previous year attributes to account node. + * @param {IBalanceSheetAccountNode} node + * @returns {IBalanceSheetAccountNode} + */ + protected previousYearAccountNodeComposer = ( + node: IBalanceSheetAccountNode + ): IBalanceSheetAccountNode => { + return R.compose( + R.when( + this.isNodeHasHorizontalTotals, + this.assocPreviousYearAccountHorizNodeComposer + ), + R.when( + this.query.isPreviousYearPercentageActive, + this.assocPreviousYearPercentageNode + ), + R.when( + this.query.isPreviousYearChangeActive, + this.assocPreviousYearChangetNode + ), + this.assocPreviousYearAccountNode + )(node); + }; + + // ------------------------------ + // # Aggregate + // ------------------------------ + /** + * Assoc previous year on aggregate node. + * @param {IBalanceSheetAccountNode} node + * @returns {IBalanceSheetAccountNode} + */ + protected assocPreviousYearAggregateNode = ( + node: IBalanceSheetAccountNode + ): IBalanceSheetAccountNode => { + const total = sumBy(node.children, 'previousYear.amount'); + + return R.assoc('previousYear', this.getTotalAmountMeta(total), node); + }; + + /** + * Assoc previous year attributes to aggregate node. + * @param {IBalanceSheetAccountNode} node + * @returns {IBalanceSheetAccountNode} + */ + protected previousYearAggregateNodeComposer = ( + node: IBalanceSheetAccountNode + ): IBalanceSheetAccountNode => { + return R.compose( + R.when( + this.query.isPreviousYearPercentageActive, + this.assocPreviousYearTotalPercentageNode + ), + R.when( + this.query.isPreviousYearChangeActive, + this.assocPreviousYearTotalChangeNode + ), + R.when( + this.isNodeHasHorizontalTotals, + this.assocPreviousYearAggregateHorizNode + ), + this.assocPreviousYearAggregateNode + )(node); + }; + + // ------------------------------ + // # Horizontal Nodes - Aggregate + // ------------------------------ + /** + * Assoc previous year total to horizontal node. + * @param node + * @returns + */ + private assocPreviousYearAggregateHorizTotalNode = R.curry( + (node, index, totalNode) => { + const total = this.getPYHorizNodesTotalSumation(index, node); + + return R.assoc( + 'previousYear', + this.getTotalAmountMeta(total), + totalNode + ); + } + ); + + /** + * Compose previous year to aggregate horizontal nodes. + * @param {IBalanceSheetTotal} node + * @returns {IBalanceSheetTotal} + */ + private previousYearAggregateHorizNodeComposer = R.curry( + ( + node: IBalanceSheetCommonNode, + horiontalTotalNode: IBalanceSheetTotal, + index: number + ): IBalanceSheetTotal => { + return R.compose( + R.when( + this.query.isPreviousYearPercentageActive, + this.assocPreviousYearTotalPercentageNode + ), + R.when( + this.query.isPreviousYearChangeActive, + this.assocPreviousYearTotalChangeNode + ), + R.when( + this.query.isPreviousYearActive, + this.assocPreviousYearAggregateHorizTotalNode(node, index) + ), + R.when( + this.query.isPreviousYearActive, + this.assocPreviousYearHorizNodeFromToDates + ) + )(horiontalTotalNode); + } + ); + + /** + * Assoc + * @param {IBalanceSheetCommonNode} node + * @returns {IBalanceSheetCommonNode} + */ + public assocPreviousYearAggregateHorizNode = ( + node: IBalanceSheetCommonNode + ): IBalanceSheetCommonNode => { + const horizontalTotals = R.addIndex(R.map)( + this.previousYearAggregateHorizNodeComposer(node), + node.horizontalTotals + ) as IBalanceSheetTotal[]; + + return R.assoc('horizontalTotals', horizontalTotals, node); + }; + + // ------------------------------ + // # Horizontal Nodes - Account. + // ------------------------------ + /** + * Retrieve the given account total in the given period. + * @param {number} accountId - Account id. + * @param {Date} fromDate - From date. + * @param {Date} toDate - To date. + * @returns {number} + */ + private getAccountPYDatePeriodTotal = R.curry( + (accountId: number, fromDate: Date, toDate: Date): number => { + const PYPeriodsTotal = + this.repository.PYPeriodsAccountsLedger.whereAccountId(accountId) + .whereToDate(toDate) + .getClosingBalance(); + + const PYPeriodsOpeningTotal = + this.repository.PYPeriodsOpeningAccountLedger.whereAccountId( + accountId + ).getClosingBalance(); + + return PYPeriodsOpeningTotal + PYPeriodsTotal; + } + ); + + /** + * Assoc preivous year to account horizontal total node. + * @param {IBalanceSheetAccountNode} node + * @returns {} + */ + private assocPreviousYearAccountHorizTotal = R.curry( + (node: IBalanceSheetAccountNode, totalNode) => { + const total = this.getAccountPYDatePeriodTotal( + node.id, + totalNode.previousYearFromDate.date, + totalNode.previousYearToDate.date + ); + return R.assoc('previousYear', this.getAmountMeta(total), totalNode); + } + ); + + /** + * Previous year account horizontal node composer. + * @param {IBalanceSheetAccountNode} node - + * @param {IBalanceSheetTotal} + * @returns {IBalanceSheetTotal} + */ + private previousYearAccountHorizNodeCompose = R.curry( + ( + node: IBalanceSheetAccountNode, + horizontalTotalNode: IBalanceSheetTotal + ): IBalanceSheetTotal => { + return R.compose( + R.when( + this.query.isPreviousYearPercentageActive, + this.assocPreviousYearPercentageNode + ), + R.when( + this.query.isPreviousYearChangeActive, + this.assocPreviousYearChangetNode + ), + R.when( + this.query.isPreviousYearActive, + this.assocPreviousYearAccountHorizTotal(node) + ), + R.when( + this.query.isPreviousYearActive, + this.assocPreviousYearHorizNodeFromToDates + ) + )(horizontalTotalNode); + } + ); + + /** + * Assoc previous year horizontal nodes to account node. + * @param {IBalanceSheetAccountNode} node + * @returns {IBalanceSheetAccountNode} + */ + private assocPreviousYearAccountHorizNodeComposer = ( + node: IBalanceSheetAccountNode + ) => { + const horizontalTotals = R.map( + this.previousYearAccountHorizNodeCompose(node), + node.horizontalTotals + ); + return R.assoc('horizontalTotals', horizontalTotals, node); + }; + + // ------------------------------ + // # Horizontal Nodes - Aggregate. + // ------------------------------ + /** + * Detarmines whether the given node has horizontal totals. + * @param {IBalanceSheetCommonNode} node + * @returns {boolean} + */ + public isNodeHasHorizontalTotals = (node: IBalanceSheetCommonNode) => + !isEmpty(node.horizontalTotals); + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetDatePeriods.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetDatePeriods.ts new file mode 100644 index 000000000..9ac6af1ce --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetDatePeriods.ts @@ -0,0 +1,211 @@ +import * as R from 'ramda'; +import { sumBy } from 'lodash'; +import { + IBalanceSheetQuery, + IFormatNumberSettings, + IBalanceSheetDatePeriods, + IBalanceSheetAccountNode, + IBalanceSheetTotalPeriod, + IDateRange, + IBalanceSheetCommonNode, +} from '@/interfaces'; +import FinancialSheet from '../FinancialSheet'; +import { FinancialDatePeriods } from '../FinancialDatePeriods'; + +/** + * Balance sheet date periods. + */ +export const BalanceSheetDatePeriods = (Base: FinancialSheet) => + class + extends R.compose(FinancialDatePeriods)(Base) + implements IBalanceSheetDatePeriods + { + /** + * @param {IBalanceSheetQuery} + */ + readonly query: IBalanceSheetQuery; + + /** + * Retrieves the date periods based on the report query. + * @returns {IDateRange[]} + */ + get datePeriods(): IDateRange[] { + return this.getDateRanges( + this.query.fromDate, + this.query.toDate, + this.query.displayColumnsBy + ); + } + + /** + * Retrieves the date periods of the given node based on the report query. + * @param {IBalanceSheetCommonNode} node + * @param {Function} callback + * @returns {} + */ + protected getReportNodeDatePeriods = ( + node: IBalanceSheetCommonNode, + callback: ( + node: IBalanceSheetCommonNode, + fromDate: Date, + toDate: Date, + index: number + ) => any + ) => { + return this.getNodeDatePeriods( + this.query.fromDate, + this.query.toDate, + this.query.displayColumnsBy, + node, + callback + ); + }; + + /** + * Retrieve the date period meta. + * @param {number} total - Total amount. + * @param {Date} fromDate - From date. + * @param {Date} toDate - To date. + * @return {ICashFlowDatePeriod} + */ + private getDatePeriodTotalMeta = ( + total: number, + fromDate: Date, + toDate: Date, + overrideSettings: IFormatNumberSettings = {} + ): IBalanceSheetTotalPeriod => { + return this.getDatePeriodMeta(total, fromDate, toDate, { + money: true, + ...overrideSettings, + }); + }; + + // -------------------------------- + // # Account + // -------------------------------- + /** + * Retrieve the given account date period total. + * @param {number} accountId + * @param {Date} toDate + * @returns {number} + */ + private getAccountDatePeriodTotal = ( + accountId: number, + toDate: Date + ): number => { + const periodTotalBetween = this.repository.periodsAccountsLedger + .whereAccountId(accountId) + .whereToDate(toDate) + .getClosingBalance(); + + const periodOpening = this.repository.periodsOpeningAccountLedger + .whereAccountId(accountId) + .getClosingBalance(); + + return periodOpening + periodTotalBetween; + }; + + /** + * + * @param {IBalanceSheetAccountNode} node + * @param {Date} fromDate + * @param {Date} toDate + * @returns {IBalanceSheetAccountNode} + */ + private getAccountNodeDatePeriod = ( + node: IBalanceSheetAccountNode, + fromDate: Date, + toDate: Date + ): IBalanceSheetTotalPeriod => { + const periodTotal = this.getAccountDatePeriodTotal(node.id, toDate); + + return this.getDatePeriodTotalMeta(periodTotal, fromDate, toDate); + }; + + /** + * Retrieve total date periods of the given account node. + * @param {IBalanceSheetAccountNode} node + * @returns {IBalanceSheetAccountNode} + */ + private getAccountsNodeDatePeriods = ( + node: IBalanceSheetAccountNode + ): IBalanceSheetTotalPeriod[] => { + return this.getReportNodeDatePeriods(node, this.getAccountNodeDatePeriod); + }; + + /** + * Assoc total date periods to account node. + * @param {IBalanceSheetAccountNode} node + * @returns {IBalanceSheetAccountNode} + */ + public assocAccountNodeDatePeriods = ( + node: IBalanceSheetAccountNode + ): IBalanceSheetAccountNode => { + const datePeriods = this.getAccountsNodeDatePeriods(node); + + return R.assoc('horizontalTotals', datePeriods, node); + }; + + // -------------------------------- + // # Aggregate + // -------------------------------- + /** + * + * @param {} node + * @param {number} index + * @returns {number} + */ + private getAggregateDatePeriodIndexTotal = (node, index) => { + return sumBy(node.children, `horizontalTotals[${index}].total.amount`); + }; + + /** + * + * @param {IBalanceSheetAccountNode} node + * @param {Date} fromDate + * @param {Date} toDate + * @returns + */ + public getAggregateNodeDatePeriod = ( + node: IBalanceSheetAccountNode, + fromDate: Date, + toDate: Date, + index: number + ) => { + const periodTotal = this.getAggregateDatePeriodIndexTotal(node, index); + + return this.getDatePeriodTotalMeta(periodTotal, fromDate, toDate); + }; + + /** + * + * @param node + * @returns + */ + public getAggregateNodeDatePeriods = (node) => { + return this.getReportNodeDatePeriods( + node, + this.getAggregateNodeDatePeriod + ); + }; + + /** + * Assoc total date periods to aggregate node. + * @param node + * @returns {} + */ + public assocAggregateNodeDatePeriods = (node) => { + const datePeriods = this.getAggregateNodeDatePeriods(node); + + return R.assoc('horizontalTotals', datePeriods, node); + }; + + /** + * + * @param node + * @returns + */ + public assocAccountsNodeDatePeriods = (node) => { + return this.assocAggregateNodeDatePeriods(node); + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetExportInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetExportInjectable.ts new file mode 100644 index 000000000..71269b744 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetExportInjectable.ts @@ -0,0 +1,57 @@ +import { Injectable } from '@nestjs/common'; +import { BalanceSheetTableInjectable } from './BalanceSheetTableInjectable'; +import { BalanceSheetPdfInjectable } from './BalanceSheetPdfInjectable'; +import { IBalanceSheetQuery } from './BalanceSheet.types'; +import { TableSheet } from '../../common/TableSheet'; + +@Injectable() +export class BalanceSheetExportInjectable { + constructor( + private readonly balanceSheetTable: BalanceSheetTableInjectable, + private readonly balanceSheetPdf: BalanceSheetPdfInjectable, + ) {} + + /** + * Retrieves the trial balance sheet in XLSX format. + * @param {number} tenantId + * @param {ITrialBalanceSheetQuery} query + * @returns {Promise} + */ + public async xlsx(query: IBalanceSheetQuery) { + const table = await this.balanceSheetTable.table(query); + + const tableSheet = new TableSheet(table.table); + const tableCsv = tableSheet.convertToXLSX(); + + return tableSheet.convertToBuffer(tableCsv, 'xlsx'); + } + + /** + * Retrieves the trial balance sheet in CSV format. + * @param {number} tenantId + * @param {ITrialBalanceSheetQuery} query + * @returns {Promise} + */ + public async csv( + query: IBalanceSheetQuery, + ): Promise { + const table = await this.balanceSheetTable.table(query); + + const tableSheet = new TableSheet(table.table); + const tableCsv = tableSheet.convertToCSV(); + + return tableCsv; + } + + /** + * Retrieves the balance sheet in pdf format. + * @param {number} tenantId + * @param {IBalanceSheetQuery} query + * @returns {Promise} + */ + public async pdf( + query: IBalanceSheetQuery, + ): Promise { + return this.balanceSheetPdf.pdf(query); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetFiltering.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetFiltering.ts new file mode 100644 index 000000000..79808d857 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetFiltering.ts @@ -0,0 +1,168 @@ +import * as R from 'ramda'; +import { get } from 'lodash'; +import { + IBalanceSheetDataNode, + BALANCE_SHEET_NODE_TYPE, +} from '../../../interfaces'; +import { FinancialFilter } from '../FinancialFilter'; +import { Constructor } from '@/common/types/Constructor'; + +export const BalanceSheetFiltering = (Base: T) => + class extends R.compose(FinancialFilter)(Base) { + // ----------------------- + // # Account + // ----------------------- + /** + * Filter report node detarmine. + * @param {IBalanceSheetDataNode} node - Balance sheet node. + * @return {boolean} + */ + private accountNoneZeroNodesFilterDetarminer = ( + node: IBalanceSheetDataNode, + ): boolean => { + return R.ifElse( + this.isNodeType(BALANCE_SHEET_NODE_TYPE.ACCOUNT), + this.isNodeNoneZero, + R.always(true), + )(node); + }; + + /** + * Detarmines account none-transactions node. + * @param {IBalanceSheetDataNode} node + * @returns {boolean} + */ + private accountNoneTransFilterDetarminer = ( + node: IBalanceSheetDataNode, + ): boolean => { + return R.ifElse( + this.isNodeType(BALANCE_SHEET_NODE_TYPE.ACCOUNT), + this.isNodeNoneZero, + R.always(true), + )(node); + }; + + /** + * Report nodes filter. + * @param {IBalanceSheetSection[]} nodes - + * @return {IBalanceSheetSection[]} + */ + private accountsNoneZeroNodesFilter = ( + nodes: IBalanceSheetDataNode[], + ): IBalanceSheetDataNode[] => { + return this.filterNodesDeep( + nodes, + this.accountNoneZeroNodesFilterDetarminer, + ); + }; + + /** + * Filters the accounts none-transactions nodes. + * @param {IBalanceSheetDataNode[]} nodes + * @returns {IBalanceSheetDataNode[]} + */ + private accountsNoneTransactionsNodesFilter = ( + nodes: IBalanceSheetDataNode[], + ) => { + return this.filterNodesDeep(nodes, this.accountNoneTransFilterDetarminer); + }; + + // ----------------------- + // # Aggregate/Accounts. + // ----------------------- + /** + * Detearmines aggregate none-children filtering. + * @param {IBalanceSheetDataNode} node + * @returns {boolean} + */ + private aggregateNoneChildrenFilterDetarminer = ( + node: IBalanceSheetDataNode, + ): boolean => { + // Detarmines whether the given node is aggregate or accounts node. + const isAggregateOrAccounts = + this.isNodeType(BALANCE_SHEET_NODE_TYPE.AGGREGATE, node) || + this.isNodeType(BALANCE_SHEET_NODE_TYPE.ACCOUNTS, node); + + // Retrieve the schema node of the given id. + const schemaNode = this.getSchemaNodeById(node.id); + + // Detarmines if the schema node is always should show. + const isSchemaAlwaysShow = get(schemaNode, 'alwaysShow', false); + + return isAggregateOrAccounts && !isSchemaAlwaysShow + ? this.isNodeHasChildren(node) + : true; + }; + + /** + * Filters aggregate none-children nodes. + * @param {IBalanceSheetDataNode[]} nodes + * @returns {IBalanceSheetDataNode[]} + */ + private aggregateNoneChildrenFilter = ( + nodes: IBalanceSheetDataNode[], + ): IBalanceSheetDataNode[] => { + return this.filterNodesDeep2( + this.aggregateNoneChildrenFilterDetarminer, + nodes, + ); + }; + + // ----------------------- + // # Composers. + // ----------------------- + /** + * Filters none-zero nodes. + * @param {IBalanceSheetDataNode[]} nodes + * @returns {IBalanceSheetDataNode[]} + */ + private filterNoneZeroNodesCompose = ( + nodes: IBalanceSheetDataNode[], + ): IBalanceSheetDataNode[] => { + return R.compose( + this.aggregateNoneChildrenFilter, + this.accountsNoneZeroNodesFilter, + )(nodes); + }; + + /** + * Filters none-transactions nodes. + * @param {IBalanceSheetDataNode[]} nodes + * @returns {IBalanceSheetDataNode[]} + */ + private filterNoneTransNodesCompose = ( + nodes: IBalanceSheetDataNode[], + ): IBalanceSheetDataNode[] => { + return R.compose( + this.aggregateNoneChildrenFilter, + this.accountsNoneTransactionsNodesFilter, + )(nodes); + }; + + /** + * Supress nodes when accounts transactions ledger is empty. + * @param {IBalanceSheetDataNode[]} nodes + * @returns {IBalanceSheetDataNode[]} + */ + private supressNodesWhenAccountsTransactionsEmpty = ( + nodes: IBalanceSheetDataNode[], + ): IBalanceSheetDataNode[] => { + return this.repository.totalAccountsLedger.isEmpty() ? [] : nodes; + }; + + /** + * Compose report nodes filtering. + * @param {IBalanceSheetDataNode[]} nodes + * @returns {IBalanceSheetDataNode[]} + */ + protected reportFilterPlugin = (nodes: IBalanceSheetDataNode[]) => { + return R.compose( + this.supressNodesWhenAccountsTransactionsEmpty, + R.when(R.always(this.query.noneZero), this.filterNoneZeroNodesCompose), + R.when( + R.always(this.query.noneTransactions), + this.filterNoneTransNodesCompose, + ), + )(nodes); + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetInjectable.ts new file mode 100644 index 000000000..ca12afd32 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetInjectable.ts @@ -0,0 +1,101 @@ +import moment from 'moment'; +import { + IBalanceSheetStatementService, + IBalanceSheetQuery, + IBalanceSheetStatement, +} from './BalanceSheet.types'; +import BalanceSheetRepository from './BalanceSheetRepository'; +import { BalanceSheetMetaInjectable } from './BalanceSheetMeta'; +import { Injectable } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { events } from '@/common/events/events'; + +@Injectable() +export class BalanceSheetInjectable { + constructor( + private readonly balanceSheetMeta: BalanceSheetMetaInjectable, + private readonly eventPublisher: EventEmitter2, + ) {} + + /** + * Defaults balance sheet filter query. + * @return {IBalanceSheetQuery} + */ + get defaultQuery(): IBalanceSheetQuery { + return { + displayColumnsType: 'total', + displayColumnsBy: 'month', + + fromDate: moment().startOf('year').format('YYYY-MM-DD'), + toDate: moment().format('YYYY-MM-DD'), + + numberFormat: { + precision: 2, + divideOn1000: false, + showZero: false, + formatMoney: 'total', + negativeFormat: 'mines', + }, + noneZero: false, + noneTransactions: false, + + basis: 'cash', + accountIds: [], + + percentageOfColumn: false, + percentageOfRow: false, + + previousPeriod: false, + previousPeriodAmountChange: false, + previousPeriodPercentageChange: false, + + previousYear: false, + previousYearAmountChange: false, + previousYearPercentageChange: false, + }; + } + + /** + * Retrieve balance sheet statement. + * @param {number} tenantId + * @param {IBalanceSheetQuery} query + * @return {IBalanceSheetStatement} + */ + public async balanceSheet( + tenantId: number, + query: IBalanceSheetQuery, + ): Promise { + + const filter = { + ...this.defaultQuery, + ...query, + }; + const balanceSheetRepo = new BalanceSheetRepository(models, filter); + + // Loads all resources. + await balanceSheetRepo.asyncInitialize(); + + // Balance sheet report instance. + const balanceSheetInstanace = new BalanceSheetStatementService( + filter, + balanceSheetRepo, + tenant.metadata.baseCurrency, + i18n, + ); + // Balance sheet data. + const data = balanceSheetInstanace.reportData(); + + // Balance sheet meta. + const meta = await this.balanceSheetMeta.meta(tenantId, filter); + + // Triggers `onBalanceSheetViewed` event. + await this.eventPublisher.emitAsync(events.reports.onBalanceSheetViewed, { + query, + }); + return { + query: filter, + data, + meta, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetMeta.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetMeta.ts new file mode 100644 index 000000000..c7a3e87c0 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetMeta.ts @@ -0,0 +1,32 @@ +import { Inject, Service } from 'typedi'; +import { FinancialSheetMeta } from '../FinancialSheetMeta'; +import { IBalanceSheetMeta, IBalanceSheetQuery } from '@/interfaces'; +import moment from 'moment'; + +@Service() +export class BalanceSheetMetaInjectable { + @Inject() + private financialSheetMeta: FinancialSheetMeta; + + /** + * Retrieve the balance sheet meta. + * @param {number} tenantId - + * @returns {IBalanceSheetMeta} + */ + public async meta( + tenantId: number, + query: IBalanceSheetQuery + ): Promise { + const commonMeta = await this.financialSheetMeta.meta(tenantId); + const formattedAsDate = moment(query.toDate).format('YYYY/MM/DD'); + const formattedDateRange = `As ${formattedAsDate}`; + const sheetName = 'Balance Sheet Statement'; + + return { + ...commonMeta, + sheetName, + formattedAsDate, + formattedDateRange, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetNetIncome.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetNetIncome.ts new file mode 100644 index 000000000..a6695b370 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetNetIncome.ts @@ -0,0 +1,226 @@ +import * as R from 'ramda'; +import { + BALANCE_SHEET_SCHEMA_NODE_TYPE, + IBalanceSheetDataNode, + IBalanceSheetNetIncomeNode, + IBalanceSheetSchemaNetIncomeNode, + IBalanceSheetSchemaNode, + IBalanceSheetTotalPeriod, +} from '@/interfaces'; +import { BalanceSheetComparsionPreviousYear } from './BalanceSheetComparsionPreviousYear'; +import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod'; +import { FinancialPreviousPeriod } from '../FinancialPreviousPeriod'; +import { FinancialHorizTotals } from '../FinancialHorizTotals'; +import BalanceSheetRepository from './BalanceSheetRepository'; +import { BalanceSheetQuery } from './BalanceSheetQuery'; +import { BalanceSheetNetIncomePP } from './BalanceSheetNetIncomePP'; +import { BalanceSheetNetIncomePY } from './BalanceSheetNetIncomePY'; + +export const BalanceSheetNetIncome = (Base: any) => + class extends R.compose( + BalanceSheetNetIncomePP, + BalanceSheetNetIncomePY, + BalanceSheetComparsionPreviousYear, + BalanceSheetComparsionPreviousPeriod, + FinancialPreviousPeriod, + FinancialHorizTotals + )(Base) { + private repository: BalanceSheetRepository; + private query: BalanceSheetQuery; + + /** + * Retrieves the closing balance of income accounts. + * @returns {number} + */ + private getIncomeTotal = () => { + const closeingBalance = this.repository.incomeLedger.getClosingBalance(); + return closeingBalance; + }; + + /** + * Retrieves the closing balance of expenses accounts. + * @returns {number} + */ + private getExpensesTotal = () => { + const closingBalance = this.repository.expensesLedger.getClosingBalance(); + return closingBalance; + }; + + /** + * Retrieves the total net income. + * @returns {number} + */ + protected getNetIncomeTotal = () => { + const income = this.getIncomeTotal(); + const expenses = this.getExpensesTotal(); + + return income - expenses; + }; + + /** + * Mappes the aggregate schema node type. + * @param {IBalanceSheetSchemaNetIncomeNode} node - Schema node. + * @return {IBalanceSheetAggregateNode} + */ + protected schemaNetIncomeNodeMapper = ( + node: IBalanceSheetSchemaNetIncomeNode + ): IBalanceSheetNetIncomeNode => { + const total = this.getNetIncomeTotal(); + + return { + id: node.id, + name: this.i18n.__(node.name), + nodeType: BALANCE_SHEET_SCHEMA_NODE_TYPE.NET_INCOME, + total: this.getTotalAmountMeta(total), + }; + }; + + /** + * Mapps the net income shcema node to report node. + * @param {IBalanceSheetSchemaNetIncomeNode} node + * @returns {IBalanceSheetNetIncomeNode} + */ + protected schemaNetIncomeNodeCompose = ( + node: IBalanceSheetSchemaNetIncomeNode + ): IBalanceSheetNetIncomeNode => { + return R.compose( + R.when( + this.query.isPreviousYearActive, + this.previousYearNetIncomeNodeCompose + ), + R.when( + this.query.isPreviousPeriodActive, + this.previousPeriodNetIncomeNodeCompose + ), + R.when( + this.query.isDatePeriodsColumnsType, + this.assocNetIncomeDatePeriodsNode + ), + this.schemaNetIncomeNodeMapper + )(node); + }; + + // -------------------------------- + // # Date Periods + // -------------------------------- + /** + * Retreives total income of the given date period. + * @param {number} accountId - + * @param {Date} toDate - + * @returns {number} + */ + private getIncomeDatePeriodTotal = (toDate: Date): number => { + const periodTotalBetween = this.repository.incomePeriodsAccountsLedger + .whereToDate(toDate) + .getClosingBalance(); + + const periodOpening = + this.repository.incomePeriodsOpeningAccountsLedger.getClosingBalance(); + + return periodOpening + periodTotalBetween; + }; + + /** + * Retrieves total expense of the given date period. + * @param {number} accountId - + * @param {Date} toDate - + * @returns {number} + */ + private getExpensesDatePeriodTotal = (toDate: Date): number => { + const periodTotalBetween = this.repository.expensesPeriodsAccountsLedger + .whereToDate(toDate) + .getClosingBalance(); + + const periodOpening = + this.repository.expensesOpeningAccountLedger.getClosingBalance(); + + return periodOpening + periodTotalBetween; + }; + + /** + * Retrieve the given net income date period total. + * @param {number} accountId + * @param {Date} toDate + * @returns {number} + */ + private getNetIncomeDatePeriodTotal = (toDate: Date): number => { + const income = this.getIncomeDatePeriodTotal(toDate); + const expense = this.getExpensesDatePeriodTotal(toDate); + + return income - expense; + }; + + /** + * Retrieves the net income date period node. + * @param {IBalanceSheetNetIncomeNode} node + * @param {Date} fromDate + * @param {Date} toDate + * @returns {IBalanceSheetNetIncomeNode} + */ + private getNetIncomeDatePeriodNode = ( + node: IBalanceSheetNetIncomeNode, + fromDate: Date, + toDate: Date + ): IBalanceSheetTotalPeriod => { + const periodTotal = this.getNetIncomeDatePeriodTotal(toDate); + + return this.getDatePeriodTotalMeta(periodTotal, fromDate, toDate); + }; + + /** + * Retrieve total date periods of the given net income node. + * @param {IBalanceSheetNetIncomeNode} node + * @returns {IBalanceSheetNetIncomeNode} + */ + private getNetIncomeDatePeriodsNode = ( + node: IBalanceSheetNetIncomeNode + ): IBalanceSheetTotalPeriod[] => { + return this.getReportNodeDatePeriods( + node, + this.getNetIncomeDatePeriodNode + ); + }; + + /** + * Assoc total date periods to net income node. + * @param {IBalanceSheetNetIncomeNode} node + * @returns {IBalanceSheetNetIncomeNode} + */ + public assocNetIncomeDatePeriodsNode = ( + node: IBalanceSheetNetIncomeNode + ): IBalanceSheetNetIncomeNode => { + const datePeriods = this.getNetIncomeDatePeriodsNode(node); + + return R.assoc('horizontalTotals', datePeriods, node); + }; + + // ----------------------------- + // - Net Income Nodes Praser + // ----------------------------- + /** + * Mappes the given report schema node. + * @param {IBalanceSheetSchemaNode} node - Schema node. + * @return {IBalanceSheetDataNode} + */ + private reportNetIncomeNodeSchemaParser = ( + schemaNode: IBalanceSheetSchemaNode + ): IBalanceSheetDataNode => { + return R.compose( + R.when( + this.isSchemaNodeType(BALANCE_SHEET_SCHEMA_NODE_TYPE.NET_INCOME), + this.schemaNetIncomeNodeCompose + ) + )(schemaNode); + }; + + /** + * Parses the report net income schema nodes. + * @param {(IBalanceSheetSchemaNode | IBalanceSheetDataNode)[]} nodes - + * @return {IBalanceSheetDataNode[]} + */ + public netIncomeSchemaParser = ( + nodes: (IBalanceSheetSchemaNode | IBalanceSheetDataNode)[] + ): IBalanceSheetDataNode[] => { + return this.mapNodesDeep(nodes, this.reportNetIncomeNodeSchemaParser); + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetNetIncomeDatePeriods.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetNetIncomeDatePeriods.ts new file mode 100644 index 000000000..63bfa362a --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetNetIncomeDatePeriods.ts @@ -0,0 +1,120 @@ +import * as R from 'ramda'; +import { + IBalanceSheetNetIncomeNode, + IBalanceSheetTotalPeriod, +} from '@/interfaces'; +import { BalanceSheetComparsionPreviousYear } from './BalanceSheetComparsionPreviousYear'; +import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod'; +import { FinancialPreviousPeriod } from '../FinancialPreviousPeriod'; +import { FinancialHorizTotals } from '../FinancialHorizTotals'; +import BalanceSheetRepository from './BalanceSheetRepository'; +import { BalanceSheetQuery } from './BalanceSheetQuery'; +import { BalanceSheetNetIncomePP } from './BalanceSheetNetIncomePP'; +import { BalanceSheetNetIncomePY } from './BalanceSheetNetIncomePY'; + +export const BalanceSheetNetIncomeDatePeriods = (Base: any) => + class extends R.compose( + BalanceSheetNetIncomePP, + BalanceSheetNetIncomePY, + BalanceSheetComparsionPreviousYear, + BalanceSheetComparsionPreviousPeriod, + FinancialPreviousPeriod, + FinancialHorizTotals + )(Base) { + private repository: BalanceSheetRepository; + private query: BalanceSheetQuery; + + // -------------------------------- + // # Date Periods + // -------------------------------- + /** + * Retreives total income of the given date period. + * @param {number} accountId - + * @param {Date} toDate - + * @returns {number} + */ + private getIncomeDatePeriodTotal = (toDate: Date): number => { + const periodTotalBetween = this.repository.incomePeriodsAccountsLedger + .whereToDate(toDate) + .getClosingBalance(); + + const periodOpening = + this.repository.incomePeriodsOpeningAccountsLedger.getClosingBalance(); + + return periodOpening + periodTotalBetween; + }; + + /** + * Retrieves total expense of the given date period. + * @param {number} accountId - + * @param {Date} toDate - + * @returns {number} + */ + private getExpensesDatePeriodTotal = (toDate: Date): number => { + const periodTotalBetween = this.repository.expensesPeriodsAccountsLedger + .whereToDate(toDate) + .getClosingBalance(); + + const periodOpening = + this.repository.expensesOpeningAccountLedger.getClosingBalance(); + + return periodOpening + periodTotalBetween; + }; + + /** + * Retrieve the given net income date period total. + * @param {number} accountId + * @param {Date} toDate + * @returns {number} + */ + private getNetIncomeDatePeriodTotal = (toDate: Date): number => { + const income = this.getIncomeDatePeriodTotal(toDate); + const expense = this.getExpensesDatePeriodTotal(toDate); + + return income - expense; + }; + + /** + * Retrieves the net income date period node. + * @param {IBalanceSheetNetIncomeNode} node + * @param {Date} fromDate + * @param {Date} toDate + * @returns {IBalanceSheetNetIncomeNode} + */ + private getNetIncomeDatePeriodNode = ( + node: IBalanceSheetNetIncomeNode, + fromDate: Date, + toDate: Date + ): IBalanceSheetTotalPeriod => { + const periodTotal = this.getNetIncomeDatePeriodTotal(toDate); + + return this.getDatePeriodTotalMeta(periodTotal, fromDate, toDate); + }; + + /** + * Retrieve total date periods of the given net income node. + * @param {IBalanceSheetNetIncomeNode} node + * @returns {IBalanceSheetNetIncomeNode} + */ + private getNetIncomeDatePeriodsNode = ( + node: IBalanceSheetNetIncomeNode + ): IBalanceSheetTotalPeriod[] => { + return this.getReportNodeDatePeriods( + node, + this.getNetIncomeDatePeriodNode + ); + }; + + /** + * Assoc total date periods to net income node. + * @param {IBalanceSheetNetIncomeNode} node + * @returns {IBalanceSheetNetIncomeNode} + */ + public assocNetIncomeDatePeriodsNode = ( + node: IBalanceSheetNetIncomeNode + ): IBalanceSheetNetIncomeNode => { + const datePeriods = this.getNetIncomeDatePeriodsNode(node); + + return R.assoc('horizontalTotals', datePeriods, node); + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetNetIncomeDatePeriodsPP.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetNetIncomeDatePeriodsPP.ts new file mode 100644 index 000000000..94038de38 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetNetIncomeDatePeriodsPP.ts @@ -0,0 +1,127 @@ +import * as R from 'ramda'; +import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod'; +import { FinancialPreviousPeriod } from '../FinancialPreviousPeriod'; +import { FinancialHorizTotals } from '../FinancialHorizTotals'; +import { IBalanceSheetNetIncomeNode, IBalanceSheetTotal } from '@/interfaces'; +import { BalanceSheetQuery } from './BalanceSheetQuery'; +import BalanceSheetRepository from './BalanceSheetRepository'; + +export const BalanceSheetNetIncomeDatePeriodsPP = (Base: any) => + class extends R.compose( + BalanceSheetComparsionPreviousPeriod, + FinancialPreviousPeriod, + FinancialHorizTotals + )(Base) { + query: BalanceSheetQuery; + repository: BalanceSheetRepository; + + /** + * Retrieves the PY total income of the given date period. + * @param {number} accountId - + * @param {Date} toDate - + * @return {number} + */ + private getPPIncomeDatePeriodTotal = R.curry((toDate: Date) => { + const PYPeriodsTotal = this.repository.incomePPPeriodsAccountsLedger + .whereToDate(toDate) + .getClosingBalance(); + + const PYPeriodsOpeningTotal = + this.repository.incomePPPeriodsOpeningAccountLedger.getClosingBalance(); + + return PYPeriodsOpeningTotal + PYPeriodsTotal; + }); + + /** + * Retrieves the PY total expense of the given date period. + * @param {number} accountId - + * @param {Date} toDate - + * @returns {number} + */ + private getPPExpenseDatePeriodTotal = R.curry((toDate: Date) => { + const PYPeriodsTotal = this.repository.expensePPPeriodsAccountsLedger + .whereToDate(toDate) + .getClosingBalance(); + + const PYPeriodsOpeningTotal = + this.repository.expensePPPeriodsOpeningAccountLedger.getClosingBalance(); + + return PYPeriodsOpeningTotal + PYPeriodsTotal; + }); + + /** + * Retrieve the given net income total of the given period. + * @param {number} accountId - Account id. + * @param {Date} toDate - To date. + * @returns {number} + */ + private getPPNetIncomeDatePeriodTotal = R.curry((toDate: Date) => { + const income = this.getPPIncomeDatePeriodTotal(toDate); + const expense = this.getPPExpenseDatePeriodTotal(toDate); + + return income - expense; + }); + + /** + * Assoc preivous period to account horizontal total node. + * @param {IBalanceSheetAccountNode} node + * @returns {} + */ + private assocPreviousPeriodNetIncomeHorizTotal = R.curry( + (node: IBalanceSheetNetIncomeNode, totalNode) => { + const total = this.getPPNetIncomeDatePeriodTotal( + totalNode.previousPeriodToDate.date + ); + return R.assoc('previousPeriod', this.getAmountMeta(total), totalNode); + } + ); + + /** + * Compose previous period to aggregate horizontal nodes. + * @param {IBalanceSheetTotal} node + * @returns {IBalanceSheetTotal} + */ + private previousPeriodNetIncomeHorizNodeComposer = R.curry( + ( + node: IBalanceSheetNetIncomeNode, + horiontalTotalNode: IBalanceSheetTotal + ): IBalanceSheetTotal => { + return R.compose( + R.when( + this.query.isPreviousPeriodPercentageActive, + this.assocPreviousPeriodTotalPercentageNode + ), + R.when( + this.query.isPreviousPeriodChangeActive, + this.assocPreviousPeriodTotalChangeNode + ), + R.when( + this.query.isPreviousPeriodActive, + this.assocPreviousPeriodNetIncomeHorizTotal(node) + ), + R.when( + this.query.isPreviousPeriodActive, + this.assocPreviousPeriodHorizNodeFromToDates( + this.query.displayColumnsBy + ) + ) + )(horiontalTotalNode); + } + ); + + /** + * Associate the PP to net income horizontal nodes. + * @param {IBalanceSheetCommonNode} node + * @returns {IBalanceSheetCommonNode} + */ + public assocPreviousPeriodNetIncomeHorizNode = ( + node: IBalanceSheetNetIncomeNode + ): IBalanceSheetNetIncomeNode => { + const horizontalTotals = R.addIndex(R.map)( + this.previousPeriodNetIncomeHorizNodeComposer(node), + node.horizontalTotals + ) as IBalanceSheetTotal[]; + + return R.assoc('horizontalTotals', horizontalTotals, node); + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetNetIncomeDatePeriodsPY.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetNetIncomeDatePeriodsPY.ts new file mode 100644 index 000000000..3e4a289e9 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetNetIncomeDatePeriodsPY.ts @@ -0,0 +1,122 @@ +import * as R from 'ramda'; +import { BalanceSheetComparsionPreviousYear } from './BalanceSheetComparsionPreviousYear'; +import { FinancialPreviousPeriod } from '../FinancialPreviousPeriod'; +import { FinancialHorizTotals } from '../FinancialHorizTotals'; +import { IBalanceSheetNetIncomeNode, IBalanceSheetTotal } from '@/interfaces'; +import { BalanceSheetQuery } from './BalanceSheetQuery'; +import BalanceSheetRepository from './BalanceSheetRepository'; + +export const BalanceSheetNetIncomeDatePeriodsPY = (Base: any) => + class extends R.compose( + BalanceSheetComparsionPreviousYear, + FinancialPreviousPeriod, + FinancialHorizTotals + )(Base) { + query: BalanceSheetQuery; + repository: BalanceSheetRepository; + + /** + * Retrieves the PY total income of the given date period. + * @param {Date} toDate - + * @return {number} + */ + private getPYIncomeDatePeriodTotal = R.curry((toDate: Date) => { + const PYPeriodsTotal = this.repository.incomePYPeriodsAccountsLedger + .whereToDate(toDate) + .getClosingBalance(); + + const PYPeriodsOpeningTotal = + this.repository.incomePYPeriodsOpeningAccountLedger.getClosingBalance(); + + return PYPeriodsOpeningTotal + PYPeriodsTotal; + }); + + /** + * Retrieves the PY total expense of the given date period. + * @param {Date} toDate - + * @returns {number} + */ + private getPYExpenseDatePeriodTotal = R.curry((toDate: Date) => { + const PYPeriodsTotal = this.repository.expensePYPeriodsAccountsLedger + .whereToDate(toDate) + .getClosingBalance(); + + const PYPeriodsOpeningTotal = + this.repository.expensePYPeriodsOpeningAccountLedger.getClosingBalance(); + + return PYPeriodsOpeningTotal + PYPeriodsTotal; + }); + + /** + * Retrieve the given net income total of the given period. + * @param {Date} toDate - To date. + * @returns {number} + */ + private getPYNetIncomeDatePeriodTotal = R.curry((toDate: Date) => { + const income = this.getPYIncomeDatePeriodTotal(toDate); + const expense = this.getPYExpenseDatePeriodTotal(toDate); + + return income - expense; + }); + + /** + * Assoc preivous year to account horizontal total node. + * @param {IBalanceSheetAccountNode} node + * @returns {} + */ + private assocPreviousYearNetIncomeHorizTotal = R.curry( + (node: IBalanceSheetNetIncomeNode, totalNode) => { + const total = this.getPYNetIncomeDatePeriodTotal( + totalNode.previousYearToDate.date + ); + return R.assoc('previousYear', this.getAmountMeta(total), totalNode); + } + ); + + /** + * Compose PY to net income horizontal nodes. + * @param {IBalanceSheetTotal} node + * @returns {IBalanceSheetTotal} + */ + private previousYearNetIncomeHorizNodeComposer = R.curry( + ( + node: IBalanceSheetNetIncomeNode, + horiontalTotalNode: IBalanceSheetTotal + ): IBalanceSheetTotal => { + return R.compose( + R.when( + this.query.isPreviousYearPercentageActive, + this.assocPreviousYearTotalPercentageNode + ), + R.when( + this.query.isPreviousYearChangeActive, + this.assocPreviousYearTotalChangeNode + ), + R.when( + this.query.isPreviousYearActive, + this.assocPreviousYearNetIncomeHorizTotal(node) + ), + R.when( + this.query.isPreviousYearActive, + this.assocPreviousYearHorizNodeFromToDates + ) + )(horiontalTotalNode); + } + ); + + /** + * Associate the PY to net income horizontal nodes. + * @param {IBalanceSheetCommonNode} node + * @returns {IBalanceSheetCommonNode} + */ + public assocPreviousYearNetIncomeHorizNode = ( + node: IBalanceSheetNetIncomeNode + ): IBalanceSheetNetIncomeNode => { + const horizontalTotals = R.addIndex(R.map)( + this.previousYearNetIncomeHorizNodeComposer(node), + node.horizontalTotals + ) as IBalanceSheetTotal[]; + + return R.assoc('horizontalTotals', horizontalTotals, node); + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetNetIncomePP.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetNetIncomePP.ts new file mode 100644 index 000000000..c377511a3 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetNetIncomePP.ts @@ -0,0 +1,75 @@ +import * as R from 'ramda'; +import { + IBalanceSheetDataNode, + IBalanceSheetNetIncomeNode, +} from '@/interfaces'; +import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod'; +import { FinancialPreviousPeriod } from '../FinancialPreviousPeriod'; +import { FinancialHorizTotals } from '../FinancialHorizTotals'; +import BalanceSheetRepository from './BalanceSheetRepository'; +import { BalanceSheetQuery } from './BalanceSheetQuery'; +import { BalanceSheetNetIncomeDatePeriodsPP } from './BalanceSheetNetIncomeDatePeriodsPP'; + +export const BalanceSheetNetIncomePP = (Base: any) => + class extends R.compose( + BalanceSheetNetIncomeDatePeriodsPP, + BalanceSheetComparsionPreviousPeriod, + FinancialPreviousPeriod, + FinancialHorizTotals + )(Base) { + private repository: BalanceSheetRepository; + private query: BalanceSheetQuery; + + // ------------------------------- + // # Previous Period (PP) + // ------------------------------- + /** + * Retrieves the PP net income. + * @returns {} + */ + protected getPreviousPeriodNetIncome = () => { + const income = this.repository.incomePPAccountsLedger.getClosingBalance(); + const expense = + this.repository.expensePPAccountsLedger.getClosingBalance(); + + return income - expense; + }; + + /** + * Associates the previous period to account node. + * @param {IBalanceSheetDataNode} node + * @returns {IBalanceSheetDataNode} + */ + protected assocPreviousPeriodNetIncomeNode = ( + node: IBalanceSheetDataNode + ): IBalanceSheetDataNode => { + const total = this.getPreviousPeriodNetIncome(); + + return R.assoc('previousPeriod', this.getAmountMeta(total), node); + }; + + /** + * Previous period account node composer. + * @param {IBalanceSheetNetIncomeNode} node + * @returns {IBalanceSheetNetIncomeNode} + */ + protected previousPeriodNetIncomeNodeCompose = ( + node: IBalanceSheetNetIncomeNode + ): IBalanceSheetNetIncomeNode => { + return R.compose( + R.when( + this.isNodeHasHorizTotals, + this.assocPreviousPeriodNetIncomeHorizNode + ), + R.when( + this.query.isPreviousPeriodPercentageActive, + this.assocPreviousPeriodPercentageNode + ), + R.when( + this.query.isPreviousPeriodChangeActive, + this.assocPreviousPeriodChangeNode + ), + this.assocPreviousPeriodNetIncomeNode + )(node); + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetNetIncomePY.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetNetIncomePY.ts new file mode 100644 index 000000000..29d57813c --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetNetIncomePY.ts @@ -0,0 +1,79 @@ +import * as R from 'ramda'; +import { + IBalanceSheetDataNode, + IBalanceSheetNetIncomeNode, +} from '@/interfaces'; +import { BalanceSheetComparsionPreviousYear } from './BalanceSheetComparsionPreviousYear'; +import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod'; +import { FinancialPreviousPeriod } from '../FinancialPreviousPeriod'; +import { FinancialHorizTotals } from '../FinancialHorizTotals'; +import BalanceSheetRepository from './BalanceSheetRepository'; +import { BalanceSheetQuery } from './BalanceSheetQuery'; +import { BalanceSheetNetIncomeDatePeriodsPY } from './BalanceSheetNetIncomeDatePeriodsPY'; + +export const BalanceSheetNetIncomePY = (Base: any) => + class extends R.compose( + BalanceSheetNetIncomeDatePeriodsPY, + BalanceSheetComparsionPreviousYear, + BalanceSheetComparsionPreviousPeriod, + FinancialPreviousPeriod, + FinancialHorizTotals + )(Base) { + private repository: BalanceSheetRepository; + private query: BalanceSheetQuery; + + // ------------------------------ + // # Previous Year (PY) + // ------------------------------ + /** + * Retrieves the previous year (PY) net income. + * @returns {number} + */ + protected getPreviousYearNetIncome = () => { + const income = + this.repository.incomePYTotalAccountsLedger.getClosingBalance(); + const expense = + this.repository.expensePYTotalAccountsLedger.getClosingBalance(); + + return income - expense; + }; + + /** + * Assoc previous year on aggregate node. + * @param {IBalanceSheetAccountNode} node + * @returns {IBalanceSheetAccountNode} + */ + protected assocPreviousYearNetIncomeNode = ( + node: IBalanceSheetNetIncomeNode + ): IBalanceSheetNetIncomeNode => { + const total = this.getPreviousYearNetIncome(); + + return R.assoc('previousYear', this.getTotalAmountMeta(total), node); + }; + + /** + * Assoc previous year attributes to aggregate node. + * @param {IBalanceSheetAccountNode} node + * @returns {IBalanceSheetAccountNode} + */ + protected previousYearNetIncomeNodeCompose = ( + node: IBalanceSheetNetIncomeNode + ): IBalanceSheetNetIncomeNode => { + return R.compose( + R.when( + this.query.isPreviousYearPercentageActive, + this.assocPreviousYearTotalPercentageNode + ), + R.when( + this.query.isPreviousYearChangeActive, + this.assocPreviousYearTotalChangeNode + ), + // Associate the PY to date periods horizontal nodes. + R.when( + this.isNodeHasHorizontalTotals, + this.assocPreviousYearNetIncomeHorizNode + ), + this.assocPreviousYearNetIncomeNode + )(node); + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetPdfInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetPdfInjectable.ts new file mode 100644 index 000000000..6727e57d4 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetPdfInjectable.ts @@ -0,0 +1,32 @@ +import { TableSheetPdf } from '../../TableSheetPdf'; +import { IBalanceSheetQuery } from './BalanceSheet.types'; +import { BalanceSheetTableInjectable } from './BalanceSheetTableInjectable'; +import { HtmlTableCustomCss } from './constants'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class BalanceSheetPdfInjectable { + constructor( + private readonly balanceSheetTable: BalanceSheetTableInjectable, + private readonly tableSheetPdf: TableSheetPdf, + ) {} + + /** + * Converts the given balance sheet table to pdf. + * @param {number} tenantId - Tenant ID. + * @param {IBalanceSheetQuery} query - Balance sheet query. + * @returns {Promise} + */ + public async pdf( + query: IBalanceSheetQuery, + ): Promise { + const table = await this.balanceSheetTable.table(query); + + return this.tableSheetPdf.convertToPdf( + table.table, + table.meta.sheetName, + table.meta.formattedDateRange, + HtmlTableCustomCss, + ); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetPercentage.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetPercentage.ts new file mode 100644 index 000000000..4c97ca1d4 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetPercentage.ts @@ -0,0 +1,226 @@ +import * as R from 'ramda'; +import { get } from 'lodash'; +import { BalanceSheetQuery } from './BalanceSheetQuery'; +import { IBalanceSheetDataNode } from './BalanceSheet.types'; +import { Constructor } from '@/common/types/Constructor'; + +export const BalanceSheetPercentage = (Base: any) => + class extends Base { + readonly query: BalanceSheetQuery; + + /** + * Assoc percentage of column to report node. + * @param {IBalanceSheetDataNode} node + * @returns {IBalanceSheetDataNode} + */ + public assocReportNodeColumnPercentage = R.curry( + ( + parentTotal: number, + node: IBalanceSheetDataNode + ): IBalanceSheetDataNode => { + const percentage = this.getPercentageBasis( + parentTotal, + node.total.amount + ); + return R.assoc( + 'percentageColumn', + this.getPercentageAmountMeta(percentage), + node + ); + } + ); + + /** + * Assoc percentage of row to report node. + * @param {IBalanceSheetDataNode} node + * @returns {IBalanceSheetDataNode} + */ + public assocReportNodeRowPercentage = R.curry( + ( + parentTotal: number, + node: IBalanceSheetDataNode + ): IBalanceSheetDataNode => { + const percenatage = this.getPercentageBasis( + parentTotal, + node.total.amount + ); + return R.assoc( + 'percentageRow', + this.getPercentageAmountMeta(percenatage), + node + ); + } + ); + + /** + * Assoc percentage of row to horizontal total. + * @param {number} parentTotal - + * @param {IBalanceSheetDataNode} node + * @returns {IBalanceSheetDataNode} + */ + public assocRowPercentageHorizTotals = R.curry( + ( + parentTotal: number, + node: IBalanceSheetDataNode + ): IBalanceSheetDataNode => { + const assocRowPercen = this.assocReportNodeRowPercentage(parentTotal); + const horTotals = R.map(assocRowPercen)(node.horizontalTotals); + + return R.assoc('horizontalTotals', horTotals, node); + } + ); + + /** + * + * @param {} parentNode - + * @param {} horTotalNode - + * @param {number} index - + */ + private assocColumnPercentageHorizTotal = R.curry( + (parentNode, horTotalNode, index) => { + const parentTotal = get( + parentNode, + `horizontalTotals[${index}].total.amount`, + 0 + ); + return this.assocReportNodeColumnPercentage(parentTotal, horTotalNode); + } + ); + + /** + * Assoc column percentage to horizontal totals nodes. + * @param {IBalanceSheetDataNode} node + * @returns {IBalanceSheetDataNode} + */ + public assocColumnPercentageHorizTotals = R.curry( + ( + parentNode: IBalanceSheetDataNode, + node: IBalanceSheetDataNode + ): IBalanceSheetDataNode => { + // Horizontal totals. + const assocColPerc = this.assocColumnPercentageHorizTotal(parentNode); + const horTotals = R.addIndex(R.map)(assocColPerc)( + node.horizontalTotals + ); + return R.assoc('horizontalTotals', horTotals, node); + } + ); + + /** + * + * @param {number} parentTotal - + * @param {} node + * @returns + */ + public reportNodeColumnPercentageComposer = R.curry( + (parentNode, node) => { + const parentTotal = parentNode.total.amount; + + return R.compose( + R.when( + this.isNodeHasHorizoTotals, + this.assocColumnPercentageHorizTotals(parentNode) + ), + this.assocReportNodeColumnPercentage(parentTotal) + )(node); + } + ); + + /** + * + * @param node + * @returns + */ + private reportNodeRowPercentageComposer = (node) => { + const total = node.total.amount; + + return R.compose( + R.when( + this.isNodeHasHorizoTotals, + this.assocRowPercentageHorizTotals(total) + ), + this.assocReportNodeRowPercentage(total) + )(node); + }; + + /** + * + */ + private assocNodeColumnPercentageChildren = (node) => { + const children = this.mapNodesDeep( + node.children, + this.reportNodeColumnPercentageComposer(node) + ); + return R.assoc('children', children, node); + }; + + /** + * + * @param node + * @returns + */ + private reportNodeColumnPercentageDeepMap = (node) => { + const parentTotal = node.total.amount; + const parentNode = node; + + return R.compose( + R.when( + this.isNodeHasHorizoTotals, + this.assocColumnPercentageHorizTotals(parentNode) + ), + this.assocReportNodeColumnPercentage(parentTotal), + this.assocNodeColumnPercentageChildren + )(node); + }; + + /** + * + * @param {IBalanceSheetDataNode[]} node + * @returns {IBalanceSheetDataNode[]} + */ + private reportColumnsPercentageMapper = ( + nodes: IBalanceSheetDataNode[] + ): IBalanceSheetDataNode[] => { + return R.map(this.reportNodeColumnPercentageDeepMap, nodes); + }; + + /** + * + * @param nodes + * @returns + */ + private reportRowsPercentageMapper = (nodes) => { + return this.mapNodesDeep(nodes, this.reportNodeRowPercentageComposer); + }; + + /** + * + * @param nodes + * @returns + */ + public reportPercentageCompose = (nodes) => { + return R.compose( + R.when( + this.query.isColumnsPercentageActive, + this.reportColumnsPercentageMapper + ), + R.when( + this.query.isRowsPercentageActive, + this.reportRowsPercentageMapper + ) + )(nodes); + }; + + /** + * Detarmines whether the given node has horizontal total. + * @param {IBalanceSheetDataNode} node + * @returns {boolean} + */ + public isNodeHasHorizoTotals = ( + node: IBalanceSheetDataNode + ): boolean => { + return ( + !R.isEmpty(node.horizontalTotals) && !R.isNil(node.horizontalTotals) + ); + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetQuery.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetQuery.ts new file mode 100644 index 000000000..1a636952a --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetQuery.ts @@ -0,0 +1,182 @@ +import { merge } from 'lodash'; +import * as R from 'ramda'; +import { IBalanceSheetQuery, IFinancialDatePeriodsUnit } from '@/interfaces'; +import { FinancialDateRanges } from '../FinancialDateRanges'; +import { DISPLAY_COLUMNS_BY } from './constants'; + +export class BalanceSheetQuery extends R.compose(FinancialDateRanges)( + class {} +) { + /** + * Balance sheet query. + * @param {IBalanceSheetQuery} + */ + public readonly query: IBalanceSheetQuery; + + /** + * Previous year to date. + * @param {Date} + */ + public readonly PYToDate: Date; + + /** + * Previous year from date. + * @param {Date} + */ + public readonly PYFromDate: Date; + + /** + * Previous period to date. + * @param {Date} + */ + public readonly PPToDate: Date; + + /** + * Previous period from date. + * @param {Date} + */ + public readonly PPFromDate: Date; + + /** + * Constructor method + * @param {IBalanceSheetQuery} query + */ + constructor(query: IBalanceSheetQuery) { + super(); + this.query = query; + + // Pervious Year (PY) Dates. + this.PYToDate = this.getPreviousYearDate(this.query.toDate); + this.PYFromDate = this.getPreviousYearDate(this.query.fromDate); + + // Previous Period (PP) Dates for Total column. + if (this.isTotalColumnType()) { + const { fromDate, toDate } = this.getPPTotalDateRange( + this.query.fromDate, + this.query.toDate + ); + this.PPToDate = toDate; + this.PPFromDate = fromDate; + // Previous Period (PP) Dates for Date period columns type. + } else if (this.isDatePeriodsColumnsType()) { + const { fromDate, toDate } = this.getPPDatePeriodDateRange( + this.query.fromDate, + this.query.toDate, + this.query.displayColumnsBy as IFinancialDatePeriodsUnit + ); + this.PPToDate = toDate; + this.PPFromDate = fromDate; + } + return merge(this, query); + } + + // --------------------------- + // # Columns Type/By. + // --------------------------- + /** + * Detarmines the given display columns type. + * @param {string} displayColumnsBy + * @returns {boolean} + */ + public isDisplayColumnsBy = (displayColumnsBy: string): boolean => { + return this.query.displayColumnsBy === displayColumnsBy; + }; + + /** + * Detarmines the given display columns by type. + * @param {string} displayColumnsBy + * @returns {boolean} + */ + public isDisplayColumnsType = (displayColumnsType: string): boolean => { + return this.query.displayColumnsType === displayColumnsType; + }; + + /** + * Detarmines whether the columns type is date periods. + * @returns {boolean} + */ + public isDatePeriodsColumnsType = (): boolean => { + return this.isDisplayColumnsType(DISPLAY_COLUMNS_BY.DATE_PERIODS); + }; + + /** + * Detarmines whether the columns type is total. + * @returns {boolean} + */ + public isTotalColumnType = (): boolean => { + return this.isDisplayColumnsType(DISPLAY_COLUMNS_BY.TOTAL); + }; + + // --------------------------- + // # Percentage column/row. + // --------------------------- + /** + * Detarmines whether the percentage of column active. + * @returns {boolean} + */ + public isColumnsPercentageActive = (): boolean => { + return this.query.percentageOfColumn; + }; + + /** + * Detarmines whether the percentage of row active. + * @returns {boolean} + */ + public isRowsPercentageActive = (): boolean => { + return this.query.percentageOfRow; + }; + + // --------------------------- + // # Previous Year (PY) + // --------------------------- + /** + * Detarmines the report query has previous year enabled. + * @returns {boolean} + */ + public isPreviousYearActive = (): boolean => { + return this.query.previousYear; + }; + + /** + * Detarmines the report query has previous year percentage change active. + * @returns {boolean} + */ + public isPreviousYearPercentageActive = (): boolean => { + return this.query.previousYearPercentageChange; + }; + + /** + * Detarmines the report query has previous year change active. + * @returns {boolean} + */ + public isPreviousYearChangeActive = (): boolean => { + return this.query.previousYearAmountChange; + }; + + // --------------------------- + // # Previous Period (PP). + // --------------------------- + /** + * Detarmines the report query has previous period enabled. + * @returns {boolean} + */ + public isPreviousPeriodActive = (): boolean => { + return this.query.previousPeriod; + }; + + /** + * Detarmines wether the preivous period percentage is active. + * @returns {boolean} + */ + public isPreviousPeriodPercentageActive = (): boolean => { + return this.query.previousPeriodPercentageChange; + }; + + /** + * Detarmines wether the previous period change is active. + * @returns {boolean} + */ + public isPreviousPeriodChangeActive = (): boolean => { + return this.query.previousPeriodAmountChange; + }; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetRepository.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetRepository.ts new file mode 100644 index 000000000..75a257191 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetRepository.ts @@ -0,0 +1,402 @@ +import { Service } from 'typedi'; +import * as R from 'ramda'; +import { Knex } from 'knex'; +import { isEmpty } from 'lodash'; +import { + IAccountTransactionsGroupBy, + IBalanceSheetQuery, + ILedger, +} from '@/interfaces'; +import { transformToMapBy } from 'utils'; +import Ledger from '@/services/Accounting/Ledger'; +import { BalanceSheetQuery } from './BalanceSheetQuery'; +import { FinancialDatePeriods } from '../FinancialDatePeriods'; +import { BalanceSheetRepositoryNetIncome } from './BalanceSheetRepositoryNetIncome'; + +@Service() +export default class BalanceSheetRepository extends R.compose( + BalanceSheetRepositoryNetIncome, + FinancialDatePeriods +)(class {}) { + /** + * + */ + private readonly models; + + /** + * @param {number} + */ + public readonly tenantId: number; + + /** + * @param {BalanceSheetQuery} + */ + public readonly query: BalanceSheetQuery; + + /** + * @param {} + */ + public accounts: any; + + /** + * @param {} + */ + public accountsGraph: any; + + /** + * + */ + public accountsByType: any; + + /** + * PY from date. + * @param {Date} + */ + public readonly PYFromDate: Date; + + /** + * PY to date. + * @param {Date} + */ + public readonly PYToDate: Date; + + /** + * PP to date. + * @param {Date} + */ + public readonly PPToDate: Date; + + /** + * PP from date. + * @param {Date} + */ + public readonly PPFromDate: Date; + + /** + * Total closing accounts ledger. + * @param {Ledger} + */ + public totalAccountsLedger: Ledger; + + /** + * Total income accounts ledger. + */ + public incomeLedger: Ledger; + + /** + * Total expense accounts ledger. + */ + public expensesLedger: Ledger; + + /** + * Transactions group type. + * @param {IAccountTransactionsGroupBy} + */ + public transactionsGroupType: IAccountTransactionsGroupBy = + IAccountTransactionsGroupBy.Month; + + // ----------------------- + // # Date Periods + // ----------------------- + /** + * @param {Ledger} + */ + public periodsAccountsLedger: Ledger; + + /** + * @param {Ledger} + */ + public periodsOpeningAccountLedger: Ledger; + + // ----------------------- + // # Previous Year (PY). + // ----------------------- + /** + * @param {Ledger} + */ + public PYPeriodsOpeningAccountLedger: Ledger; + + /** + * @param {Ledger} + */ + public PYPeriodsAccountsLedger: Ledger; + + /** + * @param {Ledger} + */ + public PYTotalAccountsLedger: ILedger; + + // ----------------------- + // # Previous Period (PP). + // ----------------------- + /** + * @param {Ledger} + */ + public PPTotalAccountsLedger: Ledger; + + /** + * @param {Ledger} + */ + public PPPeriodsAccountsLedger: ILedger; + + /** + * @param {Ledger} + */ + public PPPeriodsOpeningAccountLedger: ILedger; + + /** + * Constructor method. + * @param {number} tenantId + * @param {IBalanceSheetQuery} query + */ + constructor(models: any, query: IBalanceSheetQuery) { + super(); + + this.query = new BalanceSheetQuery(query); + this.models = models; + + this.transactionsGroupType = this.getGroupByFromDisplayColumnsBy( + this.query.displayColumnsBy + ); + } + + /** + * Async initialize. + * @returns {Promise} + */ + public asyncInitialize = async () => { + await this.initAccounts(); + await this.initAccountsGraph(); + + await this.initAccountsTotalLedger(); + + // Date periods. + if (this.query.isDatePeriodsColumnsType()) { + await this.initTotalDatePeriods(); + } + // Previous Year (PY). + if (this.query.isPreviousYearActive()) { + await this.initTotalPreviousYear(); + } + if ( + this.query.isPreviousYearActive() && + this.query.isDatePeriodsColumnsType() + ) { + await this.initPeriodsPreviousYear(); + } + // Previous Period (PP). + if (this.query.isPreviousPeriodActive()) { + await this.initTotalPreviousPeriod(); + } + if ( + this.query.isPreviousPeriodActive() && + this.query.isDatePeriodsColumnsType() + ) { + await this.initPeriodsPreviousPeriod(); + } + // + await this.asyncInitializeNetIncome(); + }; + + // ---------------------------- + // # Accounts + // ---------------------------- + public initAccounts = async () => { + const accounts = await this.getAccounts(); + + this.accounts = accounts; + this.accountsByType = transformToMapBy(accounts, 'accountType'); + this.accountsByParentType = transformToMapBy(accounts, 'accountParentType'); + }; + + /** + * Initialize accounts graph. + */ + public initAccountsGraph = async () => { + const { Account } = this.models; + + this.accountsGraph = Account.toDependencyGraph(this.accounts); + }; + + // ---------------------------- + // # Closing Total + // ---------------------------- + /** + * Initialize accounts closing total based on the given query. + * @returns {Promise} + */ + private initAccountsTotalLedger = async (): Promise => { + const totalByAccount = await this.closingAccountsTotal(this.query.toDate); + + // Inject to the repository. + this.totalAccountsLedger = Ledger.fromTransactions(totalByAccount); + }; + + // ---------------------------- + // # Date periods. + // ---------------------------- + /** + * Initialize date periods total. + * @returns {Promise} + */ + public initTotalDatePeriods = async (): Promise => { + // Retrieves grouped transactions by given date group. + const periodsByAccount = await this.accountsDatePeriods( + this.query.fromDate, + this.query.toDate, + this.transactionsGroupType + ); + // Retrieves opening balance of grouped transactions. + const periodsOpeningByAccount = await this.closingAccountsTotal( + this.query.fromDate + ); + // Inject to the repository. + this.periodsAccountsLedger = Ledger.fromTransactions(periodsByAccount); + this.periodsOpeningAccountLedger = Ledger.fromTransactions( + periodsOpeningByAccount + ); + }; + + // ---------------------------- + // # Previous Year (PY). + // ---------------------------- + /** + * Initialize total of previous year. + * @returns {Promise} + */ + private initTotalPreviousYear = async (): Promise => { + const PYTotalsByAccounts = await this.closingAccountsTotal( + this.query.PYToDate + ); + // Inject to the repository. + this.PYTotalAccountsLedger = Ledger.fromTransactions(PYTotalsByAccounts); + }; + + /** + * Initialize date periods of previous year. + * @returns {Promise} + */ + private initPeriodsPreviousYear = async (): Promise => { + const PYPeriodsBYAccounts = await this.accountsDatePeriods( + this.query.PYFromDate, + this.query.PYToDate, + this.transactionsGroupType + ); + // Retrieves opening balance of grouped transactions. + const periodsOpeningByAccount = await this.closingAccountsTotal( + this.query.PYFromDate + ); + // Inject to the repository. + this.PYPeriodsAccountsLedger = Ledger.fromTransactions(PYPeriodsBYAccounts); + this.PYPeriodsOpeningAccountLedger = Ledger.fromTransactions( + periodsOpeningByAccount + ); + }; + + // ---------------------------- + // # Previous Year (PP). + // ---------------------------- + /** + * Initialize total of previous year. + * @returns {Promise} + */ + private initTotalPreviousPeriod = async (): Promise => { + const PPTotalsByAccounts = await this.closingAccountsTotal( + this.query.PPToDate + ); + // Inject to the repository. + this.PPTotalAccountsLedger = Ledger.fromTransactions(PPTotalsByAccounts); + }; + + /** + * Initialize date periods of previous year. + * @returns {Promise} + */ + private initPeriodsPreviousPeriod = async (): Promise => { + const PPPeriodsBYAccounts = await this.accountsDatePeriods( + this.query.PPFromDate, + this.query.PPToDate, + this.transactionsGroupType + ); + // Retrieves opening balance of grouped transactions. + const periodsOpeningByAccount = await this.closingAccountsTotal( + this.query.PPFromDate + ); + // Inject to the repository. + this.PPPeriodsAccountsLedger = Ledger.fromTransactions(PPPeriodsBYAccounts); + this.PPPeriodsOpeningAccountLedger = Ledger.fromTransactions( + periodsOpeningByAccount + ); + }; + + // ---------------------------- + // # Utils + // ---------------------------- + /** + * Retrieve accounts of the report. + * @return {Promise} + */ + private getAccounts = () => { + const { Account } = this.models; + + return Account.query(); + }; + + /** + * Closing accounts date periods. + * @param {Date} fromDate + * @param {Date} toDate + * @param {string} datePeriodsType + * @returns + */ + public accountsDatePeriods = async ( + fromDate: Date, + toDate: Date, + datePeriodsType: 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('groupByDateFormat', datePeriodsType); + query.modify('filterDateRange', fromDate, toDate); + query.withGraphFetched('account'); + + this.commonFilterBranchesQuery(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-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetRepositoryNetIncome.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetRepositoryNetIncome.ts new file mode 100644 index 000000000..e551dbb49 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetRepositoryNetIncome.ts @@ -0,0 +1,227 @@ +import * as R from 'ramda'; +import { FinancialDatePeriods } from '../../common/FinancialDatePeriods'; +import { ModelObject } from 'objection'; +import { Account } from '@/modules/Accounts/models/Account.model'; +import { ILedger } from '@/modules/Ledger/types/Ledger.types'; +import { ACCOUNT_PARENT_TYPE } from '@/constants/accounts'; +import { Constructor } from '@/common/types/Constructor'; + +export const BalanceSheetRepositoryNetIncome = ( + Base: T, +) => + class extends R.compose(FinancialDatePeriods)(Base) { + // ----------------------- + // # Net Income + // ----------------------- + public incomeAccounts: ModelObject[]; + public incomeAccountsIds: number[]; + + public expenseAccounts: ModelObject[]; + public expenseAccountsIds: number[]; + + public incomePeriodsAccountsLedger: ILedger; + public incomePeriodsOpeningAccountsLedger: ILedger; + public expensesPeriodsAccountsLedger: ILedger; + public expensesOpeningAccountLedger: ILedger; + + public incomePPAccountsLedger: ILedger; + public expensePPAccountsLedger: ILedger; + + public incomePPPeriodsAccountsLedger: ILedger; + public incomePPPeriodsOpeningAccountLedger: ILedger; + public expensePPPeriodsAccountsLedger: ILedger; + public expensePPPeriodsOpeningAccountLedger: ILedger; + + public incomePYTotalAccountsLedger: ILedger; + public expensePYTotalAccountsLedger: ILedger; + + public incomePYPeriodsAccountsLedger: ILedger; + public incomePYPeriodsOpeningAccountLedger: ILedger; + public expensePYPeriodsAccountsLedger: ILedger; + public expensePYPeriodsOpeningAccountLedger: ILedger; + + /** + * Async initialize. + * @returns {Promise} + */ + public asyncInitializeNetIncome = async () => { + await this.initAccounts(); + await this.initAccountsTotalLedger(); + + // Net Income + this.initIncomeAccounts(); + this.initExpenseAccounts(); + + this.initIncomeTotalLedger(); + this.initExpensesTotalLedger(); + + // Date periods + if (this.query.isDatePeriodsColumnsType()) { + this.initNetIncomeDatePeriods(); + } + // Previous Year (PY). + if (this.query.isPreviousYearActive()) { + this.initNetIncomePreviousYear(); + } + // Previous Period (PP). + if (this.query.isPreviousPeriodActive()) { + this.initNetIncomePreviousPeriod(); + } + // Previous Year (PY) / Date Periods. + if ( + this.query.isPreviousYearActive() && + this.query.isDatePeriodsColumnsType() + ) { + this.initNetIncomePeriodsPreviewYear(); + } + // Previous Period (PP) / Date Periods. + if ( + this.query.isPreviousPeriodActive() && + this.query.isDatePeriodsColumnsType() + ) { + this.initNetIncomePeriodsPreviousPeriod(); + } + }; + + // ---------------------------- + // # Net Income + // ---------------------------- + /** + * Initialize income accounts. + */ + private initIncomeAccounts = () => { + const incomeAccounts = this.accountsByParentType.get( + ACCOUNT_PARENT_TYPE.INCOME, + ); + const incomeAccountsIds = incomeAccounts.map((a) => a.id); + + this.incomeAccounts = incomeAccounts; + this.incomeAccountsIds = incomeAccountsIds; + }; + + /** + * Initialize expense accounts. + */ + private initExpenseAccounts = () => { + const expensesAccounts = this.accountsByParentType.get( + ACCOUNT_PARENT_TYPE.EXPENSE, + ); + const expensesAccountsIds = expensesAccounts.map((a) => a.id); + + this.expenseAccounts = expensesAccounts; + this.expenseAccountsIds = expensesAccountsIds; + }; + + /** + * Initialize the income total ledger. + */ + private initIncomeTotalLedger = (): void => { + // Inject to the repository. + this.incomeLedger = this.totalAccountsLedger.whereAccountsIds( + this.incomeAccountsIds, + ); + }; + + /** + * Initialize the expenses total ledger. + */ + private initExpensesTotalLedger = (): void => { + this.expensesLedger = this.totalAccountsLedger.whereAccountsIds( + this.expenseAccountsIds, + ); + }; + + // ---------------------------- + // # Net Income - Date Periods + // ---------------------------- + /** + * Initialize the net income date periods. + */ + public initNetIncomeDatePeriods = () => { + this.incomePeriodsAccountsLedger = + this.periodsAccountsLedger.whereAccountsIds(this.incomeAccountsIds); + + this.incomePeriodsOpeningAccountsLedger = + this.periodsOpeningAccountLedger.whereAccountsIds( + this.incomeAccountsIds, + ); + + this.expensesPeriodsAccountsLedger = + this.periodsAccountsLedger.whereAccountsIds(this.expenseAccountsIds); + + this.expensesOpeningAccountLedger = + this.periodsOpeningAccountLedger.whereAccountsIds( + this.expenseAccountsIds, + ); + }; + + // ---------------------------- + // # Net Income - Previous Period + // ---------------------------- + /** + * Initialize the total net income PP. + */ + public initNetIncomePreviousPeriod = () => { + this.incomePPAccountsLedger = this.PPTotalAccountsLedger.whereAccountsIds( + this.incomeAccountsIds, + ); + this.expensePPAccountsLedger = + this.PPTotalAccountsLedger.whereAccountsIds(this.expenseAccountsIds); + }; + + /** + * Initialize the net income periods of previous period. + */ + public initNetIncomePeriodsPreviousPeriod = () => { + this.incomePPPeriodsAccountsLedger = + this.PPPeriodsAccountsLedger.whereAccountsIds(this.incomeAccountsIds); + + this.incomePPPeriodsOpeningAccountLedger = + this.PPPeriodsOpeningAccountLedger.whereAccountsIds( + this.incomeAccountsIds, + ); + + this.expensePPPeriodsAccountsLedger = + this.PPPeriodsAccountsLedger.whereAccountsIds(this.expenseAccountsIds); + + this.expensePPPeriodsOpeningAccountLedger = + this.PPPeriodsOpeningAccountLedger.whereAccountsIds( + this.expenseAccountsIds, + ); + }; + + // ---------------------------- + // # Net Income - Previous Year + // ---------------------------- + /** + * Initialize the net income PY total. + */ + public initNetIncomePreviousYear = () => { + this.incomePYTotalAccountsLedger = + this.PYTotalAccountsLedger.whereAccountsIds(this.incomeAccountsIds); + + this.expensePYTotalAccountsLedger = + this.PYTotalAccountsLedger.whereAccountsIds(this.expenseAccountsIds); + }; + + /** + * Initialize the net income PY periods. + */ + public initNetIncomePeriodsPreviewYear = () => { + this.incomePYPeriodsAccountsLedger = + this.PYPeriodsAccountsLedger.whereAccountsIds(this.incomeAccountsIds); + + this.incomePYPeriodsOpeningAccountLedger = + this.PYPeriodsOpeningAccountLedger.whereAccountsIds( + this.incomeAccountsIds, + ); + + this.expensePYPeriodsAccountsLedger = + this.PYPeriodsAccountsLedger.whereAccountsIds(this.expenseAccountsIds); + + this.expensePYPeriodsOpeningAccountLedger = + this.PYPeriodsOpeningAccountLedger.whereAccountsIds( + this.expenseAccountsIds, + ); + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetSchema.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetSchema.ts new file mode 100644 index 000000000..78cba2d35 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetSchema.ts @@ -0,0 +1,129 @@ +/* eslint-disable import/prefer-default-export */ +import * as R from 'ramda'; +import { + BALANCE_SHEET_SCHEMA_NODE_ID, + BALANCE_SHEET_SCHEMA_NODE_TYPE, +} from './BalanceSheet.types'; +import { FinancialSchema } from '../../common/FinancialSchema'; +import { Constructor } from '@/common/types/Constructor'; +import { ACCOUNT_TYPE } from '@/constants/accounts'; + +export const BalanceSheetSchema = (Base: T) => + class extends R.compose(FinancialSchema)(Base) { + /** + * Retrieves the balance sheet schema. + * @returns + */ + getSchema = () => { + return getBalanceSheetSchema(); + }; + }; + +/** + * Retrieve the balance sheet report schema. + */ +export const getBalanceSheetSchema = () => [ + { + name: 'balance_sheet.assets', + id: BALANCE_SHEET_SCHEMA_NODE_ID.ASSETS, + type: BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE, + children: [ + { + name: 'balance_sheet.current_asset', + id: BALANCE_SHEET_SCHEMA_NODE_ID.CURRENT_ASSETS, + type: BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE, + children: [ + { + name: 'balance_sheet.cash_and_cash_equivalents', + id: BALANCE_SHEET_SCHEMA_NODE_ID.CASH_EQUIVALENTS, + type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS, + accountsTypes: [ACCOUNT_TYPE.CASH, ACCOUNT_TYPE.BANK], + }, + { + name: 'balance_sheet.accounts_receivable', + id: BALANCE_SHEET_SCHEMA_NODE_ID.ACCOUNTS_RECEIVABLE, + type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS, + accountsTypes: [ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE], + }, + { + name: 'balance_sheet.inventory', + id: BALANCE_SHEET_SCHEMA_NODE_ID.INVENTORY, + type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS, + accountsTypes: [ACCOUNT_TYPE.INVENTORY], + }, + { + name: 'balance_sheet.other_current_assets', + id: BALANCE_SHEET_SCHEMA_NODE_ID.OTHER_CURRENT_ASSET, + type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS, + accountsTypes: [ACCOUNT_TYPE.OTHER_CURRENT_ASSET], + }, + ], + alwaysShow: true, + }, + { + name: 'balance_sheet.fixed_asset', + id: BALANCE_SHEET_SCHEMA_NODE_ID.FIXED_ASSET, + type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS, + accountsTypes: [ACCOUNT_TYPE.FIXED_ASSET], + }, + { + name: 'balance_sheet.non_current_assets', + id: BALANCE_SHEET_SCHEMA_NODE_ID.NON_CURRENT_ASSET, + type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS, + accountsTypes: [ACCOUNT_TYPE.NON_CURRENT_ASSET], + }, + ], + alwaysShow: true, + }, + { + name: 'balance_sheet.liabilities_and_equity', + id: BALANCE_SHEET_SCHEMA_NODE_ID.LIABILITY_EQUITY, + type: BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE, + children: [ + { + name: 'balance_sheet.liabilities', + id: BALANCE_SHEET_SCHEMA_NODE_ID.LIABILITY, + type: BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE, + children: [ + { + name: 'balance_sheet.current_liabilties', + id: BALANCE_SHEET_SCHEMA_NODE_ID.CURRENT_LIABILITY, + type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS, + accountsTypes: [ + ACCOUNT_TYPE.ACCOUNTS_PAYABLE, + ACCOUNT_TYPE.TAX_PAYABLE, + ACCOUNT_TYPE.CREDIT_CARD, + ACCOUNT_TYPE.OTHER_CURRENT_LIABILITY, + ], + }, + { + name: 'balance_sheet.long_term_liabilities', + id: BALANCE_SHEET_SCHEMA_NODE_ID.LOGN_TERM_LIABILITY, + type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS, + accountsTypes: [ACCOUNT_TYPE.LOGN_TERM_LIABILITY], + }, + { + name: 'balance_sheet.non_current_liabilities', + id: BALANCE_SHEET_SCHEMA_NODE_ID.NON_CURRENT_LIABILITY, + type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS, + accountsTypes: [ACCOUNT_TYPE.NON_CURRENT_LIABILITY], + }, + ], + }, + { + name: 'balance_sheet.equity', + id: BALANCE_SHEET_SCHEMA_NODE_ID.EQUITY, + type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS, + accountsTypes: [ACCOUNT_TYPE.EQUITY], + children: [ + { + name: 'balance_sheet.net_income', + id: BALANCE_SHEET_SCHEMA_NODE_ID.NET_INCOME, + type: BALANCE_SHEET_SCHEMA_NODE_TYPE.NET_INCOME, + }, + ], + }, + ], + alwaysShow: true, + }, +]; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetTable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetTable.ts new file mode 100644 index 000000000..1190f3c05 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetTable.ts @@ -0,0 +1,278 @@ +import * as R from 'ramda'; +import { + IBalanceSheetStatementData, + ITableColumnAccessor, + IBalanceSheetQuery, + ITableColumn, + ITableRow, + BALANCE_SHEET_SCHEMA_NODE_TYPE, + IBalanceSheetDataNode, + IBalanceSheetSchemaNode, + IBalanceSheetNetIncomeNode, + IBalanceSheetAccountNode, + IBalanceSheetAccountsNode, + IBalanceSheetAggregateNode, +} from '@/interfaces'; +import { tableRowMapper } from 'utils'; +import FinancialSheet from '../FinancialSheet'; +import { BalanceSheetComparsionPreviousYear } from './BalanceSheetComparsionPreviousYear'; +import { IROW_TYPE, DISPLAY_COLUMNS_BY } from './constants'; +import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod'; +import { BalanceSheetPercentage } from './BalanceSheetPercentage'; +import { FinancialSheetStructure } from '../FinancialSheetStructure'; +import { BalanceSheetBase } from './BalanceSheetBase'; +import { BalanceSheetTablePercentage } from './BalanceSheetTablePercentage'; +import { BalanceSheetTablePreviousYear } from './BalanceSheetTablePreviousYear'; +import { BalanceSheetTablePreviousPeriod } from './BalanceSheetTablePreviousPeriod'; +import { FinancialTable } from '../FinancialTable'; +import { BalanceSheetQuery } from './BalanceSheetQuery'; +import { BalanceSheetTableDatePeriods } from './BalanceSheetTableDatePeriods'; + +export class BalanceSheetTable extends R.compose( + BalanceSheetTablePreviousPeriod, + BalanceSheetTablePreviousYear, + BalanceSheetTableDatePeriods, + BalanceSheetTablePercentage, + BalanceSheetComparsionPreviousYear, + BalanceSheetComparsionPreviousPeriod, + BalanceSheetPercentage, + FinancialSheetStructure, + FinancialTable, + BalanceSheetBase +)(FinancialSheet) { + /** + * Balance sheet data. + * @param {IBalanceSheetStatementData} + */ + private reportData: IBalanceSheetStatementData; + + /** + * Balance sheet query. + * @parma {BalanceSheetQuery} + */ + private query: BalanceSheetQuery; + + /** + * Constructor method. + * @param {IBalanceSheetStatementData} reportData - + * @param {IBalanceSheetQuery} query - + */ + constructor( + reportData: IBalanceSheetStatementData, + query: IBalanceSheetQuery, + i18n: any + ) { + super(); + + this.reportData = reportData; + this.query = new BalanceSheetQuery(query); + this.i18n = i18n; + } + + /** + * Detarmines the node type of the given schema node. + * @param {IBalanceSheetStructureSection} node - + * @param {string} type - + * @return {boolean} + */ + protected isNodeType = R.curry( + (type: string, node: IBalanceSheetSchemaNode): boolean => { + return node.nodeType === type; + } + ); + + // ------------------------- + // # Accessors. + // ------------------------- + /** + * Retrieve the common columns for all report nodes. + * @param {ITableColumnAccessor[]} + */ + private commonColumnsAccessors = (): ITableColumnAccessor[] => { + return R.compose( + R.concat([{ key: 'name', accessor: 'name' }]), + R.ifElse( + R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)), + R.concat(this.datePeriodsColumnsAccessors()), + R.concat(this.totalColumnAccessor()) + ) + )([]); + }; + + /** + * Retrieve the total column accessor. + * @return {ITableColumnAccessor[]} + */ + private totalColumnAccessor = (): ITableColumnAccessor[] => { + return R.pipe( + R.concat(this.previousPeriodColumnAccessor()), + R.concat(this.previousYearColumnAccessor()), + R.concat(this.percentageColumnsAccessor()), + R.concat([{ key: 'total', accessor: 'total.formattedAmount' }]) + )([]); + }; + + /** + * Retrieves the table row from the given report aggregate node. + * @param {IBalanceSheetAggregateNode} node + * @returns {ITableRow} + */ + private aggregateNodeTableRowsMapper = ( + node: IBalanceSheetAggregateNode + ): ITableRow => { + const columns = this.commonColumnsAccessors(); + const meta = { + rowTypes: [IROW_TYPE.AGGREGATE], + id: node.id, + }; + return tableRowMapper(node, columns, meta); + }; + + /** + * Retrieves the table row from the given report accounts node. + * @param {IBalanceSheetAccountsNode} node + * @returns {ITableRow} + */ + private accountsNodeTableRowsMapper = ( + node: IBalanceSheetAccountsNode + ): ITableRow => { + const columns = this.commonColumnsAccessors(); + const meta = { + rowTypes: [IROW_TYPE.ACCOUNTS], + id: node.id, + }; + return tableRowMapper(node, columns, meta); + }; + + /** + * Retrieves the table row from the given report account node. + * @param {IBalanceSheetAccountNode} node + * @returns {ITableRow} + */ + private accountNodeTableRowsMapper = ( + node: IBalanceSheetAccountNode + ): ITableRow => { + const columns = this.commonColumnsAccessors(); + + const meta = { + rowTypes: [IROW_TYPE.ACCOUNT], + id: node.id, + }; + return tableRowMapper(node, columns, meta); + }; + + /** + * Retrieves the table row from the given report net income node. + * @param {IBalanceSheetNetIncomeNode} node + * @returns {ITableRow} + */ + private netIncomeNodeTableRowsMapper = ( + node: IBalanceSheetNetIncomeNode + ): ITableRow => { + const columns = this.commonColumnsAccessors(); + const meta = { + rowTypes: [IROW_TYPE.NET_INCOME], + id: node.id, + }; + return tableRowMapper(node, columns, meta); + }; + + /** + * Mappes the given report node to table rows. + * @param {IBalanceSheetDataNode} node - + * @returns {ITableRow} + */ + private nodeToTableRowsMapper = (node: IBalanceSheetDataNode): ITableRow => { + return R.cond([ + [ + this.isNodeType(BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE), + this.aggregateNodeTableRowsMapper, + ], + [ + this.isNodeType(BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS), + this.accountsNodeTableRowsMapper, + ], + [ + this.isNodeType(BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNT), + this.accountNodeTableRowsMapper, + ], + [ + this.isNodeType(BALANCE_SHEET_SCHEMA_NODE_TYPE.NET_INCOME), + this.netIncomeNodeTableRowsMapper, + ], + ])(node); + }; + + /** + * Mappes the given report sections to table rows. + * @param {IBalanceSheetDataNode[]} nodes - + * @return {ITableRow} + */ + private nodesToTableRowsMapper = ( + nodes: IBalanceSheetDataNode[] + ): ITableRow[] => { + return this.mapNodesDeep(nodes, this.nodeToTableRowsMapper); + }; + + /** + * Retrieves the total children columns. + * @returns {ITableColumn[]} + */ + private totalColumnChildren = (): ITableColumn[] => { + return R.compose( + R.unless( + R.isEmpty, + R.concat([{ key: 'total', label: this.i18n.__('balance_sheet.total') }]) + ), + R.concat(this.percentageColumns()), + R.concat(this.getPreviousYearColumns()), + R.concat(this.previousPeriodColumns()) + )([]); + }; + + /** + * Retrieve the total column. + * @returns {ITableColumn[]} + */ + private totalColumn = (): ITableColumn[] => { + return [ + { + key: 'total', + label: this.i18n.__('balance_sheet.total'), + children: this.totalColumnChildren(), + }, + ]; + }; + + /** + * Retrieve the report table rows. + * @returns {ITableRow[]} + */ + public tableRows = (): ITableRow[] => { + return R.compose( + this.addTotalRows, + this.nodesToTableRowsMapper + )(this.reportData); + }; + + // ------------------------- + // # Columns. + // ------------------------- + /** + * Retrieve the report table columns. + * @returns {ITableColumn[]} + */ + public tableColumns = (): ITableColumn[] => { + return R.compose( + this.tableColumnsCellIndexing, + R.concat([ + { key: 'name', label: this.i18n.__('balance_sheet.account_name') }, + ]), + R.ifElse( + this.query.isDatePeriodsColumnsType, + R.concat(this.datePeriodsColumns()), + R.concat(this.totalColumn()) + ) + )([]); + }; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetTableDatePeriods.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetTableDatePeriods.ts new file mode 100644 index 000000000..3e195aa24 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetTableDatePeriods.ts @@ -0,0 +1,137 @@ +import * as R from 'ramda'; +import moment from 'moment'; +import { + ITableColumn, + IDateRange, + ICashFlowDateRange, + ITableColumnAccessor, +} from '@/interfaces'; +import { FinancialDatePeriods } from '../FinancialDatePeriods'; + +export const BalanceSheetTableDatePeriods = (Base) => + class extends R.compose(FinancialDatePeriods)(Base) { + /** + * Retrieves the date periods based on the report query. + * @returns {IDateRange[]} + */ + get datePeriods() { + return this.getDateRanges( + this.query.fromDate, + this.query.toDate, + this.query.displayColumnsBy + ); + } + + /** + * Retrieve the formatted column label from the given date range. + * @param {ICashFlowDateRange} dateRange - + * @return {string} + */ + private formatColumnLabel = (dateRange: ICashFlowDateRange) => { + const monthFormat = (range) => moment(range.toDate).format('YYYY-MM'); + const yearFormat = (range) => moment(range.toDate).format('YYYY'); + const dayFormat = (range) => moment(range.toDate).format('YYYY-MM-DD'); + + const conditions = [ + ['month', monthFormat], + ['year', yearFormat], + ['day', dayFormat], + ['quarter', monthFormat], + ['week', dayFormat], + ]; + const conditionsPairs = R.map( + ([type, formatFn]) => [ + R.always(this.query.isDisplayColumnsBy(type)), + formatFn, + ], + conditions + ); + return R.compose(R.cond(conditionsPairs))(dateRange); + }; + + // ------------------------- + // # Accessors. + // ------------------------- + /** + * Date period columns accessor. + * @param {IDateRange} dateRange - + * @param {number} index - + */ + private datePeriodColumnsAccessor = R.curry( + (dateRange: IDateRange, index: number) => { + return R.pipe( + R.concat(this.previousPeriodHorizColumnAccessors(index)), + R.concat(this.previousYearHorizontalColumnAccessors(index)), + R.concat(this.percetangeDatePeriodColumnsAccessor(index)), + R.concat([ + { + key: `date-range-${index}`, + accessor: `horizontalTotals[${index}].total.formattedAmount`, + }, + ]) + )([]); + } + ); + + /** + * Retrieve the date periods columns accessors. + * @returns {ITableColumnAccessor[]} + */ + protected datePeriodsColumnsAccessors = (): ITableColumnAccessor[] => { + return R.compose( + R.flatten, + R.addIndex(R.map)(this.datePeriodColumnsAccessor) + )(this.datePeriods); + }; + + // ------------------------- + // # Columns. + // ------------------------- + /** + * + * @param {number} index + * @param {} dateRange + * @returns {} + */ + private datePeriodChildrenColumns = ( + index: number, + dateRange: IDateRange + ) => { + return R.compose( + R.unless( + R.isEmpty, + R.concat([ + { key: `total`, label: this.i18n.__('balance_sheet.total') }, + ]) + ), + R.concat(this.percentageColumns()), + R.concat(this.getPreviousYearHorizontalColumns(dateRange)), + R.concat(this.previousPeriodHorizontalColumns(dateRange)) + )([]); + }; + + /** + * + * @param dateRange + * @param index + * @returns + */ + private datePeriodColumn = ( + dateRange: IDateRange, + index: number + ): ITableColumn => { + return { + key: `date-range-${index}`, + label: this.formatColumnLabel(dateRange), + children: this.datePeriodChildrenColumns(index, dateRange), + }; + }; + + /** + * Date periods columns. + * @returns {ITableColumn[]} + */ + protected datePeriodsColumns = (): ITableColumn[] => { + return this.datePeriods.map(this.datePeriodColumn); + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetTableInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetTableInjectable.ts new file mode 100644 index 000000000..48b33bbed --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetTableInjectable.ts @@ -0,0 +1,35 @@ +import { BalanceSheetInjectable } from './BalanceSheetInjectable'; +import { BalanceSheetTable } from './BalanceSheetTable'; +import { IBalanceSheetQuery, IBalanceSheetTable } from './BalanceSheet.types'; +import { Injectable } from '@nestjs/common'; +import { I18nService } from 'nestjs-i18n'; + +@Injectable() +export class BalanceSheetTableInjectable { + constructor( + private readonly balanceSheetService: BalanceSheetInjectable, + private readonly i18nService: I18nService, + ) {} + + /** + * Retrieves the balance sheet in table format. + * @param {number} tenantId + * @param {number} query + * @returns {Promise} + */ + public async table(filter: IBalanceSheetQuery): Promise { + const { data, query, meta } = + await this.balanceSheetService.balanceSheet(filter); + + const table = new BalanceSheetTable(data, query, this.i18nService); + + return { + table: { + columns: table.tableColumns(), + rows: table.tableRows(), + }, + query, + meta, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetTablePercentage.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetTablePercentage.ts new file mode 100644 index 000000000..a7d3f82c4 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetTablePercentage.ts @@ -0,0 +1,83 @@ +import * as R from 'ramda'; +import { ITableColumn } from '@/interfaces'; + +export const BalanceSheetTablePercentage = (Base) => + class extends Base { + // -------------------- + // # Columns + // -------------------- + /** + * Retrieve percentage of column/row columns. + * @returns {ITableColumn[]} + */ + protected percentageColumns = (): ITableColumn[] => { + return R.pipe( + R.when( + this.query.isColumnsPercentageActive, + R.append({ + key: 'percentage_of_column', + label: this.i18n.__('balance_sheet.percentage_of_column'), + }) + ), + R.when( + this.query.isRowsPercentageActive, + R.append({ + key: 'percentage_of_row', + label: this.i18n.__('balance_sheet.percentage_of_row'), + }) + ) + )([]); + }; + + // -------------------- + // # Accessors + // -------------------- + /** + * Retrieves percentage of column/row accessors. + * @returns {ITableColumn[]} + */ + protected percentageColumnsAccessor = (): ITableColumn[] => { + return R.pipe( + R.when( + this.query.isColumnsPercentageActive, + R.append({ + key: 'percentage_of_column', + accessor: 'percentageColumn.formattedAmount', + }) + ), + R.when( + this.query.isRowsPercentageActive, + R.append({ + key: 'percentage_of_row', + accessor: 'percentageRow.formattedAmount', + }) + ) + )([]); + }; + + /** + * Percentage columns accessors for date period columns. + * @param {number} index + * @returns {ITableColumn[]} + */ + protected percetangeDatePeriodColumnsAccessor = ( + index: number + ): ITableColumn[] => { + return R.pipe( + R.when( + this.query.isColumnsPercentageActive, + R.append({ + key: `percentage_of_column-${index}`, + accessor: `horizontalTotals[${index}].percentageColumn.formattedAmount`, + }) + ), + R.when( + this.query.isRowsPercentageActive, + R.append({ + key: `percentage_of_row-${index}`, + accessor: `horizontalTotals[${index}].percentageRow.formattedAmount`, + }) + ) + )([]); + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetTablePreviousPeriod.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetTablePreviousPeriod.ts new file mode 100644 index 000000000..1541df9c2 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetTablePreviousPeriod.ts @@ -0,0 +1,109 @@ +import * as R from 'ramda'; +import { IDateRange, ITableColumn } from '@/interfaces'; +import { BalanceSheetQuery } from './BalanceSheetQuery'; +import { FinancialTablePreviousPeriod } from '../FinancialTablePreviousPeriod'; +import { FinancialDateRanges } from '../FinancialDateRanges'; + +export const BalanceSheetTablePreviousPeriod = (Base) => + class extends R.compose( + FinancialTablePreviousPeriod, + FinancialDateRanges + )(Base) { + readonly query: BalanceSheetQuery; + + // -------------------- + // # Columns + // -------------------- + /** + * Retrieves the previous period columns. + * @returns {ITableColumn[]} + */ + protected previousPeriodColumns = ( + dateRange?: IDateRange + ): ITableColumn[] => { + return R.pipe( + // Previous period columns. + R.when( + this.query.isPreviousPeriodActive, + R.append(this.getPreviousPeriodTotalColumn(dateRange)) + ), + R.when( + this.query.isPreviousPeriodChangeActive, + R.append(this.getPreviousPeriodChangeColumn()) + ), + R.when( + this.query.isPreviousPeriodPercentageActive, + R.append(this.getPreviousPeriodPercentageColumn()) + ) + )([]); + }; + + /** + * Previous period for date periods + * @param {IDateRange} dateRange + * @returns {ITableColumn} + */ + protected previousPeriodHorizontalColumns = ( + dateRange: IDateRange + ): ITableColumn[] => { + const PPDateRange = this.getPPDatePeriodDateRange( + dateRange.fromDate, + dateRange.toDate, + this.query.displayColumnsBy + ); + return this.previousPeriodColumns({ + fromDate: PPDateRange.fromDate, + toDate: PPDateRange.toDate, + }); + }; + + // -------------------- + // # Accessors + // -------------------- + /** + * Retrieves previous period columns accessors. + * @returns {ITableColumn[]} + */ + protected previousPeriodColumnAccessor = (): ITableColumn[] => { + return R.pipe( + // Previous period columns. + R.when( + this.query.isPreviousPeriodActive, + R.append(this.getPreviousPeriodTotalAccessor()) + ), + R.when( + this.query.isPreviousPeriodChangeActive, + R.append(this.getPreviousPeriodChangeAccessor()) + ), + R.when( + this.query.isPreviousPeriodPercentageActive, + R.append(this.getPreviousPeriodPercentageAccessor()) + ) + )([]); + }; + + /** + * + * @param {number} index + * @returns + */ + protected previousPeriodHorizColumnAccessors = ( + index: number + ): ITableColumn[] => { + return R.pipe( + // Previous period columns. + R.when( + this.query.isPreviousPeriodActive, + R.append(this.getPreviousPeriodTotalHorizAccessor(index)) + ), + R.when( + this.query.isPreviousPeriodChangeActive, + R.append(this.getPreviousPeriodChangeHorizAccessor(index)) + ), + R.when( + this.query.isPreviousPeriodPercentageActive, + R.append(this.getPreviousPeriodPercentageHorizAccessor(index)) + ) + )([]); + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetTablePreviousYear.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetTablePreviousYear.ts new file mode 100644 index 000000000..f5f9e329b --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetTablePreviousYear.ts @@ -0,0 +1,99 @@ +import * as R from 'ramda'; +import { IDateRange, } from '../../types/Report.types'; +import { ITableColumn } from '../../types/Table.types'; +import { FinancialTablePreviousYear } from '../../common/FinancialTablePreviousYear'; +import { FinancialDateRanges } from '../../common/FinancialDateRanges'; +import { Constructor } from '@/common/types/Constructor'; + +export const BalanceSheetTablePreviousYear = (Base: T) => + class extends R.compose(FinancialTablePreviousYear, FinancialDateRanges)(Base) { + // -------------------- + // # Columns. + // -------------------- + /** + * Retrieves pervious year comparison columns. + * @returns {ITableColumn[]} + */ + public getPreviousYearColumns = ( + dateRange?: IDateRange + ): ITableColumn[] => { + return R.pipe( + // Previous year columns. + R.when( + this.query.isPreviousYearActive, + R.append(this.getPreviousYearTotalColumn(dateRange)) + ), + R.when( + this.query.isPreviousYearChangeActive, + R.append(this.getPreviousYearChangeColumn()) + ), + R.when( + this.query.isPreviousYearPercentageActive, + R.append(this.getPreviousYearPercentageColumn()) + ) + )([]); + }; + + /** + * + * @param {IDateRange} dateRange + * @returns + */ + public getPreviousYearHorizontalColumns = (dateRange: IDateRange) => { + const PYDateRange = this.getPreviousYearDateRange( + dateRange.fromDate, + dateRange.toDate + ); + return this.getPreviousYearColumns(PYDateRange); + }; + + // -------------------- + // # Accessors. + // -------------------- + /** + * Retrieves previous year columns accessors. + * @returns {ITableColumn[]} + */ + public previousYearColumnAccessor = (): ITableColumn[] => { + return R.pipe( + // Previous year columns. + R.when( + this.query.isPreviousYearActive, + R.append(this.getPreviousYearTotalAccessor()) + ), + R.when( + this.query.isPreviousYearChangeActive, + R.append(this.getPreviousYearChangeAccessor()) + ), + R.when( + this.query.isPreviousYearPercentageActive, + R.append(this.getPreviousYearPercentageAccessor()) + ) + )([]); + }; + + /** + * Previous year period column accessor. + * @param {number} index + * @returns {ITableColumn[]} + */ + public previousYearHorizontalColumnAccessors = ( + index: number + ): ITableColumn[] => { + return R.pipe( + // Previous year columns. + R.when( + this.query.isPreviousYearActive, + R.append(this.getPreviousYearTotalHorizAccessor(index)) + ), + R.when( + this.query.isPreviousYearChangeActive, + R.append(this.getPreviousYearChangeHorizAccessor(index)) + ), + R.when( + this.query.isPreviousYearPercentageActive, + R.append(this.getPreviousYearPercentageHorizAccessor(index)) + ) + )([]); + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetTotal.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetTotal.ts new file mode 100644 index 000000000..c04fd6d7b --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/BalanceSheetTotal.ts @@ -0,0 +1,3 @@ +import * as R from 'ramda'; + +export const BalanceSheetTotal = (Base: any) => class extends Base {}; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/constants.ts b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/constants.ts new file mode 100644 index 000000000..2081d2859 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/BalanceSheet/constants.ts @@ -0,0 +1,64 @@ +export const MAP_CONFIG = { childrenPath: 'children', pathFormat: 'array' }; + +export const DISPLAY_COLUMNS_BY = { + DATE_PERIODS: 'date_periods', + TOTAL: 'total', +}; + +export enum IROW_TYPE { + AGGREGATE = 'AGGREGATE', + ACCOUNTS = 'ACCOUNTS', + ACCOUNT = 'ACCOUNT', + NET_INCOME = 'NET_INCOME', + TOTAL = 'TOTAL', +} + +export const HtmlTableCustomCss = ` +table tr.row-type--total td { + font-weight: 600; + border-top: 1px solid #bbb; + color: #000; +} +table tr.row-type--total.row-id--assets td, +table tr.row-type--total.row-id--liability-equity td { + border-bottom: 3px double #000; +} +table .column--name, +table .cell--name { + width: 400px; +} + +table .column--total { + width: 25%; +} + +table td.cell--total, +table td.cell--previous_year, +table td.cell--previous_year_change, +table td.cell--previous_year_percentage, + +table td.cell--previous_period, +table td.cell--previous_period_change, +table td.cell--previous_period_percentage, + +table td.cell--percentage_of_row, +table td.cell--percentage_of_column, +table td[class*="cell--date-range"] { + text-align: right; +} + +table .column--total, +table .column--previous_year, +table .column--previous_year_change, +table .column--previous_year_percentage, + +table .column--previous_period, +table .column--previous_period_change, +table .column--previous_period_percentage, + +table .column--percentage_of_row, +table .column--percentage_of_column, +table [class*="column--date-range"] { + text-align: right; +} +`; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashFlow.ts b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashFlow.ts new file mode 100644 index 000000000..019b252ba --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashFlow.ts @@ -0,0 +1,703 @@ +import * as R from 'ramda'; +import { defaultTo, map, set, sumBy, isEmpty, mapValues, get } from 'lodash'; +import * as mathjs from 'mathjs'; +import moment from 'moment'; +import { compose } from 'lodash/fp'; +import { + IAccount, + ILedger, + INumberFormatQuery, + ICashFlowSchemaSection, + ICashFlowStatementQuery, + ICashFlowStatementNetIncomeSection, + ICashFlowStatementAccountSection, + ICashFlowSchemaSectionAccounts, + ICashFlowStatementAccountMeta, + ICashFlowSchemaAccountRelation, + ICashFlowStatementSectionType, + ICashFlowStatementData, + ICashFlowSchemaTotalSection, + ICashFlowStatementTotalSection, + ICashFlowStatementSection, + ICashFlowCashBeginningNode, + ICashFlowStatementAggregateSection, +} from '@/interfaces'; +import CASH_FLOW_SCHEMA from './schema'; +import FinancialSheet from '../FinancialSheet'; +import { transformToMapBy, accumSum } from 'utils'; +import { ACCOUNT_ROOT_TYPE } from '@/data/AccountTypes'; +import { CashFlowStatementDatePeriods } from './CashFlowDatePeriods'; +import I18nService from '@/services/I18n/I18nService'; +import { DISPLAY_COLUMNS_BY } from './constants'; +import { FinancialSheetStructure } from '../FinancialSheetStructure'; + +export default class CashFlowStatement extends compose( + CashFlowStatementDatePeriods, + FinancialSheetStructure +)(FinancialSheet) { + readonly baseCurrency: string; + readonly i18n: I18nService; + readonly sectionsByIds = {}; + readonly cashFlowSchemaMap: Map; + readonly cashFlowSchemaSeq: Array; + readonly accountByTypeMap: Map; + readonly accountsByRootType: Map; + readonly ledger: ILedger; + readonly cashLedger: ILedger; + readonly netIncomeLedger: ILedger; + readonly query: ICashFlowStatementQuery; + readonly numberFormat: INumberFormatQuery; + readonly comparatorDateType: string; + readonly dateRangeSet: { fromDate: Date; toDate: Date }[]; + + /** + * Constructor method. + * @constructor + */ + constructor( + accounts: IAccount[], + ledger: ILedger, + cashLedger: ILedger, + netIncomeLedger: ILedger, + query: ICashFlowStatementQuery, + baseCurrency: string, + i18n + ) { + super(); + + this.baseCurrency = baseCurrency; + this.i18n = i18n; + this.ledger = ledger; + this.cashLedger = cashLedger; + this.netIncomeLedger = netIncomeLedger; + this.accountByTypeMap = transformToMapBy(accounts, 'accountType'); + this.accountsByRootType = transformToMapBy(accounts, 'accountRootType'); + this.query = query; + this.numberFormat = this.query.numberFormat; + this.dateRangeSet = []; + this.comparatorDateType = + query.displayColumnsType === 'total' ? 'day' : query.displayColumnsBy; + + this.initDateRangeCollection(); + } + + // -------------------------------------------- + // # GENERAL UTILITIES + // -------------------------------------------- + /** + * Retrieve the expense accounts ids. + * @return {number[]} + */ + private getAccountsIdsByType = (accountType: string): number[] => { + const expenseAccounts = this.accountsByRootType.get(accountType); + const expenseAccountsIds = map(expenseAccounts, 'id'); + + return expenseAccountsIds; + }; + + /** + * Detarmines the given display columns by type. + * @param {string} displayColumnsBy + * @returns {boolean} + */ + private isDisplayColumnsBy = (displayColumnsBy: string): boolean => { + return this.query.displayColumnsType === displayColumnsBy; + }; + + /** + * Adjustments the given amount. + * @param {string} direction + * @param {number} amount - + * @return {number} + */ + private amountAdjustment = (direction: 'mines' | 'plus', amount): number => { + return R.when( + R.always(R.equals(direction, 'mines')), + R.multiply(-1) + )(amount); + }; + + // -------------------------------------------- + // # NET INCOME NODE + // -------------------------------------------- + /** + * Retrieve the accounts net income. + * @returns {number} - Amount of net income. + */ + private getAccountsNetIncome(): number { + // Mapping income/expense accounts ids. + const incomeAccountsIds = this.getAccountsIdsByType( + ACCOUNT_ROOT_TYPE.INCOME + ); + const expenseAccountsIds = this.getAccountsIdsByType( + ACCOUNT_ROOT_TYPE.EXPENSE + ); + // Income closing balance. + const incomeClosingBalance = accumSum(incomeAccountsIds, (id) => + this.netIncomeLedger.whereAccountId(id).getClosingBalance() + ); + // Expense closing balance. + const expenseClosingBalance = accumSum(expenseAccountsIds, (id) => + this.netIncomeLedger.whereAccountId(id).getClosingBalance() + ); + // Net income = income - expenses. + const netIncome = incomeClosingBalance - expenseClosingBalance; + + return netIncome; + } + + /** + * Parses the net income section from the given section schema. + * @param {ICashFlowSchemaSection} nodeSchema - Report section schema. + * @returns {ICashFlowStatementNetIncomeSection} + */ + private netIncomeSectionMapper = ( + nodeSchema: ICashFlowSchemaSection + ): ICashFlowStatementNetIncomeSection => { + const netIncome = this.getAccountsNetIncome(); + + const node = { + id: nodeSchema.id, + label: this.i18n.__(nodeSchema.label), + total: this.getAmountMeta(netIncome), + sectionType: ICashFlowStatementSectionType.NET_INCOME, + }; + return R.compose( + R.when( + R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)), + this.assocPeriodsToNetIncomeNode + ) + )(node); + }; + + // -------------------------------------------- + // # ACCOUNT NODE + // -------------------------------------------- + /** + * Retrieve account meta. + * @param {ICashFlowSchemaAccountRelation} relation - Account relation. + * @param {IAccount} account - + * @returns {ICashFlowStatementAccountMeta} + */ + private accountMetaMapper = ( + relation: ICashFlowSchemaAccountRelation, + account: IAccount + ): ICashFlowStatementAccountMeta => { + // Retrieve the closing balance of the given account. + const getClosingBalance = (id) => + this.ledger.whereAccountId(id).getClosingBalance(); + + const closingBalance = R.compose( + // Multiplies the amount by -1 in case the relation in mines. + R.curry(this.amountAdjustment)(relation.direction) + )(getClosingBalance(account.id)); + + const node = { + id: account.id, + code: account.code, + label: account.name, + accountType: account.accountType, + adjustmentType: relation.direction, + total: this.getAmountMeta(closingBalance), + sectionType: ICashFlowStatementSectionType.ACCOUNT, + }; + return R.compose( + R.when( + R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)), + this.assocPeriodsToAccountNode + ) + )(node); + }; + + /** + * Retrieve accounts sections by the given schema relation. + * @param {ICashFlowSchemaAccountRelation} relation + * @returns {ICashFlowStatementAccountMeta[]} + */ + private getAccountsBySchemaRelation = ( + relation: ICashFlowSchemaAccountRelation + ): ICashFlowStatementAccountMeta[] => { + const accounts = defaultTo(this.accountByTypeMap.get(relation.type), []); + const accountMetaMapper = R.curry(this.accountMetaMapper)(relation); + return R.map(accountMetaMapper)(accounts); + }; + + /** + * Retrieve the accounts meta. + * @param {string[]} types + * @returns {ICashFlowStatementAccountMeta[]} + */ + private getAccountsBySchemaRelations = ( + relations: ICashFlowSchemaAccountRelation[] + ): ICashFlowStatementAccountMeta[] => { + return R.pipe( + R.append(R.map(this.getAccountsBySchemaRelation)(relations)), + R.flatten + )([]); + }; + + /** + * Calculates the accounts total + * @param {ICashFlowStatementAccountMeta[]} accounts + * @returns {number} + */ + private getAccountsMetaTotal = ( + accounts: ICashFlowStatementAccountMeta[] + ): number => { + return sumBy(accounts, 'total.amount'); + }; + + /** + * Retrieve the accounts section from the section schema. + * @param {ICashFlowSchemaSectionAccounts} sectionSchema + * @returns {ICashFlowStatementAccountSection} + */ + private accountsSectionParser = ( + sectionSchema: ICashFlowSchemaSectionAccounts + ): ICashFlowStatementAccountSection => { + const { accountsRelations } = sectionSchema; + + const accounts = this.getAccountsBySchemaRelations(accountsRelations); + const accountsTotal = this.getAccountsMetaTotal(accounts); + const total = this.getTotalAmountMeta(accountsTotal); + + const node = { + sectionType: ICashFlowStatementSectionType.ACCOUNTS, + id: sectionSchema.id, + label: this.i18n.__(sectionSchema.label), + footerLabel: this.i18n.__(sectionSchema.footerLabel), + children: accounts, + total, + }; + return R.compose( + R.when( + R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)), + this.assocPeriodsToAggregateNode + ) + )(node); + }; + + /** + * Detarmines the schema section type. + * @param {string} type + * @param {ICashFlowSchemaSection} section + * @returns {boolean} + */ + private isSchemaSectionType = R.curry( + (type: string, section: ICashFlowSchemaSection): boolean => { + return type === section.sectionType; + } + ); + + // -------------------------------------------- + // # AGGREGATE NODE + // -------------------------------------------- + /** + * Aggregate schema node parser to aggregate report node. + * @param {ICashFlowSchemaSection} schemaSection + * @returns {ICashFlowStatementAggregateSection} + */ + private regularSectionParser = R.curry( + ( + children, + schemaSection: ICashFlowSchemaSection + ): ICashFlowStatementAggregateSection => { + const node = { + id: schemaSection.id, + label: this.i18n.__(schemaSection.label), + footerLabel: this.i18n.__(schemaSection.footerLabel), + sectionType: ICashFlowStatementSectionType.AGGREGATE, + children, + }; + return R.compose( + R.when( + this.isSchemaSectionType(ICashFlowStatementSectionType.AGGREGATE), + this.assocRegularSectionTotal + ), + R.when( + this.isSchemaSectionType(ICashFlowStatementSectionType.AGGREGATE), + R.when( + R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)), + this.assocPeriodsToAggregateNode + ) + ) + )(node); + } + ); + + private transformSectionsToMap = (sections: ICashFlowSchemaSection[]) => { + return this.reduceNodesDeep( + sections, + (acc, section) => { + if (section.id) { + acc[`${section.id}`] = section; + } + return acc; + }, + {} + ); + }; + + // -------------------------------------------- + // # TOTAL EQUATION NODE + // -------------------------------------------- + + private sectionsMapToTotal = (mappedSections: { [key: number]: any }) => { + return mapValues(mappedSections, (node) => get(node, 'total.amount') || 0); + }; + + /** + * Evauluate equaation string with the given scope table. + * @param {string} equation - + * @param {{ [key: string]: number }} scope - + * @return {number} + */ + private evaluateEquation = ( + equation: string, + scope: { [key: string | number]: number } + ): number => { + return mathjs.evaluate(equation, scope); + }; + + /** + * Retrieve the total section from the eqauation parser. + * @param {ICashFlowSchemaTotalSection} sectionSchema + * @param {ICashFlowSchemaSection[]} accumulatedSections + * @returns {ICashFlowStatementTotalSection} + */ + private totalEquationSectionParser = ( + accumulatedSections: ICashFlowSchemaSection[], + sectionSchema: ICashFlowSchemaTotalSection + ): ICashFlowStatementTotalSection => { + const mappedSectionsById = this.transformSectionsToMap(accumulatedSections); + const nodesTotalById = this.sectionsMapToTotal(mappedSectionsById); + + const total = this.evaluateEquation(sectionSchema.equation, nodesTotalById); + + return R.compose( + R.when( + R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)), + R.curry(this.assocTotalEquationDatePeriods)( + mappedSectionsById, + sectionSchema.equation + ) + ) + )({ + sectionType: ICashFlowStatementSectionType.TOTAL, + id: sectionSchema.id, + label: this.i18n.__(sectionSchema.label), + total: this.getTotalAmountMeta(total), + }); + }; + + /** + * Retrieve the beginning cash from date. + * @param {Date|string} fromDate - + * @return {Date} + */ + private beginningCashFrom = (fromDate: string | Date): Date => { + return moment(fromDate).subtract(1, 'days').toDate(); + }; + + /** + * Retrieve account meta. + * @param {ICashFlowSchemaAccountRelation} relation + * @param {IAccount} account + * @returns {ICashFlowStatementAccountMeta} + */ + private cashAccountMetaMapper = ( + relation: ICashFlowSchemaAccountRelation, + account: IAccount + ): ICashFlowStatementAccountMeta => { + const cashToDate = this.beginningCashFrom(this.query.fromDate); + + const closingBalance = this.cashLedger + .whereToDate(cashToDate) + .whereAccountId(account.id) + .getClosingBalance(); + + const node = { + id: account.id, + code: account.code, + label: account.name, + accountType: account.accountType, + adjustmentType: relation.direction, + total: this.getAmountMeta(closingBalance), + sectionType: ICashFlowStatementSectionType.ACCOUNT, + }; + return R.compose( + R.when( + R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)), + this.assocCashAtBeginningAccountDatePeriods + ) + )(node); + }; + + /** + * Retrieve accounts sections by the given schema relation. + * @param {ICashFlowSchemaAccountRelation} relation + * @returns {ICashFlowStatementAccountMeta[]} + */ + private getCashAccountsBySchemaRelation = ( + relation: ICashFlowSchemaAccountRelation + ): ICashFlowStatementAccountMeta[] => { + const accounts = this.accountByTypeMap.get(relation.type) || []; + const accountMetaMapper = R.curry(this.cashAccountMetaMapper)(relation); + return accounts.map(accountMetaMapper); + }; + + /** + * Retrieve the accounts meta. + * @param {string[]} types + * @returns {ICashFlowStatementAccountMeta[]} + */ + private getCashAccountsBySchemaRelations = ( + relations: ICashFlowSchemaAccountRelation[] + ): ICashFlowStatementAccountMeta[] => { + return R.concat(...R.map(this.getCashAccountsBySchemaRelation)(relations)); + }; + + /** + * Parses the cash at beginning section. + * @param {ICashFlowSchemaTotalSection} sectionSchema - + * @return {ICashFlowCashBeginningNode} + */ + private cashAtBeginningSectionParser = ( + nodeSchema: ICashFlowSchemaSection + ): ICashFlowCashBeginningNode => { + const { accountsRelations } = nodeSchema; + const children = this.getCashAccountsBySchemaRelations(accountsRelations); + const total = this.getAccountsMetaTotal(children); + + const node = { + sectionType: ICashFlowStatementSectionType.CASH_AT_BEGINNING, + id: nodeSchema.id, + label: this.i18n.__(nodeSchema.label), + children, + total: this.getTotalAmountMeta(total), + }; + return R.compose( + R.when( + R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)), + this.assocCashAtBeginningDatePeriods + ) + )(node); + }; + + /** + * Parses the schema section. + * @param {ICashFlowSchemaSection} schemaNode + * @returns {ICashFlowSchemaSection} + */ + private schemaSectionParser = ( + schemaNode: ICashFlowSchemaSection, + children + ): ICashFlowSchemaSection | ICashFlowStatementSection => { + return R.compose( + // Accounts node. + R.when( + this.isSchemaSectionType(ICashFlowStatementSectionType.ACCOUNTS), + this.accountsSectionParser + ), + // Net income node. + R.when( + this.isSchemaSectionType(ICashFlowStatementSectionType.NET_INCOME), + this.netIncomeSectionMapper + ), + // Cash at beginning node. + R.when( + this.isSchemaSectionType( + ICashFlowStatementSectionType.CASH_AT_BEGINNING + ), + this.cashAtBeginningSectionParser + ), + // Aggregate node. (that has no section type). + R.when( + this.isSchemaSectionType(ICashFlowStatementSectionType.AGGREGATE), + this.regularSectionParser(children) + ) + )(schemaNode); + }; + + /** + * Parses the schema section. + * @param {ICashFlowSchemaSection | ICashFlowStatementSection} section + * @param {number} key + * @param {ICashFlowSchemaSection[]} parentValue + * @param {(ICashFlowSchemaSection | ICashFlowStatementSection)[]} accumulatedSections + * @returns {ICashFlowSchemaSection} + */ + private schemaSectionTotalParser = ( + section: ICashFlowSchemaSection | ICashFlowStatementSection, + key: number, + parentValue: ICashFlowSchemaSection[], + context, + accumulatedSections: (ICashFlowSchemaSection | ICashFlowStatementSection)[] + ): ICashFlowSchemaSection | ICashFlowStatementSection => { + return R.compose( + // Total equation section. + R.when( + this.isSchemaSectionType(ICashFlowStatementSectionType.TOTAL), + R.curry(this.totalEquationSectionParser)(accumulatedSections) + ) + )(section); + }; + + /** + * Schema sections parser. + * @param {ICashFlowSchemaSection[]}schema + * @returns {ICashFlowStatementSection[]} + */ + private schemaSectionsParser = ( + schema: ICashFlowSchemaSection[] + ): ICashFlowStatementSection[] => { + return this.mapNodesDeepReverse(schema, this.schemaSectionParser); + }; + + /** + * Writes the `total` property to the aggregate node. + * @param {ICashFlowStatementSection} section + * @return {ICashFlowStatementSection} + */ + private assocRegularSectionTotal = (section: ICashFlowStatementSection) => { + const total = this.getAccountsMetaTotal(section.children); + return R.assoc('total', this.getTotalAmountMeta(total), section); + }; + + /** + * Parses total schema nodes. + * @param {(ICashFlowSchemaSection | ICashFlowStatementSection)[]} sections + * @returns {(ICashFlowSchemaSection | ICashFlowStatementSection)[]} + */ + private totalSectionsParser = ( + sections: (ICashFlowSchemaSection | ICashFlowStatementSection)[] + ): (ICashFlowSchemaSection | ICashFlowStatementSection)[] => { + return this.reduceNodesDeep( + sections, + (acc, value, key, parentValue, context) => { + set( + acc, + context.path, + this.schemaSectionTotalParser(value, key, parentValue, context, acc) + ); + return acc; + }, + [] + ); + }; + + // -------------------------------------------- + // REPORT FILTERING + // -------------------------------------------- + /** + * Detarmines the given section has children and not empty. + * @param {ICashFlowStatementSection} section + * @returns {boolean} + */ + private isSectionHasChildren = ( + section: ICashFlowStatementSection + ): boolean => { + return !isEmpty(section.children); + }; + + /** + * Detarmines whether the section has no zero amount. + * @param {ICashFlowStatementSection} section + * @returns {boolean} + */ + private isSectionNoneZero = (section: ICashFlowStatementSection): boolean => { + return section.total.amount !== 0; + }; + + /** + * Detarmines whether the parent accounts sections has children. + * @param {ICashFlowStatementSection} section + * @returns {boolean} + */ + private isAccountsSectionHasChildren = ( + section: ICashFlowStatementSection[] + ): boolean => { + return R.ifElse( + this.isSchemaSectionType(ICashFlowStatementSectionType.ACCOUNTS), + this.isSectionHasChildren, + R.always(true) + )(section); + }; + + /** + * Detarmines the account section has no zero otherwise returns true. + * @param {ICashFlowStatementSection} section + * @returns {boolean} + */ + private isAccountLeafNoneZero = ( + section: ICashFlowStatementSection[] + ): boolean => { + return R.ifElse( + this.isSchemaSectionType(ICashFlowStatementSectionType.ACCOUNT), + this.isSectionNoneZero, + R.always(true) + )(section); + }; + + /** + * Deep filters the non-zero accounts leafs of the report sections. + * @param {ICashFlowStatementSection[]} sections + * @returns {ICashFlowStatementSection[]} + */ + private filterNoneZeroAccountsLeafs = ( + sections: ICashFlowStatementSection[] + ): ICashFlowStatementSection[] => { + return this.filterNodesDeep(sections, this.isAccountLeafNoneZero); + }; + + /** + * Deep filter the non-children sections of the report sections. + * @param {ICashFlowStatementSection[]} sections + * @returns {ICashFlowStatementSection[]} + */ + private filterNoneChildrenSections = ( + sections: ICashFlowStatementSection[] + ): ICashFlowStatementSection[] => { + return this.filterNodesDeep(sections, this.isAccountsSectionHasChildren); + }; + + /** + * Filters the report data. + * @param {ICashFlowStatementSection[]} sections + * @returns {ICashFlowStatementSection[]} + */ + private filterReportData = ( + sections: ICashFlowStatementSection[] + ): ICashFlowStatementSection[] => { + return R.compose( + this.filterNoneChildrenSections, + this.filterNoneZeroAccountsLeafs + )(sections); + }; + + /** + * Schema parser. + * @param {ICashFlowSchemaSection[]} schema + * @returns {ICashFlowSchemaSection[]} + */ + private schemaParser = ( + schema: ICashFlowSchemaSection[] + ): ICashFlowSchemaSection[] => { + return R.compose( + R.when( + R.always(this.query.noneTransactions || this.query.noneZero), + this.filterReportData + ), + this.totalSectionsParser, + this.schemaSectionsParser + )(schema); + }; + + /** + * Retrieve the cashflow statement data. + * @return {ICashFlowStatementData} + */ + public reportData = (): ICashFlowStatementData => { + return this.schemaParser(R.clone(CASH_FLOW_SCHEMA)); + }; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashFlowDatePeriods.ts b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashFlowDatePeriods.ts new file mode 100644 index 000000000..7d493cc5d --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashFlowDatePeriods.ts @@ -0,0 +1,411 @@ +import * as R from 'ramda'; +import { sumBy, mapValues, get } from 'lodash'; +import { ACCOUNT_ROOT_TYPE } from '@/data/AccountTypes'; +import { accumSum, dateRangeFromToCollection } from 'utils'; +import { + ICashFlowDatePeriod, + ICashFlowStatementNetIncomeSection, + ICashFlowStatementAccountSection, + ICashFlowStatementSection, + ICashFlowSchemaTotalSection, + IFormatNumberSettings, + ICashFlowStatementTotalSection, + IDateRange, + ICashFlowStatementQuery, +} from '@/interfaces'; + +export const CashFlowStatementDatePeriods = (Base) => + class extends Base { + dateRangeSet: IDateRange[]; + query: ICashFlowStatementQuery; + + /** + * Initialize date range set. + */ + private initDateRangeCollection() { + this.dateRangeSet = dateRangeFromToCollection( + this.query.fromDate, + this.query.toDate, + this.comparatorDateType + ); + } + + /** + * Retrieve the date period meta. + * @param {number} total - Total amount. + * @param {Date} fromDate - From date. + * @param {Date} toDate - To date. + * @return {ICashFlowDatePeriod} + */ + private getDatePeriodTotalMeta = ( + total: number, + fromDate: Date, + toDate: Date, + overrideSettings: IFormatNumberSettings = {} + ): ICashFlowDatePeriod => { + return this.getDatePeriodMeta(total, fromDate, toDate, { + money: true, + ...overrideSettings, + }); + }; + + /** + * Retrieve the date period meta. + * @param {number} total - Total amount. + * @param {Date} fromDate - From date. + * @param {Date} toDate - To date. + * @return {ICashFlowDatePeriod} + */ + private getDatePeriodMeta = ( + total: number, + fromDate: Date, + toDate: Date, + overrideSettings?: IFormatNumberSettings + ): ICashFlowDatePeriod => { + return { + fromDate: this.getDateMeta(fromDate), + toDate: this.getDateMeta(toDate), + total: this.getAmountMeta(total, overrideSettings), + }; + }; + + // Net income -------------------- + /** + * Retrieve the net income between the given date range. + * @param {Date} fromDate + * @param {Date} toDate + * @returns {number} + */ + private getNetIncomeDateRange = (fromDate: Date, toDate: Date) => { + // Mapping income/expense accounts ids. + const incomeAccountsIds = this.getAccountsIdsByType( + ACCOUNT_ROOT_TYPE.INCOME + ); + const expenseAccountsIds = this.getAccountsIdsByType( + ACCOUNT_ROOT_TYPE.EXPENSE + ); + // Income closing balance. + const incomeClosingBalance = accumSum(incomeAccountsIds, (id) => + this.netIncomeLedger + .whereFromDate(fromDate) + .whereToDate(toDate) + .whereAccountId(id) + .getClosingBalance() + ); + // Expense closing balance. + const expenseClosingBalance = accumSum(expenseAccountsIds, (id) => + this.netIncomeLedger + .whereToDate(toDate) + .whereFromDate(fromDate) + .whereAccountId(id) + .getClosingBalance() + ); + // Net income = income - expenses. + const netIncome = incomeClosingBalance - expenseClosingBalance; + + return netIncome; + }; + + /** + * Retrieve the net income of date period. + * @param {IDateRange} dateRange - + * @retrun {ICashFlowDatePeriod} + */ + private getNetIncomeDatePeriod = (dateRange): ICashFlowDatePeriod => { + const total = this.getNetIncomeDateRange( + dateRange.fromDate, + dateRange.toDate + ); + return this.getDatePeriodMeta( + total, + dateRange.fromDate, + dateRange.toDate + ); + }; + + /** + * Retrieve the net income node between the given date ranges. + * @param {Date} fromDate + * @param {Date} toDate + * @returns {ICashFlowDatePeriod[]} + */ + private getNetIncomeDatePeriods = ( + section: ICashFlowStatementNetIncomeSection + ): ICashFlowDatePeriod[] => { + return this.dateRangeSet.map(this.getNetIncomeDatePeriod.bind(this)); + }; + + /** + * Writes periods property to net income section. + * @param {ICashFlowStatementNetIncomeSection} section + * @returns {ICashFlowStatementNetIncomeSection} + */ + protected assocPeriodsToNetIncomeNode = ( + section: ICashFlowStatementNetIncomeSection + ): ICashFlowStatementNetIncomeSection => { + const incomeDatePeriods = this.getNetIncomeDatePeriods(section); + return R.assoc('periods', incomeDatePeriods, section); + }; + + // Account nodes -------------------- + /** + * Retrieve the account total between date range. + * @param {Date} fromDate - From date. + * @param {Date} toDate - To date. + * @return {number} + */ + private getAccountTotalDateRange = ( + node: ICashFlowStatementAccountSection, + fromDate: Date, + toDate: Date + ): number => { + const closingBalance = this.ledger + .whereFromDate(fromDate) + .whereToDate(toDate) + .whereAccountId(node.id) + .getClosingBalance(); + + return this.amountAdjustment(node.adjustmentType, closingBalance); + }; + + /** + * Retrieve the given account node total date period. + * @param {ICashFlowStatementAccountSection} node - + * @param {Date} fromDate - From date. + * @param {Date} toDate - To date. + * @return {ICashFlowDatePeriod} + */ + private getAccountTotalDatePeriod = ( + node: ICashFlowStatementAccountSection, + fromDate: Date, + toDate: Date + ): ICashFlowDatePeriod => { + const total = this.getAccountTotalDateRange(node, fromDate, toDate); + return this.getDatePeriodMeta(total, fromDate, toDate); + }; + + /** + * Retrieve the accounts date periods nodes of the give account node. + * @param {ICashFlowStatementAccountSection} node - + * @return {ICashFlowDatePeriod[]} + */ + private getAccountDatePeriods = ( + node: ICashFlowStatementAccountSection + ): ICashFlowDatePeriod[] => { + return this.getNodeDatePeriods( + node, + this.getAccountTotalDatePeriod.bind(this) + ); + } + + /** + * Writes `periods` property to account node. + * @param {ICashFlowStatementAccountSection} node - + * @return {ICashFlowStatementAccountSection} + */ + protected assocPeriodsToAccountNode = ( + node: ICashFlowStatementAccountSection + ): ICashFlowStatementAccountSection => { + const datePeriods = this.getAccountDatePeriods(node); + return R.assoc('periods', datePeriods, node); + } + + // Aggregate node ------------------------- + /** + * Retrieve total of the given period index for node that has children nodes. + * @return {number} + */ + private getChildrenTotalPeriodByIndex = ( + node: ICashFlowStatementSection, + index: number + ): number => { + return sumBy(node.children, `periods[${index}].total.amount`); + } + + /** + * Retrieve date period meta of the given node index. + * @param {ICashFlowStatementSection} node - + * @param {number} index - Loop index. + * @param {Date} fromDate - From date. + * @param {Date} toDate - To date. + */ + private getChildrenTotalPeriodMetaByIndex( + node: ICashFlowStatementSection, + index: number, + fromDate: Date, + toDate: Date + ) { + const total = this.getChildrenTotalPeriodByIndex(node, index); + return this.getDatePeriodTotalMeta(total, fromDate, toDate); + } + + /** + * Retrieve the date periods of aggregate node. + * @param {ICashFlowStatementSection} node + */ + private getAggregateNodeDatePeriods(node: ICashFlowStatementSection) { + const getChildrenTotalPeriodMetaByIndex = R.curry( + this.getChildrenTotalPeriodMetaByIndex.bind(this) + )(node); + + return this.dateRangeSet.map((dateRange, index) => + getChildrenTotalPeriodMetaByIndex( + index, + dateRange.fromDate, + dateRange.toDate + ) + ); + } + + /** + * Writes `periods` property to aggregate section node. + * @param {ICashFlowStatementSection} node - + * @return {ICashFlowStatementSection} + */ + protected assocPeriodsToAggregateNode = ( + node: ICashFlowStatementSection + ): ICashFlowStatementSection => { + const datePeriods = this.getAggregateNodeDatePeriods(node); + return R.assoc('periods', datePeriods, node); + }; + + // Total equation node -------------------- + + private sectionsMapToTotalPeriod = ( + mappedSections: { [key: number]: any }, + index + ) => { + return mapValues( + mappedSections, + (node) => get(node, `periods[${index}].total.amount`) || 0 + ); + }; + + /** + * Retrieve the date periods of the given total equation. + * @param {ICashFlowSchemaTotalSection} + * @param {string} equation - + * @return {ICashFlowDatePeriod[]} + */ + private getTotalEquationDatePeriods = ( + node: ICashFlowSchemaTotalSection, + equation: string, + nodesTable + ): ICashFlowDatePeriod[] => { + return this.getNodeDatePeriods(node, (node, fromDate, toDate, index) => { + const periodScope = this.sectionsMapToTotalPeriod(nodesTable, index); + const total = this.evaluateEquation(equation, periodScope); + + return this.getDatePeriodTotalMeta(total, fromDate, toDate); + }); + }; + + /** + * Associates the total periods of total equation to the ginve total node.. + * @param {ICashFlowSchemaTotalSection} totalSection - + * @return {ICashFlowStatementTotalSection} + */ + protected assocTotalEquationDatePeriods = ( + nodesTable: any, + equation: string, + node: ICashFlowSchemaTotalSection + ): ICashFlowStatementTotalSection => { + const datePeriods = this.getTotalEquationDatePeriods( + node, + equation, + nodesTable + ); + + return R.assoc('periods', datePeriods, node); + }; + + // Cash at beginning ---------------------- + + /** + * Retrieve the date preioods of the given node and accumulated function. + * @param {} node + * @param {} + * @return {} + */ + private getNodeDatePeriods = (node, callback) => { + const curriedCallback = R.curry(callback)(node); + + return this.dateRangeSet.map((dateRange, index) => { + return curriedCallback(dateRange.fromDate, dateRange.toDate, index); + }); + }; + + /** + * Retrieve the account total between date range. + * @param {Date} fromDate - From date. + * @param {Date} toDate - To date. + * @return {number} + */ + private getBeginningCashAccountDateRange = ( + node: ICashFlowStatementSection, + fromDate: Date, + toDate: Date + ) => { + const cashToDate = this.beginningCashFrom(fromDate); + + return this.cashLedger + .whereToDate(cashToDate) + .whereAccountId(node.id) + .getClosingBalance(); + }; + + /** + * Retrieve the beginning cash date period. + * @param {ICashFlowStatementSection} node - + * @param {Date} fromDate - From date. + * @param {Date} toDate - To date. + * @return {ICashFlowDatePeriod} + */ + private getBeginningCashDatePeriod = ( + node: ICashFlowStatementSection, + fromDate: Date, + toDate: Date + ) => { + const total = this.getBeginningCashAccountDateRange( + node, + fromDate, + toDate + ); + return this.getDatePeriodTotalMeta(total, fromDate, toDate); + }; + + /** + * Retrieve the beginning cash account periods. + * @param {ICashFlowStatementSection} node + * @return {ICashFlowDatePeriod} + */ + private getBeginningCashAccountPeriods = ( + node: ICashFlowStatementSection + ): ICashFlowDatePeriod => { + return this.getNodeDatePeriods(node, this.getBeginningCashDatePeriod); + }; + + /** + * Writes `periods` property to cash at beginning date periods. + * @param {ICashFlowStatementSection} section - + * @return {ICashFlowStatementSection} + */ + protected assocCashAtBeginningDatePeriods = ( + node: ICashFlowStatementSection + ): ICashFlowStatementSection => { + const datePeriods = this.getAggregateNodeDatePeriods(node); + return R.assoc('periods', datePeriods, node); + }; + + /** + * Associates `periods` propery to cash at beginning account node. + * @param {ICashFlowStatementSection} node - + * @return {ICashFlowStatementSection} + */ + protected assocCashAtBeginningAccountDatePeriods = ( + node: ICashFlowStatementSection + ): ICashFlowStatementSection => { + const datePeriods = this.getBeginningCashAccountPeriods(node); + return R.assoc('periods', datePeriods, node); + }; + }; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashFlowRepository.ts b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashFlowRepository.ts new file mode 100644 index 000000000..d7e7532ac --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashFlowRepository.ts @@ -0,0 +1,173 @@ +import { Inject, Service } from 'typedi'; +import moment from 'moment'; +import { Knex } from 'knex'; +import { isEmpty } from 'lodash'; +import { + ICashFlowStatementQuery, + IAccountTransaction, + IAccount, +} from '@/interfaces'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; + +@Service() +export default class CashFlowRepository { + @Inject() + tenancy: HasTenancyService; + + /** + * Retrieve the group type from periods type. + * @param {string} displayType + * @returns {string} + */ + protected getGroupTypeFromPeriodsType(displayType: string) { + const displayTypes = { + year: 'year', + day: 'day', + month: 'month', + quarter: 'month', + week: 'day', + }; + return displayTypes[displayType] || 'month'; + } + + /** + * Retrieve the cashflow accounts. + * @returns {Promise} + */ + public async cashFlowAccounts(tenantId: number): Promise { + const { Account } = this.tenancy.models(tenantId); + + const accounts = await Account.query(); + + return accounts; + } + + /** + * Retrieve total of csah at beginning transactions. + * @param {number} tenantId - + * @param {ICashFlowStatementQuery} filter - + * @return {Promise} + */ + public cashAtBeginningTotalTransactions( + tenantId: number, + filter: ICashFlowStatementQuery + ): Promise { + const { AccountTransaction } = this.tenancy.models(tenantId); + const cashBeginningPeriod = moment(filter.fromDate) + .subtract(1, 'day') + .toDate(); + + return AccountTransaction.query().onBuild((query) => { + query.modify('creditDebitSummation'); + + query.select('accountId'); + query.groupBy('accountId'); + + query.withGraphFetched('account'); + query.modify('filterDateRange', null, cashBeginningPeriod); + + this.commonFilterBranchesQuery(filter, query); + }); + } + + /** + * Retrieve accounts transactions. + * @param {number} tenantId - + * @param {ICashFlowStatementQuery} filter + * @return {Promise} + */ + public getAccountsTransactions( + tenantId: number, + filter: ICashFlowStatementQuery + ): Promise { + const { AccountTransaction } = this.tenancy.models(tenantId); + const groupByDateType = this.getGroupTypeFromPeriodsType( + filter.displayColumnsBy + ); + + return AccountTransaction.query().onBuild((query) => { + query.modify('creditDebitSummation'); + query.modify('groupByDateFormat', groupByDateType); + + query.select('accountId'); + + query.groupBy('accountId'); + query.withGraphFetched('account'); + + query.modify('filterDateRange', filter.fromDate, filter.toDate); + + this.commonFilterBranchesQuery(filter, query); + }); + } + + /** + * Retrieve the net income tranasctions. + * @param {number} tenantId - + * @param {ICashFlowStatementQuery} query - + * @return {Promise} + */ + public getNetIncomeTransactions( + tenantId: number, + filter: ICashFlowStatementQuery + ): Promise { + const { AccountTransaction } = this.tenancy.models(tenantId); + const groupByDateType = this.getGroupTypeFromPeriodsType( + filter.displayColumnsBy + ); + + return AccountTransaction.query().onBuild((query) => { + query.modify('creditDebitSummation'); + query.modify('groupByDateFormat', groupByDateType); + + query.select('accountId'); + query.groupBy('accountId'); + + query.withGraphFetched('account'); + query.modify('filterDateRange', filter.fromDate, filter.toDate); + + this.commonFilterBranchesQuery(filter, query); + }); + } + + /** + * Retrieve peridos of cash at beginning transactions. + * @param {number} tenantId - + * @param {ICashFlowStatementQuery} filter - + * @return {Promise} + */ + public cashAtBeginningPeriodTransactions( + tenantId: number, + filter: ICashFlowStatementQuery + ): Promise { + const { AccountTransaction } = this.tenancy.models(tenantId); + const groupByDateType = this.getGroupTypeFromPeriodsType( + filter.displayColumnsBy + ); + + return AccountTransaction.query().onBuild((query) => { + query.modify('creditDebitSummation'); + query.modify('groupByDateFormat', groupByDateType); + + query.select('accountId'); + query.groupBy('accountId'); + + query.withGraphFetched('account'); + query.modify('filterDateRange', filter.fromDate, filter.toDate); + + this.commonFilterBranchesQuery(filter, query); + }); + } + + /** + * Common branches filter query. + * @param {Knex.QueryBuilder} query + */ + private commonFilterBranchesQuery = ( + query: ICashFlowStatementQuery, + knexQuery: Knex.QueryBuilder + ) => { + if (!isEmpty(query.branchesIds)) { + knexQuery.modify('filterByBranches', query.branchesIds); + } + }; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashFlowService.ts b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashFlowService.ts new file mode 100644 index 000000000..201a31be4 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashFlowService.ts @@ -0,0 +1,154 @@ +import moment from 'moment'; +import { Service, Inject } from 'typedi'; +import * as R from 'ramda'; +import TenancyService from '@/services/Tenancy/TenancyService'; +import FinancialSheet from '../FinancialSheet'; +import { + ICashFlowStatementService, + ICashFlowStatementQuery, + ICashFlowStatementDOO, + IAccountTransaction, + ICashFlowStatementMeta, +} from '@/interfaces'; +import CashFlowStatement from './CashFlow'; +import Ledger from '@/services/Accounting/Ledger'; +import CashFlowRepository from './CashFlowRepository'; +import InventoryService from '@/services/Inventory/Inventory'; +import { parseBoolean } from 'utils'; +import { Tenant } from '@/system/models'; +import { CashflowSheetMeta } from './CashflowSheetMeta'; + +@Service() +export default class CashFlowStatementService + extends FinancialSheet + implements ICashFlowStatementService +{ + @Inject() + tenancy: TenancyService; + + @Inject() + cashFlowRepo: CashFlowRepository; + + @Inject() + inventoryService: InventoryService; + + @Inject() + private cashflowSheetMeta: CashflowSheetMeta; + + /** + * Defaults balance sheet filter query. + * @return {IBalanceSheetQuery} + */ + get defaultQuery(): ICashFlowStatementQuery { + return { + displayColumnsType: 'total', + displayColumnsBy: 'day', + fromDate: moment().startOf('year').format('YYYY-MM-DD'), + toDate: moment().format('YYYY-MM-DD'), + numberFormat: { + precision: 2, + divideOn1000: false, + showZero: false, + formatMoney: 'total', + negativeFormat: 'mines', + }, + noneZero: false, + noneTransactions: false, + basis: 'cash', + }; + } + + /** + * Retrieve cash at beginning transactions. + * @param {number} tenantId - + * @param {ICashFlowStatementQuery} filter - + * @retrun {Promise} + */ + private async cashAtBeginningTransactions( + tenantId: number, + filter: ICashFlowStatementQuery + ): Promise { + const appendPeriodsOperToChain = (trans) => + R.append( + this.cashFlowRepo.cashAtBeginningPeriodTransactions(tenantId, filter), + trans + ); + + const promisesChain = R.pipe( + R.append( + this.cashFlowRepo.cashAtBeginningTotalTransactions(tenantId, filter) + ), + R.when( + R.always(R.equals(filter.displayColumnsType, 'date_periods')), + appendPeriodsOperToChain + ) + )([]); + const promisesResults = await Promise.all(promisesChain); + const transactions = R.flatten(promisesResults); + + return transactions; + } + + /** + * Retrieve the cash flow sheet statement. + * @param {number} tenantId + * @param {ICashFlowStatementQuery} query + * @returns {Promise} + */ + public async cashFlow( + tenantId: number, + query: ICashFlowStatementQuery + ): Promise { + const i18n = this.tenancy.i18n(tenantId); + + // Retrieve all accounts on the storage. + const accounts = await this.cashFlowRepo.cashFlowAccounts(tenantId); + + const tenant = await Tenant.query() + .findById(tenantId) + .withGraphFetched('metadata'); + + const filter = { + ...this.defaultQuery, + ...query, + }; + // Retrieve the accounts transactions. + const transactions = await this.cashFlowRepo.getAccountsTransactions( + tenantId, + filter + ); + // Retrieve the net income transactions. + const netIncome = await this.cashFlowRepo.getNetIncomeTransactions( + tenantId, + filter + ); + // Retrieve the cash at beginning transactions. + const cashAtBeginningTransactions = await this.cashAtBeginningTransactions( + tenantId, + filter + ); + // Transformes the transactions to ledgers. + const ledger = Ledger.fromTransactions(transactions); + const cashLedger = Ledger.fromTransactions(cashAtBeginningTransactions); + const netIncomeLedger = Ledger.fromTransactions(netIncome); + + // Cash flow statement. + const cashFlowInstance = new CashFlowStatement( + accounts, + ledger, + cashLedger, + netIncomeLedger, + filter, + tenant.metadata.baseCurrency, + i18n + ); + // Retrieve the cashflow sheet meta. + const meta = await this.cashflowSheetMeta.meta(tenantId, filter); + + return { + data: cashFlowInstance.reportData(), + query: filter, + meta, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashFlowTable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashFlowTable.ts new file mode 100644 index 000000000..7b49a685c --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashFlowTable.ts @@ -0,0 +1,373 @@ +import * as R from 'ramda'; +import { isEmpty, times } from 'lodash'; +import moment from 'moment'; +import { + ICashFlowStatementSection, + ICashFlowStatementSectionType, + ICashFlowStatement, + ITableRow, + ITableColumn, + ICashFlowStatementQuery, + IDateRange, + ICashFlowStatementDOO, +} from '@/interfaces'; +import { dateRangeFromToCollection, tableRowMapper } from 'utils'; +import { mapValuesDeep } from 'utils/deepdash'; + +enum IROW_TYPE { + AGGREGATE = 'AGGREGATE', + NET_INCOME = 'NET_INCOME', + ACCOUNTS = 'ACCOUNTS', + ACCOUNT = 'ACCOUNT', + TOTAL = 'TOTAL', +} +const DEEP_CONFIG = { childrenPath: 'children', pathFormat: 'array' }; +const DISPLAY_COLUMNS_BY = { + DATE_PERIODS: 'date_periods', + TOTAL: 'total', +}; + +export default class CashFlowTable implements ICashFlowTable { + private report: ICashFlowStatementDOO; + private i18n; + private dateRangeSet: IDateRange[]; + + /** + * Constructor method. + * @param {ICashFlowStatement} reportStatement + */ + constructor(reportStatement: ICashFlowStatementDOO, i18n) { + this.report = reportStatement; + this.i18n = i18n; + this.dateRangeSet = []; + this.initDateRangeCollection(); + } + + /** + * Initialize date range set. + */ + private initDateRangeCollection() { + this.dateRangeSet = dateRangeFromToCollection( + this.report.query.fromDate, + this.report.query.toDate, + this.report.query.displayColumnsBy + ); + } + + /** + * Retrieve the date periods columns accessors. + */ + private datePeriodsColumnsAccessors = () => { + return this.dateRangeSet.map((dateRange: IDateRange, index) => ({ + key: `date-range-${index}`, + accessor: `periods[${index}].total.formattedAmount`, + })); + }; + + /** + * Retrieve the total column accessor. + */ + private totalColumnAccessor = () => { + return [{ key: 'total', accessor: 'total.formattedAmount' }]; + }; + + /** + * Retrieve the common columns for all report nodes. + */ + private commonColumns = () => { + return R.compose( + R.concat([{ key: 'name', accessor: 'label' }]), + R.when( + R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)), + R.concat(this.datePeriodsColumnsAccessors()) + ), + R.concat(this.totalColumnAccessor()) + )([]); + }; + + /** + * Retrieve the table rows of regular section. + * @param {ICashFlowStatementSection} section + * @returns {ITableRow[]} + */ + private regularSectionMapper = ( + section: ICashFlowStatementSection + ): ITableRow => { + const columns = this.commonColumns(); + + return tableRowMapper(section, columns, { + rowTypes: [IROW_TYPE.AGGREGATE], + id: section.id, + }); + }; + + /** + * Retrieve the net income table rows of the section. + * @param {ICashFlowStatementSection} section + * @returns {ITableRow} + */ + private netIncomeSectionMapper = ( + section: ICashFlowStatementSection + ): ITableRow => { + const columns = this.commonColumns(); + + return tableRowMapper(section, columns, { + rowTypes: [IROW_TYPE.NET_INCOME, IROW_TYPE.TOTAL], + id: section.id, + }); + }; + + /** + * Retrieve the accounts table rows of the section. + * @param {ICashFlowStatementSection} section + * @returns {ITableRow} + */ + private accountsSectionMapper = ( + section: ICashFlowStatementSection + ): ITableRow => { + const columns = this.commonColumns(); + + return tableRowMapper(section, columns, { + rowTypes: [IROW_TYPE.ACCOUNTS], + id: section.id, + }); + }; + + /** + * Retrieve the account table row of account section. + * @param {ICashFlowStatementSection} section + * @returns {ITableRow} + */ + private accountSectionMapper = ( + section: ICashFlowStatementSection + ): ITableRow => { + const columns = this.commonColumns(); + + return tableRowMapper(section, columns, { + rowTypes: [IROW_TYPE.ACCOUNT], + id: `account-${section.id}`, + }); + }; + + /** + * Retrieve the total table rows from the given total section. + * @param {ICashFlowStatementSection} section + * @returns {ITableRow} + */ + private totalSectionMapper = ( + section: ICashFlowStatementSection + ): ITableRow => { + const columns = this.commonColumns(); + + return tableRowMapper(section, columns, { + rowTypes: [IROW_TYPE.TOTAL], + id: section.id, + }); + }; + + /** + * Detarmines the schema section type. + * @param {string} type + * @param {ICashFlowSchemaSection} section + * @returns {boolean} + */ + private isSectionHasType = ( + type: string, + section: ICashFlowStatementSection + ): boolean => { + return type === section.sectionType; + }; + + /** + * The report section mapper. + * @param {ICashFlowStatementSection} section + * @returns {ITableRow} + */ + private sectionMapper = ( + section: ICashFlowStatementSection, + key: string, + parentSection: ICashFlowStatementSection + ): ITableRow => { + const isSectionHasType = R.curry(this.isSectionHasType); + + return R.pipe( + R.when( + isSectionHasType(ICashFlowStatementSectionType.AGGREGATE), + this.regularSectionMapper + ), + R.when( + isSectionHasType(ICashFlowStatementSectionType.CASH_AT_BEGINNING), + this.regularSectionMapper + ), + R.when( + isSectionHasType(ICashFlowStatementSectionType.NET_INCOME), + this.netIncomeSectionMapper + ), + R.when( + isSectionHasType(ICashFlowStatementSectionType.ACCOUNTS), + this.accountsSectionMapper + ), + R.when( + isSectionHasType(ICashFlowStatementSectionType.ACCOUNT), + this.accountSectionMapper + ), + R.when( + isSectionHasType(ICashFlowStatementSectionType.TOTAL), + this.totalSectionMapper + ) + )(section); + }; + + /** + * Mappes the sections to the table rows. + * @param {ICashFlowStatementSection[]} sections + * @returns {ITableRow[]} + */ + private mapSectionsToTableRows = ( + sections: ICashFlowStatementSection[] + ): ITableRow[] => { + return mapValuesDeep(sections, this.sectionMapper.bind(this), DEEP_CONFIG); + }; + + /** + * Appends the total to section's children. + * @param {ICashFlowStatementSection} section + * @returns {ICashFlowStatementSection} + */ + private appendTotalToSectionChildren = ( + section: ICashFlowStatementSection + ): ICashFlowStatementSection => { + const label = section.footerLabel + ? section.footerLabel + : this.i18n.__('Total {{accountName}}', { accountName: section.label }); + + section.children.push({ + sectionType: ICashFlowStatementSectionType.TOTAL, + label, + periods: section.periods, + total: section.total, + }); + return section; + }; + + /** + * + * @param {ICashFlowStatementSection} section + * @returns {ICashFlowStatementSection} + */ + private mapSectionsToAppendTotalChildren = ( + section: ICashFlowStatementSection + ): ICashFlowStatementSection => { + const isSectionHasChildren = (section) => !isEmpty(section.children); + + return R.compose( + R.when(isSectionHasChildren, this.appendTotalToSectionChildren.bind(this)) + )(section); + }; + + /** + * Appends total node to children section. + * @param {ICashFlowStatementSection[]} sections + * @returns {ICashFlowStatementSection[]} + */ + private appendTotalToChildren = (sections: ICashFlowStatementSection[]) => { + return mapValuesDeep( + sections, + this.mapSectionsToAppendTotalChildren.bind(this), + DEEP_CONFIG + ); + }; + + /** + * Retrieve the table rows of cash flow statement. + * @param {ICashFlowStatementSection[]} sections + * @returns {ITableRow[]} + */ + public tableRows = (): ITableRow[] => { + const sections = this.report.data; + + return R.pipe( + this.appendTotalToChildren, + this.mapSectionsToTableRows + )(sections); + }; + + /** + * Retrieve the total columns. + * @returns {ITableColumn} + */ + private totalColumns = (): ITableColumn[] => { + return [{ key: 'total', label: this.i18n.__('Total') }]; + }; + + /** + * Retrieve the formatted column label from the given date range. + * @param {ICashFlowDateRange} dateRange - + * @return {string} + */ + private formatColumnLabel = (dateRange: ICashFlowDateRange) => { + const monthFormat = (range) => moment(range.toDate).format('YYYY-MM'); + const yearFormat = (range) => moment(range.toDate).format('YYYY'); + const dayFormat = (range) => moment(range.toDate).format('YYYY-MM-DD'); + + const conditions = [ + ['month', monthFormat], + ['year', yearFormat], + ['day', dayFormat], + ['quarter', monthFormat], + ['week', dayFormat], + ]; + const conditionsPairs = R.map( + ([type, formatFn]) => [ + R.always(this.isDisplayColumnsType(type)), + formatFn, + ], + conditions + ); + + return R.compose(R.cond(conditionsPairs))(dateRange); + }; + + /** + * Date periods columns. + * @returns {ITableColumn[]} + */ + private datePeriodsColumns = (): ITableColumn[] => { + return this.dateRangeSet.map((dateRange, index) => ({ + key: `date-range-${index}`, + label: this.formatColumnLabel(dateRange), + })); + }; + + /** + * Detarmines the given column type is the current. + * @reutrns {boolean} + */ + private isDisplayColumnsBy = (displayColumnsType: string): Boolean => { + return this.report.query.displayColumnsType === displayColumnsType; + }; + + /** + * Detarmines whether the given display columns type is the current. + * @param {string} displayColumnsBy + * @returns {boolean} + */ + private isDisplayColumnsType = (displayColumnsBy: string): Boolean => { + return this.report.query.displayColumnsBy === displayColumnsBy; + }; + + /** + * Retrieve the table columns. + * @return {ITableColumn[]} + */ + public tableColumns = (): ITableColumn[] => { + return R.compose( + R.concat([{ key: 'name', label: this.i18n.__('Account name') }]), + R.when( + R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)), + R.concat(this.datePeriodsColumns()) + ), + R.concat(this.totalColumns()) + )([]); + }; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashflowExportInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashflowExportInjectable.ts new file mode 100644 index 000000000..8562cfbf5 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashflowExportInjectable.ts @@ -0,0 +1,46 @@ +import { Inject, Service } from 'typedi'; +import { ICashFlowStatementQuery } from '@/interfaces'; +import { TableSheet } from '@/lib/Xlsx/TableSheet'; +import { CashflowTableInjectable } from './CashflowTableInjectable'; + +@Service() +export class CashflowExportInjectable { + @Inject() + private cashflowSheetTable: CashflowTableInjectable; + + /** + * Retrieves the cashflow sheet in XLSX format. + * @param {number} tenantId + * @param {ICashFlowStatementQuery} query + * @returns {Promise} + */ + public async xlsx( + tenantId: number, + query: ICashFlowStatementQuery + ): Promise { + const table = await this.cashflowSheetTable.table(tenantId, query); + + const tableSheet = new TableSheet(table.table); + const tableCsv = tableSheet.convertToXLSX(); + + return tableSheet.convertToBuffer(tableCsv, 'xlsx'); + } + + /** + * Retrieves the cashflow sheet in CSV format. + * @param {number} tenantId + * @param {ICashFlowStatementQuery} query + * @returns {Promise} + */ + public async csv( + tenantId: number, + query: ICashFlowStatementQuery + ): Promise { + const table = await this.cashflowSheetTable.table(tenantId, query); + + const tableSheet = new TableSheet(table.table); + const tableCsv = tableSheet.convertToCSV(); + + return tableCsv; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashflowSheetApplication.ts b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashflowSheetApplication.ts new file mode 100644 index 000000000..72a587f52 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashflowSheetApplication.ts @@ -0,0 +1,75 @@ +import { Inject, Service } from 'typedi'; +import { CashflowExportInjectable } from './CashflowExportInjectable'; +import { ICashFlowStatementQuery } from '@/interfaces'; +import CashFlowStatementService from './CashFlowService'; +import { CashflowTableInjectable } from './CashflowTableInjectable'; +import { CashflowTablePdfInjectable } from './CashflowTablePdfInjectable'; + +@Service() +export class CashflowSheetApplication { + @Inject() + private cashflowExport: CashflowExportInjectable; + + @Inject() + private cashflowSheet: CashFlowStatementService; + + @Inject() + private cashflowTable: CashflowTableInjectable; + + @Inject() + private cashflowPdf: CashflowTablePdfInjectable; + + /** + * Retrieves the cashflow sheet + * @param {number} tenantId + * @param {ICashFlowStatementQuery} query + */ + public async sheet(tenantId: number, query: ICashFlowStatementQuery) { + return this.cashflowSheet.cashFlow(tenantId, query); + } + + /** + * Retrieves the cashflow sheet in table format. + * @param {number} tenantId + * @param {ICashFlowStatementQuery} query + */ + public async table(tenantId: number, query: ICashFlowStatementQuery) { + return this.cashflowTable.table(tenantId, query); + } + + /** + * Retrieves the cashflow sheet in XLSX format. + * @param {number} tenantId + * @param {ICashFlowStatementQuery} query + * @returns {Promise} + */ + public async xlsx(tenantId: number, query: ICashFlowStatementQuery) { + return this.cashflowExport.xlsx(tenantId, query); + } + + /** + * Retrieves the cashflow sheet in CSV format. + * @param {number} tenantId + * @param {ICashFlowStatementQuery} query + * @returns {Promise} + */ + public async csv( + tenantId: number, + query: ICashFlowStatementQuery + ): Promise { + return this.cashflowExport.csv(tenantId, query); + } + + /** + * Retrieves the cashflow sheet in pdf format. + * @param {number} tenantId + * @param {ICashFlowStatementQuery} query + * @returns {Promise} + */ + public async pdf( + tenantId: number, + query: ICashFlowStatementQuery + ): Promise { + return this.cashflowPdf.pdf(tenantId, query); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashflowSheetMeta.ts b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashflowSheetMeta.ts new file mode 100644 index 000000000..3a1dd40dc --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashflowSheetMeta.ts @@ -0,0 +1,36 @@ +import { Inject, Service } from 'typedi'; +import moment from 'moment'; +import { ICashFlowStatementMeta, ICashFlowStatementQuery } from '@/interfaces'; +import { FinancialSheetMeta } from '../FinancialSheetMeta'; + +@Service() +export class CashflowSheetMeta { + @Inject() + private financialSheetMeta: FinancialSheetMeta; + + /** + * CAshflow sheet meta. + * @param {number} tenantId + * @param {ICashFlowStatementQuery} query + * @returns {Promise} + */ + public async meta( + tenantId: number, + query: ICashFlowStatementQuery + ): Promise { + const meta = await this.financialSheetMeta.meta(tenantId); + const formattedToDate = moment(query.toDate).format('YYYY/MM/DD'); + const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD'); + const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`; + + const sheetName = 'Statement of Cash Flow'; + + return { + ...meta, + sheetName, + formattedToDate, + formattedFromDate, + formattedDateRange, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashflowTableInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashflowTableInjectable.ts new file mode 100644 index 000000000..0a54071f2 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashflowTableInjectable.ts @@ -0,0 +1,37 @@ +import { Inject, Service } from "typedi"; +import { ICashFlowStatementQuery, ICashFlowStatementTable } from "@/interfaces"; +import HasTenancyService from "@/services/Tenancy/TenancyService"; +import CashFlowTable from "./CashFlowTable"; +import CashFlowStatementService from "./CashFlowService"; + +@Service() +export class CashflowTableInjectable { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private cashflowSheet: CashFlowStatementService; + + /** + * Retrieves the cash flow table. + * @returns {Promise} + */ + public async table( + tenantId: number, + query: ICashFlowStatementQuery + ): Promise { + const i18n = this.tenancy.i18n(tenantId); + + const cashflowDOO = await this.cashflowSheet.cashFlow(tenantId, query); + const cashflowTable = new CashFlowTable(cashflowDOO, i18n); + + return { + table: { + columns: cashflowTable.tableColumns(), + rows: cashflowTable.tableRows(), + }, + query: cashflowDOO.query, + meta: cashflowDOO.meta, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashflowTablePdfInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashflowTablePdfInjectable.ts new file mode 100644 index 000000000..be7cb5382 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/CashflowTablePdfInjectable.ts @@ -0,0 +1,34 @@ +import { Inject } from 'typedi'; +import { CashflowTableInjectable } from './CashflowTableInjectable'; +import { TableSheetPdf } from '../TableSheetPdf'; +import { ICashFlowStatementQuery } from '@/interfaces'; +import { HtmlTableCustomCss } from './constants'; + +export class CashflowTablePdfInjectable { + @Inject() + private cashflowTable: CashflowTableInjectable; + + @Inject() + private tableSheetPdf: TableSheetPdf; + + /** + * Converts the given cashflow sheet table to pdf. + * @param {number} tenantId - Tenant ID. + * @param {IBalanceSheetQuery} query - Balance sheet query. + * @returns {Promise} + */ + public async pdf( + tenantId: number, + query: ICashFlowStatementQuery + ): Promise { + const table = await this.cashflowTable.table(tenantId, query); + + return this.tableSheetPdf.convertToPdf( + tenantId, + table.table, + table.meta.sheetName, + table.meta.formattedDateRange, + HtmlTableCustomCss + ); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/constants.ts b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/constants.ts new file mode 100644 index 000000000..f3f1858fb --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/constants.ts @@ -0,0 +1,33 @@ +export const DISPLAY_COLUMNS_BY = { + DATE_PERIODS: 'date_periods', + TOTAL: 'total', +}; + +export const MAP_CONFIG = { childrenPath: 'children', pathFormat: 'array' }; +export const HtmlTableCustomCss = ` +table tr.row-type--accounts td { + border-top: 1px solid #bbb; +} +table tr.row-id--cash-end-period td { + border-bottom: 3px double #333; +} +table tr.row-type--total { + font-weight: 600; +} +table tr.row-type--total td { + color: #000; +} +table tr.row-type--total:not(:first-child) td { + border-top: 1px solid #bbb; +} +table .column--name, +table .cell--name { + width: 400px; +} +table .column--total, +table .cell--total, +table [class*="column--date-range"], +table [class*="cell--date-range"] { + text-align: right; +} +`; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/schema.ts b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/schema.ts new file mode 100644 index 000000000..f12520e63 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/CashFlow/schema.ts @@ -0,0 +1,75 @@ +import { ICashFlowSchemaSection, CASH_FLOW_SECTION_ID, ICashFlowStatementSectionType } from '@/interfaces'; +import { ACCOUNT_TYPE } from '@/data/AccountTypes'; + +export default [ + { + id: CASH_FLOW_SECTION_ID.OPERATING, + label: 'OPERATING ACTIVITIES', + sectionType: ICashFlowStatementSectionType.AGGREGATE, + children: [ + { + id: CASH_FLOW_SECTION_ID.NET_INCOME, + label: 'Net income', + sectionType: ICashFlowStatementSectionType.NET_INCOME, + }, + { + id: CASH_FLOW_SECTION_ID.OPERATING_ACCOUNTS, + label: 'Adjustments net income by operating activities.', + sectionType: ICashFlowStatementSectionType.ACCOUNTS, + accountsRelations: [ + { type: ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE, direction: 'mines' }, + { type: ACCOUNT_TYPE.INVENTORY, direction: 'mines' }, + { type: ACCOUNT_TYPE.NON_CURRENT_ASSET, direction: 'mines' }, + { type: ACCOUNT_TYPE.ACCOUNTS_PAYABLE, direction: 'plus' }, + { type: ACCOUNT_TYPE.CREDIT_CARD, direction: 'plus' }, + { type: ACCOUNT_TYPE.TAX_PAYABLE, direction: 'plus' }, + { type: ACCOUNT_TYPE.OTHER_CURRENT_ASSET, direction: 'mines' }, + { type: ACCOUNT_TYPE.OTHER_CURRENT_LIABILITY, direction: 'plus' }, + { type: ACCOUNT_TYPE.NON_CURRENT_LIABILITY, direction: 'plus' }, + ], + showAlways: true, + }, + ], + footerLabel: 'Net cash provided by operating activities', + }, + { + id: CASH_FLOW_SECTION_ID.INVESTMENT, + sectionType: ICashFlowStatementSectionType.ACCOUNTS, + label: 'INVESTMENT ACTIVITIES', + accountsRelations: [ + { type: ACCOUNT_TYPE.FIXED_ASSET, direction: 'mines' } + ], + footerLabel: 'Net cash provided by investing activities', + }, + { + id: CASH_FLOW_SECTION_ID.FINANCIAL, + label: 'FINANCIAL ACTIVITIES', + sectionType: ICashFlowStatementSectionType.ACCOUNTS, + accountsRelations: [ + { type: ACCOUNT_TYPE.LOGN_TERM_LIABILITY, direction: 'plus' }, + { type: ACCOUNT_TYPE.EQUITY, direction: 'plus' }, + ], + footerLabel: 'Net cash provided by financing activities', + }, + { + id: CASH_FLOW_SECTION_ID.CASH_BEGINNING_PERIOD, + sectionType: ICashFlowStatementSectionType.CASH_AT_BEGINNING, + label: 'Cash at beginning of period', + accountsRelations: [ + { type: ACCOUNT_TYPE.CASH, direction: 'plus' }, + { type: ACCOUNT_TYPE.BANK, direction: 'plus' }, + ], + }, + { + id: CASH_FLOW_SECTION_ID.NET_CASH_INCREASE, + sectionType: ICashFlowStatementSectionType.TOTAL, + equation: 'OPERATING + INVESTMENT + FINANCIAL', + label: 'NET CASH INCREASE FOR PERIOD', + }, + { + id: CASH_FLOW_SECTION_ID.CASH_END_PERIOD, + label: 'CASH AT END OF PERIOD', + sectionType: ICashFlowStatementSectionType.TOTAL, + equation: 'NET_CASH_INCREASE + CASH_BEGINNING_PERIOD', + }, +] as ICashFlowSchemaSection[]; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItems.controller.ts b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItems.controller.ts new file mode 100644 index 000000000..217e65fe2 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItems.controller.ts @@ -0,0 +1,58 @@ +import { Response } from 'express'; +import { Controller, Get, Headers, Query, Res } from '@nestjs/common'; +import { PurchasesByItemsApplication } from './PurchasesByItemsApplication'; +import { IPurchasesByItemsReportQuery } from './types/PurchasesByItems.types'; +import { AcceptType } from '@/constants/accept-type'; + +@Controller('/reports/purchases-by-items') +export class PurchasesByItemReportController { + constructor( + private readonly purchasesByItemsApp: PurchasesByItemsApplication, + ) {} + + @Get() + async purchasesByItems( + @Query() filter: IPurchasesByItemsReportQuery, + @Res() res: Response, + @Headers('accept') acceptHeader: string, + ) { + // JSON table response format. + if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) { + const table = await this.purchasesByItemsApp.table(filter); + + return res.status(200).send(table); + // CSV response format. + } else if (acceptHeader.includes(AcceptType.ApplicationCsv)) { + const buffer = await this.purchasesByItemsApp.csv(filter); + + res.setHeader('Content-Disposition', 'attachment; filename=output.csv'); + res.setHeader('Content-Type', 'text/csv'); + + return res.send(buffer); + // Xlsx response format. + } else if (acceptHeader.includes(AcceptType.ApplicationXlsx)) { + const buffer = await this.purchasesByItemsApp.xlsx(filter); + + res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx'); + res.setHeader( + 'Content-Type', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ); + return res.send(buffer); + // PDF response format. + } else if (acceptHeader.includes(AcceptType.ApplicationPdf)) { + const pdfContent = await this.purchasesByItemsApp.pdf(filter); + + res.set({ + 'Content-Type': 'application/pdf', + 'Content-Length': pdfContent.length, + }); + return res.send(pdfContent); + // Json response format. + } else { + const sheet = await this.purchasesByItemsApp.sheet(filter); + + return res.status(200).send(sheet); + } + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItems.module.ts b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItems.module.ts new file mode 100644 index 000000000..10aceed8a --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItems.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable'; +import { PurchasesByItemsService } from './PurchasesByItemsService'; +import { PurchasesByItemsPdf } from './PurchasesByItemsPdf'; +import { PurchasesByItemsExport } from './PurchasesByItemsExport'; +import { PurchasesByItemsApplication } from './PurchasesByItemsApplication'; +import { PurchasesByItemReportController } from './PurchasesByItems.controller'; + +@Module({ + providers: [ + PurchasesByItemsTableInjectable, + PurchasesByItemsService, + PurchasesByItemsExport, + PurchasesByItemsPdf, + PurchasesByItemsApplication, + ], + exports: [PurchasesByItemsApplication], + controllers: [PurchasesByItemReportController], +}) +export class PurchasesByItemsModule {} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItems.ts b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItems.ts new file mode 100644 index 000000000..5df07077e --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItems.ts @@ -0,0 +1,187 @@ +import { get, isEmpty, sumBy } from 'lodash'; +import * as R from 'ramda'; +import { allPassedConditionsPass } from '@/utils/all-conditions-passed'; +import { + IPurchasesByItemsItem, + IPurchasesByItemsReportQuery, + IPurchasesByItemsSheetData, + IPurchasesByItemsTotal, +} from './types/PurchasesByItems.types'; +import FinancialSheet from '../../common/FinancialSheet'; +import { transformToMapBy } from '@/utils/transform-to-map-by'; +import { Item } from '@/modules/Items/models/Item'; +import { InventoryTransaction } from '@/modules/InventoryCost/models/InventoryTransaction'; + +export class PurchasesByItems extends FinancialSheet{ + readonly baseCurrency: string; + readonly items: Item[]; + readonly itemsTransactions: Map; + readonly query: IPurchasesByItemsReportQuery; + + /** + * Constructor method. + * @param {IPurchasesByItemsReportQuery} query + * @param {IItem[]} items + * @param {IAccountTransaction[]} itemsTransactions + * @param {string} baseCurrency + */ + constructor( + query: IPurchasesByItemsReportQuery, + items: Item[], + itemsTransactions: InventoryTransaction[], + baseCurrency: string + ) { + super(); + this.baseCurrency = baseCurrency; + this.items = items; + this.itemsTransactions = transformToMapBy(itemsTransactions, 'itemId'); + this.query = query; + this.numberFormat = this.query.numberFormat; + } + + /** + * Retrieve the item purchase item, cost and average cost price. + * @param {number} itemId + */ + getItemTransaction(itemId: number): { + quantity: number; + cost: number; + average: number; + } { + const transaction = this.itemsTransactions.get(itemId); + + const quantity = get(transaction, 'quantity', 0); + const cost = get(transaction, 'cost', 0); + + const average = cost / quantity; + + return { quantity, cost, average }; + } + + /** + * Detarmines whether the purchase node is active. + * @param {} node + * @returns {boolean} + */ + private filterPurchaseOnlyActive = (node) => { + return node.quantityPurchased !== 0 && node.purchaseCost !== 0; + }; + + /** + * Determines whether the purchase node is not none transactions. + * @param node + * @returns {boolean} + */ + private filterPurchaseNoneTransaction = (node) => { + const anyTransaction = this.itemsTransactions.get(node.id); + + return !isEmpty(anyTransaction); + }; + + /** + * Filters sales by items nodes based on the report query. + * @param {ISalesByItemsItem} saleItem - + * @return {boolean} + */ + private purchaseByItemFilter = (node): boolean => { + const { noneTransactions, onlyActive } = this.query; + + const conditions = [ + [noneTransactions, this.filterPurchaseNoneTransaction], + [onlyActive, this.filterPurchaseOnlyActive], + ]; + return allPassedConditionsPass(conditions)(node); + }; + + /** + * Mapping the given item section. + * @param {IInventoryValuationItem} item + * @returns + */ + private itemSectionMapper = (item: Item): IPurchasesByItemsItem => { + const meta = this.getItemTransaction(item.id); + + return { + id: item.id, + name: item.name, + code: item.code, + quantityPurchased: meta.quantity, + purchaseCost: meta.cost, + averageCostPrice: meta.average, + quantityPurchasedFormatted: this.formatNumber(meta.quantity, { + money: false, + }), + purchaseCostFormatted: this.formatNumber(meta.cost), + averageCostPriceFormatted: this.formatNumber(meta.average), + currencyCode: this.baseCurrency, + }; + }; + + /** + * Detarmines whether the items post filter is active. + * @returns {boolean} + */ + private isItemsPostFilter = (): boolean => { + return isEmpty(this.query.itemsIds); + }; + + /** + * Filters purchase by items nodes. + * @param {} nodes - + * @returns + */ + private itemsFilter = (nodes) => { + return nodes.filter(this.purchaseByItemFilter); + }; + + /** + * Mappes purchase by items nodes. + * @param items + * @returns + */ + private itemsMapper = (items) => { + return items.map(this.itemSectionMapper); + }; + + /** + * Retrieve the items sections. + * @returns {IPurchasesByItemsItem[]} + */ + private itemsSection = (): IPurchasesByItemsItem[] => { + return R.compose( + R.when(this.isItemsPostFilter, this.itemsFilter), + this.itemsMapper + )(this.items); + }; + + /** + * Retrieve the total section of the sheet. + * @param {IPurchasesByItemsItem[]} items + * @returns {IPurchasesByItemsTotal} + */ + private totalSection(items: IPurchasesByItemsItem[]): IPurchasesByItemsTotal { + const quantityPurchased = sumBy(items, (item) => item.quantityPurchased); + const purchaseCost = sumBy(items, (item) => item.purchaseCost); + + return { + quantityPurchased, + purchaseCost, + quantityPurchasedFormatted: this.formatTotalNumber(quantityPurchased, { + money: false, + }), + purchaseCostFormatted: this.formatTotalNumber(purchaseCost), + currencyCode: this.baseCurrency, + }; + } + + /** + * Retrieve the sheet data. + * @returns {IInventoryValuationStatement} + */ + public reportData(): IPurchasesByItemsSheetData { + const items = this.itemsSection(); + const total = this.totalSection(items); + + return { items, total }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItemsApplication.ts b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItemsApplication.ts new file mode 100644 index 000000000..db4224aa6 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItemsApplication.ts @@ -0,0 +1,69 @@ +import { Injectable } from '@nestjs/common'; +import { PurchasesByItemsExport } from './PurchasesByItemsExport'; +import { + IPurchasesByItemsReportQuery, + IPurchasesByItemsSheet, + IPurchasesByItemsTable, +} from './types/PurchasesByItems.types'; +import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable'; +import { PurchasesByItemsService } from './PurchasesByItemsService'; +import { PurchasesByItemsPdf } from './PurchasesByItemsPdf'; + +@Injectable() +export class PurchasesByItemsApplication { + constructor( + private readonly purchasesByItemsSheetService: PurchasesByItemsService, + private readonly purchasesByItemsTableService: PurchasesByItemsTableInjectable, + private readonly purchasesByItemsExportService: PurchasesByItemsExport, + private readonly purchasesByItemsPdfService: PurchasesByItemsPdf, + ) {} + + /** + * Retrieves the purchases by items in json format. + * @param {IPurchasesByItemsReportQuery} query + * @returns + */ + public sheet( + query: IPurchasesByItemsReportQuery, + ): Promise { + return this.purchasesByItemsSheetService.purchasesByItems(query); + } + + /** + * Retrieves the purchases by items in table format. + * @param {IPurchasesByItemsReportQuery} query + * @returns {Promise} + */ + public table( + query: IPurchasesByItemsReportQuery, + ): Promise { + return this.purchasesByItemsTableService.table(query); + } + + /** + * Retrieves the purchases by items in csv format. + * @param {IPurchasesByItemsReportQuery} query + * @returns {Promise} + */ + public csv(query: IPurchasesByItemsReportQuery): Promise { + return this.purchasesByItemsExportService.csv(query); + } + + /** + * Retrieves the purchases by items in xlsx format. + * @param {IPurchasesByItemsReportQuery} query + * @returns {Promise} + */ + public xlsx(query: IPurchasesByItemsReportQuery): Promise { + return this.purchasesByItemsExportService.xlsx(query); + } + + /** + * Retrieves the purchases by items in pdf format. + * @param {IPurchasesByItemsReportQuery} filter + * @returns {Promise} + */ + public pdf(filter: IPurchasesByItemsReportQuery): Promise { + return this.purchasesByItemsPdfService.pdf(filter); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItemsExport.ts b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItemsExport.ts new file mode 100644 index 000000000..6514b657c --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItemsExport.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@nestjs/common'; +import { TableSheet } from '../../common/TableSheet'; +import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable'; +import { IPurchasesByItemsReportQuery } from './types/PurchasesByItems.types'; + +@Injectable() +export class PurchasesByItemsExport { + constructor( + private purchasesByItemsTableInjectable: PurchasesByItemsTableInjectable, + ) {} + + /** + * Retrieves the purchases by items sheet in XLSX format. + * @param {IPurchasesByItemsReportQuery} query + * @returns {Promise} + */ + public async xlsx(query: IPurchasesByItemsReportQuery): Promise { + const table = await this.purchasesByItemsTableInjectable.table(query); + + const tableSheet = new TableSheet(table.table); + const tableCsv = tableSheet.convertToXLSX(); + + return tableSheet.convertToBuffer(tableCsv, 'xlsx'); + } + + /** + * Retrieves the purchases by items sheet in CSV format. + * @param {IPurchasesByItemsReportQuery} query + * @returns {Promise} + */ + public async csv(query: IPurchasesByItemsReportQuery): Promise { + const table = await this.purchasesByItemsTableInjectable.table(query); + + const tableSheet = new TableSheet(table.table); + const tableCsv = tableSheet.convertToCSV(); + + return tableCsv; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItemsMeta.ts b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItemsMeta.ts new file mode 100644 index 000000000..11ee37480 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItemsMeta.ts @@ -0,0 +1,36 @@ +import * as moment from 'moment'; +import { Injectable } from '@nestjs/common'; +import { FinancialSheetMeta } from '../../common/FinancialSheetMeta'; +import { + IPurchasesByItemsReportQuery, + IPurchasesByItemsSheetMeta, +} from './types/PurchasesByItems.types'; + +@Injectable() +export class PurchasesByItemsMeta { + constructor( + private financialSheetMetaModel: FinancialSheetMeta, + ) {} + + /** + * Retrieve the purchases by items meta. + * @param {IPurchasesByItemsReportQuery} query + * @returns {IPurchasesByItemsSheetMeta} + */ + public async meta( + query: IPurchasesByItemsReportQuery + ): Promise { + const commonMeta = await this.financialSheetMetaModel.meta(); + const formattedToDate = moment(query.toDate).format('YYYY/MM/DD'); + const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD'); + const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`; + + return { + ...commonMeta, + sheetName: 'Purchases By Items', + formattedFromDate, + formattedToDate, + formattedDateRange, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItemsPdf.ts b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItemsPdf.ts new file mode 100644 index 000000000..7dbbd95d6 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItemsPdf.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common'; +import { TableSheetPdf } from '../../TableSheetPdf'; +import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable'; +import { IPurchasesByItemsReportQuery } from './types/PurchasesByItems.types'; +import { HtmlTableCustomCss } from './_types'; + +@Injectable() +export class PurchasesByItemsPdf { + constructor( + private readonly purchasesByItemsTable: PurchasesByItemsTableInjectable, + private readonly tableSheetPdf: TableSheetPdf, + ) {} + + /** + * Converts the given journal sheet table to pdf. + * @param {IBalanceSheetQuery} query - Balance sheet query. + * @returns {Promise} + */ + public async pdf( + query: IPurchasesByItemsReportQuery, + ): Promise { + const table = await this.purchasesByItemsTable.table(query); + + return this.tableSheetPdf.convertToPdf( + table.table, + table.meta.sheetName, + table.meta.formattedDateRange, + HtmlTableCustomCss, + ); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItemsService.ts b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItemsService.ts new file mode 100644 index 000000000..f2d0e7185 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItemsService.ts @@ -0,0 +1,112 @@ +import moment from 'moment'; +import { Inject, Injectable } from '@nestjs/common'; +import { PurchasesByItems } from './PurchasesByItems'; +import { + IPurchasesByItemsReportQuery, + IPurchasesByItemsSheet, +} from './types/PurchasesByItems.types'; +import { PurchasesByItemsMeta } from './PurchasesByItemsMeta'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { InventoryTransaction } from '@/modules/InventoryCost/models/InventoryTransaction'; +import { Item } from '@/modules/Items/models/Item'; +import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; +import { events } from '@/common/events/events'; + +@Injectable() +export class PurchasesByItemsService { + constructor( + private readonly purchasesByItemsMeta: PurchasesByItemsMeta, + private readonly eventPublisher: EventEmitter2, + private readonly tenancyContext: TenancyContext, + + @Inject(InventoryTransaction.name) + private readonly inventoryTransactionModel: typeof InventoryTransaction, + + @Inject(Item.name) + private readonly itemModel: typeof Item, + ) {} + + /** + * Defaults purchases by items filter query. + * @return {IPurchasesByItemsReportQuery} + */ + private get defaultQuery(): IPurchasesByItemsReportQuery { + return { + fromDate: moment().startOf('month').format('YYYY-MM-DD'), + toDate: moment().format('YYYY-MM-DD'), + itemsIds: [], + numberFormat: { + precision: 2, + divideOn1000: false, + showZero: false, + formatMoney: 'always', + negativeFormat: 'mines', + }, + noneTransactions: true, + onlyActive: false, + }; + } + + /** + * Retrieve balance sheet statement. + * ------------- + * @param {number} tenantId + * @param {IPurchasesByItemsReportQuery} query + * @return {Promise} + */ + public async purchasesByItems( + query: IPurchasesByItemsReportQuery, + ): Promise { + const tenant = await this.tenancyContext.getTenant(); + const filter = { + ...this.defaultQuery, + ...query, + }; + const inventoryItems = await this.itemModel.query().onBuild((q) => { + q.where('type', 'inventory'); + + if (filter.itemsIds.length > 0) { + q.whereIn('id', filter.itemsIds); + } + }); + const inventoryItemsIds = inventoryItems.map((item) => item.id); + + // Calculates the total inventory total quantity and rate `IN` transactions. + const inventoryTransactions = await this.inventoryTransactionModel + .query() + .onBuild((builder: any) => { + builder.modify('itemsTotals'); + builder.modify('INDirection'); + + // Filter the inventory items only. + builder.whereIn('itemId', inventoryItemsIds); + + // Filter the date range of the sheet. + builder.modify('filterDateRange', filter.fromDate, filter.toDate); + }); + const purchasesByItemsInstance = new PurchasesByItems( + filter, + inventoryItems, + inventoryTransactions, + tenant.metadata.baseCurrency, + ); + const purchasesByItemsData = purchasesByItemsInstance.reportData(); + + // Retrieve the purchases by items meta. + const meta = await this.purchasesByItemsMeta.meta(query); + + // Triggers `onPurchasesByItemViewed` event. + await this.eventPublisher.emitAsync( + events.reports.onPurchasesByItemViewed, + { + query, + }, + ); + + return { + data: purchasesByItemsData, + query: filter, + meta, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItemsTable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItemsTable.ts new file mode 100644 index 000000000..2aa31092b --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItemsTable.ts @@ -0,0 +1,111 @@ +import * as R from 'ramda'; +import { ROW_TYPE } from './_types'; +import { + IPurchasesByItemsItem, + IPurchasesByItemsSheetData, + IPurchasesByItemsTotal, +} from './types/PurchasesByItems.types'; +import { ITableColumn, ITableColumnAccessor, ITableRow } from '../../types/Table.types'; +import { FinancialTable } from '../../common/FinancialTable'; +import { FinancialSheetStructure } from '../../common/FinancialSheetStructure'; +import FinancialSheet from '../../common/FinancialSheet'; +import { tableRowMapper } from '../../utils/Table.utils'; + +export class PurchasesByItemsTable extends R.compose( + FinancialTable, + FinancialSheetStructure +)(FinancialSheet) { + private data: IPurchasesByItemsSheetData; + + /** + * Constructor method. + * @param data + */ + constructor(data: IPurchasesByItemsSheetData) { + super(); + this.data = data; + } + + /** + * Retrieves thge common table accessors. + * @returns {ITableColumnAccessor[]} + */ + private commonTableAccessors(): ITableColumnAccessor[] { + return [ + { key: 'item_name', accessor: 'name' }, + { key: 'quantity_purchases', accessor: 'quantityPurchasedFormatted' }, + { key: 'purchase_amount', accessor: 'purchaseCostFormatted' }, + { key: 'average_cost', accessor: 'averageCostPriceFormatted' }, + ]; + } + + /** + * Retrieves the common table columns. + * @returns {ITableColumn[]} + */ + private commonTableColumns(): ITableColumn[] { + return [ + { label: 'Item name', key: 'item_name' }, + { label: 'Quantity Purchased', key: 'quantity_purchases' }, + { label: 'Purchase Amount', key: 'purchase_amount' }, + { label: 'Average Price', key: 'average_cost' }, + ]; + } + + /** + * Maps the given item node to table row. + * @param {IPurchasesByItemsItem} item + * @returns {ITableRow} + */ + private itemMap = (item: IPurchasesByItemsItem): ITableRow => { + const columns = this.commonTableAccessors(); + const meta = { + rowTypes: [ROW_TYPE.ITEM], + }; + return tableRowMapper(item, columns, meta); + }; + + /** + * Maps the given items nodes to table rows. + * @param {IPurchasesByItemsItem[]} items - Items nodes. + * @returns {ITableRow[]} + */ + private itemsMap = (items: IPurchasesByItemsItem[]): ITableRow[] => { + return R.map(this.itemMap)(items); + }; + + /** + * Maps the given total node to table rows. + * @param {IPurchasesByItemsTotal} total + * @returns {ITableRow} + */ + private totalNodeMap = (total: IPurchasesByItemsTotal): ITableRow => { + const columns = this.commonTableAccessors(); + const meta = { + rowTypes: [ROW_TYPE.TOTAL], + }; + return tableRowMapper(total, columns, meta); + }; + + /** + * Retrieves the table columns. + * @returns {ITableColumn[]} + */ + public tableColumns(): ITableColumn[] { + const columns = this.commonTableColumns(); + return R.compose(this.tableColumnsCellIndexing)(columns); + } + + /** + * Retrieves the table rows. + * @returns {ITableRow[]} + */ + public tableData(): ITableRow[] { + const itemsRows = this.itemsMap(this.data.items); + const totalRow = this.totalNodeMap(this.data.total); + + return R.compose( + R.when(R.always(R.not(R.isEmpty(itemsRows))), R.append(totalRow)) + )(itemsRows) as ITableRow[]; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItemsTableInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItemsTableInjectable.ts new file mode 100644 index 000000000..07b705a66 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/PurchasesByItemsTableInjectable.ts @@ -0,0 +1,38 @@ +import { + IPurchasesByItemsReportQuery, + IPurchasesByItemsTable, +} from './types/PurchasesByItems.types'; +import { PurchasesByItemsService } from './PurchasesByItemsService'; +import { PurchasesByItemsTable } from './PurchasesByItemsTable'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class PurchasesByItemsTableInjectable { + constructor( + private readonly purchasesByItemsSheet: PurchasesByItemsService, + ) {} + + /** + * Retrieves the purchases by items table format. + * @param {number} tenantId + * @param {IPurchasesByItemsReportQuery} filter + * @returns {Promise} + */ + public async table( + filter: IPurchasesByItemsReportQuery, + ): Promise { + const { data, query, meta } = + await this.purchasesByItemsSheet.purchasesByItems(filter); + + const table = new PurchasesByItemsTable(data); + + return { + table: { + columns: table.tableColumns(), + rows: table.tableData(), + }, + meta, + query, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/_types.ts b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/_types.ts new file mode 100644 index 000000000..bb4927958 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/_types.ts @@ -0,0 +1,23 @@ +export enum ROW_TYPE { + TOTAL = 'TOTAL', + ITEM = 'ITEM', +} + +export const HtmlTableCustomCss = ` +table tr.row-type--total td { + border-top: 1px solid #bbb; + border-bottom: 3px double #000; + font-weight: 600; +} +table .column--item_name{ + width: 300px; +} +table .column--quantity_purchases, +table .column--purchase_amount, +table .column--average_cost, +table .cell--quantity_purchases, +table .cell--purchase_amount, +table .cell--average_cost{ + text-align: right; +} +`; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/types/PurchasesByItems.types.ts b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/types/PurchasesByItems.types.ts new file mode 100644 index 000000000..648758d03 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/PurchasesByItems/types/PurchasesByItems.types.ts @@ -0,0 +1,60 @@ +import { + IFinancialSheetCommonMeta, + INumberFormatQuery, +} from '@/modules/FinancialStatements/types/Report.types'; +import { IFinancialTable } from '@/modules/FinancialStatements/types/Table.types'; + +export interface IPurchasesByItemsReportQuery { + fromDate: Date | string; + toDate: Date | string; + itemsIds: number[]; + numberFormat: INumberFormatQuery; + noneTransactions: boolean; + onlyActive: boolean; +} + +export interface IPurchasesByItemsSheetMeta extends IFinancialSheetCommonMeta { + formattedFromDate: string; + formattedToDate: string; + formattedDateRange: string; +} + +export interface IPurchasesByItemsItem { + id: number; + name: string; + code: string; + soldCost: number; + + averageSellPrice: number; + averageSellPriceFormatted: string; + + quantityPurchased: number; + quantityPurchasedFormatted: string; + + soldCostFormatted: string; + currencyCode: string; +} + +export interface IPurchasesByItemsTotal { + quantityPurchased: number; + quantityPurchasedFormatted: string; + purchaseCost: number; + purchaseCostFormatted: string; + currencyCode: string; +} + +export type IPurchasesByItemsSheetData = { + items: IPurchasesByItemsItem[]; + total: IPurchasesByItemsTotal; +}; + +export interface IPurchasesByItemsSheet { + data: IPurchasesByItemsSheetData; + query: IPurchasesByItemsReportQuery; + meta: IPurchasesByItemsSheetMeta; +} + +export interface IPurchasesByItemsTable extends IFinancialTable { + query: IPurchasesByItemsReportQuery; + meta: IPurchasesByItemsSheetMeta; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/types/Report.types.ts b/packages/server-nest/src/modules/FinancialStatements/types/Report.types.ts new file mode 100644 index 000000000..232aa7e3b --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/types/Report.types.ts @@ -0,0 +1,72 @@ +export interface INumberFormatQuery { + precision: number; + divideOn1000: boolean; + showZero: boolean; + formatMoney: 'total' | 'always' | 'none'; + negativeFormat: 'parentheses' | 'mines'; +} + +export interface IFormatNumberSettings { + precision?: number; + divideOn1000?: boolean; + excerptZero?: boolean; + negativeFormat?: 'parentheses' | 'mines'; + thousand?: string; + decimal?: string; + zeroSign?: string; + currencyCode?: string; + money?: boolean; +} + +export enum ReportsAction { + READ_BALANCE_SHEET = 'read-balance-sheet', + READ_TRIAL_BALANCE_SHEET = 'read-trial-balance-sheet', + READ_PROFIT_LOSS = 'read-profit-loss', + READ_JOURNAL = 'read-journal', + READ_GENERAL_LEDGET = 'read-general-ledger', + READ_CASHFLOW = 'read-cashflow', + READ_AR_AGING_SUMMARY = 'read-ar-aging-summary', + READ_AP_AGING_SUMMARY = 'read-ap-aging-summary', + READ_PURCHASES_BY_ITEMS = 'read-purchases-by-items', + READ_SALES_BY_ITEMS = 'read-sales-by-items', + READ_CUSTOMERS_TRANSACTIONS = 'read-customers-transactions', + READ_VENDORS_TRANSACTIONS = 'read-vendors-transactions', + READ_CUSTOMERS_SUMMARY_BALANCE = 'read-customers-summary-balance', + READ_VENDORS_SUMMARY_BALANCE = 'read-vendors-summary-balance', + READ_INVENTORY_VALUATION_SUMMARY = 'read-inventory-valuation-summary', + READ_INVENTORY_ITEM_DETAILS = 'read-inventory-item-details', + READ_CASHFLOW_ACCOUNT_TRANSACTION = 'read-cashflow-account-transactions', + READ_PROJECT_PROFITABILITY_SUMMARY = 'read-project-profitability-summary', + READ_SALES_TAX_LIABILITY_SUMMARY = 'read-sales-tax-liability-summary', +} + +export interface IFinancialSheetBranchesQuery { + branchesIds?: number[]; +} + +export interface IFinancialSheetCommonMeta { + organizationName: string; + baseCurrency: string; + dateFormat: string; + isCostComputeRunning: boolean; + sheetName: string; +} + +export enum IFinancialDatePeriodsUnit { + Day = 'day', + Month = 'month', + Year = 'year', +} + +export enum IAccountTransactionsGroupBy { + Quarter = 'quarter', + Year = 'year', + Day = 'day', + Month = 'month', + Week = 'week', +} + +export interface IDateRange { + fromDate: Date; + toDate: Date; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/types/Table.types.ts b/packages/server-nest/src/modules/FinancialStatements/types/Table.types.ts new file mode 100644 index 000000000..a567d2d92 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/types/Table.types.ts @@ -0,0 +1,40 @@ +export interface IColumnMapperMeta { + key: string; + accessor?: string; + value?: string; +} + +export interface ITableCell { + value: string; + key: string; +} + +export type ITableRow = { + cells: ITableCell[]; +}; + +export interface ITableColumn { + key: string; + label: string; + cellIndex?: number; + children?: ITableColumn[]; +} + +export interface ITable { + columns: ITableColumn[]; + data: ITableRow[]; +} + +export interface ITableColumnAccessor { + key: string; + accessor: string; +} + +export interface ITableData { + columns: ITableColumn[]; + rows: ITableRow[]; +} + +export interface IFinancialTable { + table: ITableData; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/utils.ts b/packages/server-nest/src/modules/FinancialStatements/utils.ts new file mode 100644 index 000000000..5d304ca6f --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/utils.ts @@ -0,0 +1,30 @@ +import { kebabCase } from 'lodash'; +import { ITableRow } from '@/interfaces'; + +export const formatNumber = (balance, { noCents, divideOn1000 }): string => { + let formattedBalance: number = parseFloat(balance); + + if (noCents) { + formattedBalance = parseInt(formattedBalance, 10); + } + if (divideOn1000) { + formattedBalance /= 1000; + } + return formattedBalance; +}; + +export const tableClassNames = (rows: ITableRow[]) => { + return rows.map((row) => { + const classNames = + row?.rowTypes?.map((rowType) => `row-type--${kebabCase(rowType)}`) || []; + + if (row.id) { + classNames.push(`row-id--${kebabCase(row.id)}`); + } + + return { + ...row, + classNames, + }; + }); +}; diff --git a/packages/server-nest/src/modules/FinancialStatements/utils/Table.utils.ts b/packages/server-nest/src/modules/FinancialStatements/utils/Table.utils.ts new file mode 100644 index 000000000..ae2c63aeb --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/utils/Table.utils.ts @@ -0,0 +1,34 @@ +import { get } from 'lodash'; +import { IColumnMapperMeta, ITableRow } from '../types/Table.types'; + +export function tableMapper( + data: Object[], + columns: IColumnMapperMeta[], + rowsMeta +): ITableRow[] { + return data.map((object) => tableRowMapper(object, columns, rowsMeta)); +} + +function getAccessor(object, accessor) { + return typeof accessor === 'function' + ? accessor(object) + : get(object, accessor); +} + +export function tableRowMapper( + object: Object, + columns: IColumnMapperMeta[], + rowMeta +): ITableRow { + const cells = columns.map((column) => ({ + key: column.key, + value: column.value + ? column.value + : getAccessor(object, column.accessor) || '', + })); + + return { + cells, + ...rowMeta, + }; +} diff --git a/packages/server-nest/src/utils/all-conditions-passed.ts b/packages/server-nest/src/utils/all-conditions-passed.ts new file mode 100644 index 000000000..12d9d9d28 --- /dev/null +++ b/packages/server-nest/src/utils/all-conditions-passed.ts @@ -0,0 +1,13 @@ +import * as R from 'ramda'; +/** + * All passed conditions should pass. + * @param condsPairFilters + * @returns + */ +export const allPassedConditionsPass = (condsPairFilters: any[]): Function => { + const filterCallbacks = condsPairFilters + .filter((cond) => cond[0]) + .map((cond) => cond[1]); + + return R.allPass(filterCallbacks); +}; diff --git a/packages/server-nest/src/utils/date-range-collection.ts b/packages/server-nest/src/utils/date-range-collection.ts new file mode 100644 index 000000000..3e0d3908d --- /dev/null +++ b/packages/server-nest/src/utils/date-range-collection.ts @@ -0,0 +1,57 @@ +import * as moment from 'moment'; + +export const dateRangeCollection = ( + fromDate, + toDate, + addType: moment.unitOfTime.StartOf = 'day', + increment = 1, +) => { + const collection = []; + const momentFromDate = moment(fromDate); + let dateFormat = ''; + + switch (addType) { + case 'day': + default: + dateFormat = 'YYYY-MM-DD'; + break; + case 'month': + case 'quarter': + dateFormat = 'YYYY-MM'; + break; + case 'year': + dateFormat = 'YYYY'; + break; + } + for ( + let i = momentFromDate; + i.isBefore(toDate, addType) || i.isSame(toDate, addType); + i.add(increment, `${addType}s`) + ) { + collection.push(i.endOf(addType).format(dateFormat)); + } + return collection; +}; + +export const dateRangeFromToCollection = ( + fromDate: moment.MomentInput, + toDate: moment.MomentInput, + addType: moment.unitOfTime.StartOf = 'day', + increment = 1, +) => { + const collection = []; + const momentFromDate = moment(fromDate); + const dateFormat = 'YYYY-MM-DD'; + + for ( + let i = momentFromDate; + i.isBefore(toDate, addType) || i.isSame(toDate, addType); + i.add(increment, `${addType}s`) + ) { + collection.push({ + fromDate: i.startOf(addType).format(dateFormat), + toDate: i.endOf(addType).format(dateFormat), + }); + } + return collection; +}; diff --git a/packages/server-nest/src/utils/increment.ts b/packages/server-nest/src/utils/increment.ts new file mode 100644 index 000000000..0ef538abf --- /dev/null +++ b/packages/server-nest/src/utils/increment.ts @@ -0,0 +1,9 @@ + +export const increment = (n: number = 0) => { + let counter = n; + + return () => { + counter += 1; + return counter; + }; +}; diff --git a/packages/server/src/services/Inventory/Inventory.ts b/packages/server/src/services/Inventory/Inventory.ts index 9a92685ca..db1a3fb72 100644 --- a/packages/server/src/services/Inventory/Inventory.ts +++ b/packages/server/src/services/Inventory/Inventory.ts @@ -251,7 +251,6 @@ export default class InventoryService { /** * Records the inventory transactions from items entries that have (inventory) type. - * * @param {number} tenantId * @param {number} transactionId * @param {string} transactionType diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 05d8fcd90..ef8a17409 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -664,6 +664,9 @@ importers: uuid: specifier: ^10.0.0 version: 10.0.0 + xlsx: + specifier: ^0.18.5 + version: 0.18.5 yup: specifier: ^0.28.1 version: 0.28.5