mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
feat(nestjs): migrate to NestJS
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PurchasesByItemsModule } from './modules/PurchasesByItems/PurchasesByItems.module';
|
||||
import { CustomerBalanceSummaryModule } from './modules/CustomerBalanceSummary/CustomerBalanceSummary.module';
|
||||
import { SalesByItemsModule } from './modules/SalesByItems/SalesByItems.module';
|
||||
import { GeneralLedgerModule } from './modules/GeneralLedger/GeneralLedger.module';
|
||||
import { TrialBalanceSheetModule } from './modules/TrialBalanceSheet/TrialBalanceSheet.module';
|
||||
import { TransactionsByVendorModule } from './modules/TransactionsByVendor/TransactionsByVendor.module';
|
||||
import { TransactionsByCustomerModule } from './modules/TransactionsByCustomer/TransactionsByCustomer.module';
|
||||
import { TransactionsByReferenceModule } from './modules/TransactionsByReference/TransactionByReference.module';
|
||||
import { ARAgingSummaryModule } from './modules/ARAgingSummary/ARAgingSummary.module';
|
||||
import { APAgingSummaryModule } from './modules/APAgingSummary/APAgingSummary.module';
|
||||
import { InventoryItemDetailsModule } from './modules/InventoryItemDetails/InventoryItemDetails.module';
|
||||
import { InventoryValuationSheetModule } from './modules/InventoryValuationSheet/InventoryValuationSheet.module';
|
||||
import { SalesTaxLiabilityModule } from './modules/SalesTaxLiabilitySummary/SalesTaxLiability.module';
|
||||
import { JournalSheetModule } from './modules/JournalSheet/JournalSheet.module';
|
||||
import { ProfitLossSheetModule } from './modules/ProfitLossSheet/ProfitLossSheet.module';
|
||||
import { CashflowStatementModule } from './modules/CashFlowStatement/CashflowStatement.module';
|
||||
import { VendorBalanceSummaryModule } from './modules/VendorBalanceSummary/VendorBalanceSummary.module';
|
||||
import { BalanceSheetModule } from './modules/BalanceSheet/BalanceSheet.module';
|
||||
|
||||
@Module({
|
||||
providers: [],
|
||||
imports: [
|
||||
BalanceSheetModule,
|
||||
PurchasesByItemsModule,
|
||||
CustomerBalanceSummaryModule,
|
||||
VendorBalanceSummaryModule,
|
||||
SalesByItemsModule,
|
||||
GeneralLedgerModule,
|
||||
TrialBalanceSheetModule,
|
||||
TransactionsByVendorModule,
|
||||
TransactionsByCustomerModule,
|
||||
TransactionsByReferenceModule,
|
||||
ARAgingSummaryModule,
|
||||
APAgingSummaryModule,
|
||||
InventoryItemDetailsModule,
|
||||
InventoryValuationSheetModule,
|
||||
SalesTaxLiabilityModule,
|
||||
JournalSheetModule,
|
||||
ProfitLossSheetModule,
|
||||
CashflowStatementModule,
|
||||
],
|
||||
})
|
||||
export class FinancialStatementsModule {}
|
||||
@@ -0,0 +1,113 @@
|
||||
import * as R from 'ramda';
|
||||
import { memoize } from 'lodash';
|
||||
import {
|
||||
IAccountTransactionsGroupBy,
|
||||
IFinancialDatePeriodsUnit,
|
||||
IFormatNumberSettings,
|
||||
} from '../types/Report.types';
|
||||
import { dateRangeFromToCollection } from '@/utils/date-range-collection';
|
||||
import { FinancialDateRanges } from './FinancialDateRanges';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialSheet } from './FinancialSheet';
|
||||
import { IFinancialSheetTotalPeriod } from '../modules/BalanceSheet/BalanceSheet.types';
|
||||
|
||||
export const FinancialDatePeriods = <T extends GConstructor<FinancialSheet>>(
|
||||
Base: T,
|
||||
) =>
|
||||
class extends R.pipe(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: moment.unitOfTime.StartOf) => {
|
||||
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];
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
import * as moment from 'moment';
|
||||
import { IDateRange, IFinancialDatePeriodsUnit } from '../types/Report.types';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialSheet } from './FinancialSheet';
|
||||
import { DateInput } from '@/common/types/Date';
|
||||
|
||||
export const FinancialDateRanges = <T extends GConstructor<FinancialSheet>>(
|
||||
Base: T,
|
||||
) =>
|
||||
class extends Base {
|
||||
/**
|
||||
* Retrieve previous period (PP) date of the given date.
|
||||
* @param {Date} date - Date.
|
||||
* @param {number} value - Value.
|
||||
* @param {IFinancialDatePeriodsUnit} unit - Unit of time.
|
||||
* @returns {Date}
|
||||
*/
|
||||
public getPreviousPeriodDate = (
|
||||
date: DateInput,
|
||||
value: number = 1,
|
||||
unit: IFinancialDatePeriodsUnit = IFinancialDatePeriodsUnit.Day,
|
||||
): Date => {
|
||||
return moment(date).subtract(value, unit).toDate();
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the different between two dates.
|
||||
* @param {Date} fromDate
|
||||
* @param {Date} toDate
|
||||
* @returns {number}
|
||||
*/
|
||||
public getPreviousPeriodDiff = (fromDate: DateInput, toDate: DateInput) => {
|
||||
return moment(toDate).diff(fromDate, 'days') + 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the periods period dates.
|
||||
* @param {DateInput} fromDate - From date.
|
||||
* @param {DateInput} toDate - To date.
|
||||
* @param {IFinancialDatePeriodsUnit} unit - Unit of time.
|
||||
* @param {number} amount - Amount of time.
|
||||
* @returns {IDateRange}
|
||||
*/
|
||||
public getPreviousPeriodDateRange = (
|
||||
fromDate: DateInput,
|
||||
toDate: DateInput,
|
||||
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 {DateInput} fromDate - From date.
|
||||
* @param {DateInput} toDate - To date.
|
||||
* @returns {IDateRange}
|
||||
*/
|
||||
public getPPTotalDateRange = (
|
||||
fromDate: DateInput,
|
||||
toDate: DateInput,
|
||||
): 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 - From date.
|
||||
* @param {Date} toDate - To date.
|
||||
* @param {IFinancialDatePeriodsUnit} unit - Unit of time.
|
||||
* @returns {IDateRange}
|
||||
*/
|
||||
public getPPDatePeriodDateRange = (
|
||||
fromDate: DateInput,
|
||||
toDate: DateInput,
|
||||
unit: IFinancialDatePeriodsUnit,
|
||||
): IDateRange => {
|
||||
return this.getPreviousPeriodDateRange(fromDate, toDate, unit, 1);
|
||||
};
|
||||
|
||||
// ------------------------
|
||||
// Previous Year (PY).
|
||||
// ------------------------
|
||||
/**
|
||||
* Retrieve the previous year of the given date.
|
||||
* @param {DateInput} date
|
||||
* @returns {Date}
|
||||
*/
|
||||
getPreviousYearDate = (date: DateInput) => {
|
||||
return moment(date).subtract(1, 'years').toDate();
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves previous year date range.
|
||||
* @param {DateInput} fromDate - From date.
|
||||
* @param {DateInput} toDate - To date.
|
||||
* @returns {IDateRange}
|
||||
*/
|
||||
public getPreviousYearDateRange = (
|
||||
fromDate: DateInput,
|
||||
toDate: DateInput,
|
||||
): IDateRange => {
|
||||
const PYFromDate = this.getPreviousYearDate(fromDate);
|
||||
const PYToDate = this.getPreviousYearDate(toDate);
|
||||
|
||||
return { fromDate: PYFromDate, toDate: PYToDate };
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import * as mathjs from 'mathjs';
|
||||
import * as R from 'ramda';
|
||||
import { omit, get, mapValues } from 'lodash';
|
||||
import { FinancialSheetStructure } from './FinancialSheetStructure';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialSheet } from './FinancialSheet';
|
||||
|
||||
export const FinancialEvaluateEquation = <
|
||||
T extends GConstructor<FinancialSheet>,
|
||||
>(
|
||||
Base: T
|
||||
) =>
|
||||
class FinancialEvaluateEquation extends R.pipe(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);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
// @ts-nocheck
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { FinancialSheet } from './FinancialSheet';
|
||||
import { IFinancialCommonNode } from '../types/Report.types';
|
||||
|
||||
export const FinancialFilter = <T extends GConstructor<FinancialSheet>>(
|
||||
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;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import * as R from 'ramda';
|
||||
import { get, isEmpty } from 'lodash';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialSheet } from './FinancialSheet';
|
||||
|
||||
export const FinancialHorizTotals = <T extends GConstructor<FinancialSheet>>(
|
||||
Base: T,
|
||||
) =>
|
||||
class FinancialHorizTotals extends Base {
|
||||
/**
|
||||
* Associate percentage to the given node.
|
||||
*/
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Associate horizontal percentage total to the given 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);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,133 @@
|
||||
import { sumBy } from 'lodash';
|
||||
import {
|
||||
IFinancialDatePeriodsUnit,
|
||||
IFinancialNodeWithPreviousPeriod,
|
||||
} from '../types/Report.types';
|
||||
import * as R from 'ramda';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialSheet } from './FinancialSheet';
|
||||
import { FinancialDatePeriods } from './FinancialDatePeriods';
|
||||
import { IProfitLossSheetAccountNode } from '../modules/ProfitLossSheet/ProfitLossSheet.types';
|
||||
|
||||
export const FinancialPreviousPeriod = <T extends GConstructor<FinancialSheet>>(
|
||||
Base: T,
|
||||
) =>
|
||||
class extends R.pipe(FinancialDatePeriods)(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`,
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
import * as R from 'ramda';
|
||||
import { sumBy } from 'lodash';
|
||||
import {
|
||||
IFinancialCommonHorizDatePeriodNode,
|
||||
IFinancialCommonNode,
|
||||
IFinancialNodeWithPreviousYear,
|
||||
} from '../types/Report.types';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialSheet } from './FinancialSheet';
|
||||
import { FinancialDatePeriods } from './FinancialDatePeriods';
|
||||
|
||||
export const FinancialPreviousYear = <T extends GConstructor<FinancialSheet>>(
|
||||
Base: T,
|
||||
) =>
|
||||
class extends R.compose(FinancialDatePeriods)(Base) {
|
||||
// ---------------------------
|
||||
// # Common Node
|
||||
// ---------------------------
|
||||
/**
|
||||
* Assoc previous year change attribute to account node.
|
||||
* @param {IFinancialCommonNode & IFinancialNodeWithPreviousYear} accountNode
|
||||
* @returns {IFinancialNodeWithPreviousYear}
|
||||
*/
|
||||
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`,
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export default class FinancialReportService {
|
||||
transformOrganizationMeta(tenant) {
|
||||
return {
|
||||
organizationName: tenant.metadata?.name,
|
||||
baseCurrency: tenant.metadata?.baseCurrency,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import * as R from 'ramda';
|
||||
import { FinancialSheetStructure } from './FinancialSheetStructure';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialSheet } from './FinancialSheet';
|
||||
|
||||
export const FinancialSchema = <T extends GConstructor<FinancialSheet>>(
|
||||
Base: T,
|
||||
) =>
|
||||
class FinancialSchema extends FinancialSheetStructure(Base) {
|
||||
/**
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
getSchema() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string|number} id
|
||||
* @returns
|
||||
*/
|
||||
public getSchemaNodeById = (id: string | number) => {
|
||||
const schema = this.getSchema();
|
||||
|
||||
return this.findNodeDeep(schema, (node) => node.id === id);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,177 @@
|
||||
import * as moment from 'moment';
|
||||
import {
|
||||
IFormatNumberSettings,
|
||||
INumberFormatQuery,
|
||||
} from '../types/Report.types';
|
||||
import { formatNumber } from '@/utils/format-number';
|
||||
import { IFinancialTableTotal } from '../types/Table.types';
|
||||
|
||||
export class FinancialSheet {
|
||||
public numberFormat: INumberFormatQuery = {
|
||||
precision: 2,
|
||||
divideOn1000: false,
|
||||
showZero: false,
|
||||
formatMoney: 'total',
|
||||
negativeFormat: 'mines',
|
||||
};
|
||||
public 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,
|
||||
): IFinancialTableTotal {
|
||||
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,
|
||||
): IFinancialTableTotal {
|
||||
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: moment.MomentInput, 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 };
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FinancialSheetMeta } from './FinancialSheetMeta';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
import { TableSheetPdf } from './TableSheetPdf';
|
||||
import { TemplateInjectableModule } from '@/modules/TemplateInjectable/TemplateInjectable.module';
|
||||
import { ChromiumlyTenancyModule } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.module';
|
||||
|
||||
@Module({
|
||||
imports: [TemplateInjectableModule, ChromiumlyTenancyModule],
|
||||
providers: [
|
||||
FinancialSheetMeta,
|
||||
TenancyContext,
|
||||
TableSheetPdf,
|
||||
],
|
||||
exports: [FinancialSheetMeta, TableSheetPdf],
|
||||
})
|
||||
export class FinancialSheetCommonModule {}
|
||||
@@ -0,0 +1,33 @@
|
||||
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 tenancyContext: TenancyContext) {}
|
||||
|
||||
/**
|
||||
* Retrieves the common meta data of the financial sheet.
|
||||
* @returns {Promise<IFinancialSheetCommonMeta>}
|
||||
*/
|
||||
async meta(): Promise<IFinancialSheetCommonMeta> {
|
||||
const tenantMetadata = await this.tenancyContext.getTenantMetadata();
|
||||
|
||||
const organizationName = tenantMetadata.name;
|
||||
const baseCurrency = tenantMetadata.baseCurrency;
|
||||
const dateFormat = tenantMetadata.dateFormat;
|
||||
|
||||
// const isCostComputeRunning =
|
||||
// this.inventoryService.isItemsCostComputeRunning();
|
||||
|
||||
const isCostComputeRunning = false;
|
||||
|
||||
return {
|
||||
organizationName,
|
||||
baseCurrency,
|
||||
dateFormat,
|
||||
isCostComputeRunning,
|
||||
sheetName: '',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import * as R from 'ramda';
|
||||
import { set, sumBy } from 'lodash';
|
||||
import {
|
||||
mapValuesDeepReverse,
|
||||
mapValuesDeep,
|
||||
mapValues,
|
||||
filterDeep,
|
||||
reduceDeep,
|
||||
findValueDeep,
|
||||
filterNodesDeep,
|
||||
} from '@/utils/deepdash';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialSheet } from './FinancialSheet';
|
||||
|
||||
export const FinancialSheetStructure = <T extends GConstructor<FinancialSheet>>(
|
||||
Base: T,
|
||||
) =>
|
||||
class FinancialSheetStructure 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');
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
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 { GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialSheetStructure } from './FinancialSheetStructure';
|
||||
import { I18nService } from 'nestjs-i18n';
|
||||
import { FinancialSheet } from './FinancialSheet';
|
||||
|
||||
enum IROW_TYPE {
|
||||
TOTAL = 'TOTAL',
|
||||
}
|
||||
|
||||
export const FinancialTable = <T extends GConstructor<FinancialSheet>>(
|
||||
Base: T
|
||||
) =>
|
||||
class extends R.pipe(FinancialSheetStructure)(Base) {
|
||||
public readonly i18n: I18nService;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
});
|
||||
};
|
||||
|
||||
public addTotalRow = (node: ITableRow) => {
|
||||
const clonedNode = clone(node);
|
||||
|
||||
if (clonedNode.children) {
|
||||
const cells = cloneDeep(node.cells);
|
||||
cells[0].value = this.i18n.t('financial_sheet.total_row', {
|
||||
args: {
|
||||
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);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
// @ts-nocheck
|
||||
import * as moment from 'moment';
|
||||
import { ITableColumn, ITableColumnAccessor } from '../types/Table.types';
|
||||
import { IDateRange } from '../types/Report.types';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { I18nService } from 'nestjs-i18n';
|
||||
import { FinancialSheet } from './FinancialSheet';
|
||||
|
||||
export const FinancialTablePreviousPeriod = <
|
||||
T extends GConstructor<FinancialSheet>,
|
||||
>(
|
||||
Base: T,
|
||||
) =>
|
||||
class extends Base {
|
||||
public readonly i18n: I18nService;
|
||||
|
||||
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.t(`financial_sheet.previoud_period_date`, {
|
||||
args: { date: PPFormatted, }
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve previous period change column.
|
||||
* @returns {ITableColumn}
|
||||
*/
|
||||
public getPreviousPeriodChangeColumn = (): ITableColumn => {
|
||||
return {
|
||||
key: 'previous_period_change',
|
||||
label: this.i18n.t('fianncial_sheet.previous_period_change'),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve previous period percentage column.
|
||||
* @returns {ITableColumn}
|
||||
*/
|
||||
public getPreviousPeriodPercentageColumn = (): ITableColumn => {
|
||||
return {
|
||||
key: 'previous_period_percentage',
|
||||
label: this.i18n.t('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`,
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,146 @@
|
||||
// @ts-nocheck
|
||||
import * as moment from 'moment';
|
||||
import { ITableColumn, ITableColumnAccessor } from '../types/Table.types';
|
||||
import { IDateRange } from '../types/Report.types';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { I18nService } from 'nestjs-i18n';
|
||||
import { FinancialSheet } from './FinancialSheet';
|
||||
|
||||
export const FinancialTablePreviousYear = <
|
||||
T extends GConstructor<FinancialSheet>,
|
||||
>(
|
||||
Base: T,
|
||||
) =>
|
||||
class extends Base {
|
||||
public readonly i18n: I18nService;
|
||||
|
||||
/**
|
||||
* Retrieves the total previous year date.
|
||||
* @returns {Date}
|
||||
*/
|
||||
public 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.t('financial_sheet.previous_year_date', {
|
||||
args: { date: PYFormatted },
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve previous year change column.
|
||||
* @returns {ITableColumn}
|
||||
*/
|
||||
public getPreviousYearChangeColumn = (): ITableColumn => {
|
||||
return {
|
||||
key: 'previous_year_change',
|
||||
label: this.i18n.t('financial_sheet.previous_year_change'),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve previous year percentage column.
|
||||
* @returns {ITableColumn}
|
||||
*/
|
||||
public getPreviousYearPercentageColumn = (): ITableColumn => {
|
||||
return {
|
||||
key: 'previous_year_percentage',
|
||||
label: this.i18n.t('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 - 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`,
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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<string, string>}
|
||||
*/
|
||||
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<Buffer>}
|
||||
*/
|
||||
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;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import * as R from 'ramda';
|
||||
import { ITableColumn, ITableData, ITableRow } from '../types/Table.types';
|
||||
import { FinancialTableStructure } from './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 {
|
||||
/**
|
||||
* @param {TemplateInjectable} templateInjectable - The template injectable service.
|
||||
* @param {ChromiumlyTenancy} chromiumlyTenancy - The chromiumly tenancy service.
|
||||
*/
|
||||
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<Buffer> {
|
||||
// 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 - The table rows to be converted.
|
||||
* @returns {ITableRow[]} - The converted table rows.
|
||||
*/
|
||||
private tablePdfRows = (rows: ITableRow[]): ITableRow[] => {
|
||||
const curriedFlatNestedTree = R.curry(
|
||||
FinancialTableStructure.flatNestedTree,
|
||||
);
|
||||
const flatNestedTree = curriedFlatNestedTree(R.__, {
|
||||
nestedPrefix: '<span style="padding-left: 15px;"></span>',
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
return R.compose(tableClassNames, flatNestedTree)(rows);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
|
||||
import { APAgingSummaryApplication } from './APAgingSummaryApplication';
|
||||
import { IAPAgingSummaryQuery } from './APAgingSummary.types';
|
||||
import { AcceptType } from '@/constants/accept-type';
|
||||
import { Response } from 'express';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
@Controller('reports/payable-aging-summary')
|
||||
@ApiTags('reports')
|
||||
export class APAgingSummaryController {
|
||||
constructor(private readonly APAgingSummaryApp: APAgingSummaryApplication) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get payable aging summary' })
|
||||
public async get(
|
||||
@Query() filter: IAPAgingSummaryQuery,
|
||||
@Res() res: Response,
|
||||
@Headers('accept') acceptHeader: string,
|
||||
) {
|
||||
// Retrieves the json table format.
|
||||
if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) {
|
||||
const table = await this.APAgingSummaryApp.table(filter);
|
||||
|
||||
return res.status(200).send(table);
|
||||
// Retrieves the csv format.
|
||||
} else if (acceptHeader.includes(AcceptType.ApplicationCsv)) {
|
||||
const csv = await this.APAgingSummaryApp.csv(filter);
|
||||
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
|
||||
return res.send(csv);
|
||||
// Retrieves the xlsx format.
|
||||
} else if (acceptHeader.includes(AcceptType.ApplicationXlsx)) {
|
||||
const buffer = await this.APAgingSummaryApp.xlsx(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.APAgingSummaryApp.pdf(filter);
|
||||
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
return res.send(pdfContent);
|
||||
// Retrieves the json format.
|
||||
} else {
|
||||
const sheet = await this.APAgingSummaryApp.sheet(filter);
|
||||
|
||||
return res.status(200).send(sheet);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { APAgingSummaryService } from './APAgingSummaryService';
|
||||
import { AgingSummaryModule } from '../AgingSummary/AgingSummary.module';
|
||||
import { APAgingSummaryTableInjectable } from './APAgingSummaryTableInjectable';
|
||||
import { APAgingSummaryExportInjectable } from './APAgingSummaryExportInjectable';
|
||||
import { APAgingSummaryPdfInjectable } from './APAgingSummaryPdfInjectable';
|
||||
import { APAgingSummaryRepository } from './APAgingSummaryRepository';
|
||||
import { APAgingSummaryApplication } from './APAgingSummaryApplication';
|
||||
import { APAgingSummaryController } from './APAgingSummary.controller';
|
||||
import { APAgingSummaryMeta } from './APAgingSummaryMeta';
|
||||
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
|
||||
@Module({
|
||||
imports: [AgingSummaryModule, FinancialSheetCommonModule],
|
||||
providers: [
|
||||
APAgingSummaryService,
|
||||
APAgingSummaryMeta,
|
||||
APAgingSummaryTableInjectable,
|
||||
APAgingSummaryExportInjectable,
|
||||
APAgingSummaryPdfInjectable,
|
||||
APAgingSummaryRepository,
|
||||
APAgingSummaryApplication,
|
||||
TenancyContext,
|
||||
],
|
||||
controllers: [APAgingSummaryController],
|
||||
})
|
||||
export class APAgingSummaryModule {}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { IFinancialSheetCommonMeta } from '../../types/Report.types';
|
||||
import { IFinancialTable } from '../../types/Table.types';
|
||||
import {
|
||||
IAgingPeriod,
|
||||
IAgingSummaryQuery,
|
||||
IAgingSummaryTotal,
|
||||
IAgingSummaryContact,
|
||||
IAgingSummaryData,
|
||||
} from '../AgingSummary/AgingSummary.types';
|
||||
|
||||
export interface IAPAgingSummaryQuery extends IAgingSummaryQuery {
|
||||
vendorsIds: number[];
|
||||
}
|
||||
|
||||
export interface IAPAgingSummaryVendor extends IAgingSummaryContact {
|
||||
vendorName: string;
|
||||
}
|
||||
|
||||
export interface IAPAgingSummaryTotal extends IAgingSummaryTotal {}
|
||||
|
||||
export interface IAPAgingSummaryData extends IAgingSummaryData {
|
||||
vendors: IAPAgingSummaryVendor[];
|
||||
}
|
||||
|
||||
export type IAPAgingSummaryColumns = IAgingPeriod[];
|
||||
|
||||
export interface IARAgingSummaryMeta extends IFinancialSheetCommonMeta {
|
||||
formattedAsDate: string;
|
||||
}
|
||||
|
||||
export interface IAPAgingSummaryMeta extends IFinancialSheetCommonMeta {
|
||||
formattedAsDate: string;
|
||||
}
|
||||
|
||||
export interface IAPAgingSummaryTable extends IFinancialTable {
|
||||
query: IAPAgingSummaryQuery;
|
||||
meta: IAPAgingSummaryMeta;
|
||||
}
|
||||
|
||||
export interface IAPAgingSummarySheet {
|
||||
data: IAPAgingSummaryData;
|
||||
meta: IAPAgingSummaryMeta;
|
||||
query: IAPAgingSummaryQuery;
|
||||
columns: any;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { APAgingSummaryExportInjectable } from './APAgingSummaryExportInjectable';
|
||||
import { APAgingSummaryTableInjectable } from './APAgingSummaryTableInjectable';
|
||||
import { IAPAgingSummaryQuery } from './APAgingSummary.types';
|
||||
import { APAgingSummaryService } from './APAgingSummaryService';
|
||||
import { APAgingSummaryPdfInjectable } from './APAgingSummaryPdfInjectable';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class APAgingSummaryApplication {
|
||||
constructor(
|
||||
private readonly APAgingSummaryTable: APAgingSummaryTableInjectable,
|
||||
private readonly APAgingSummaryExport: APAgingSummaryExportInjectable,
|
||||
private readonly APAgingSummarySheet: APAgingSummaryService,
|
||||
private readonly APAgingSumaryPdf: APAgingSummaryPdfInjectable,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve the A/P aging summary in sheet format.
|
||||
* @param {IAPAgingSummaryQuery} query
|
||||
*/
|
||||
public sheet(query: IAPAgingSummaryQuery) {
|
||||
return this.APAgingSummarySheet.APAgingSummary(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the A/P aging summary in table format.
|
||||
* @param {IAPAgingSummaryQuery} query
|
||||
*/
|
||||
public table(query: IAPAgingSummaryQuery) {
|
||||
return this.APAgingSummaryTable.table(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the A/P aging summary in CSV format.
|
||||
* @param {IAPAgingSummaryQuery} query
|
||||
*/
|
||||
public csv(query: IAPAgingSummaryQuery) {
|
||||
return this.APAgingSummaryExport.csv(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the A/P aging summary in XLSX format.
|
||||
* @param {IAPAgingSummaryQuery} query
|
||||
*/
|
||||
public xlsx(query: IAPAgingSummaryQuery) {
|
||||
return this.APAgingSummaryExport.xlsx(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the A/P aging summary in pdf format.
|
||||
* @param {IAPAgingSummaryQuery} query
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public pdf(query: IAPAgingSummaryQuery) {
|
||||
return this.APAgingSumaryPdf.pdf(query);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { TableSheet } from '../../common/TableSheet';
|
||||
import { IAPAgingSummaryQuery } from './APAgingSummary.types';
|
||||
import { APAgingSummaryTableInjectable } from './APAgingSummaryTableInjectable';
|
||||
|
||||
@Injectable()
|
||||
export class APAgingSummaryExportInjectable {
|
||||
constructor(
|
||||
private readonly APAgingSummaryTable: APAgingSummaryTableInjectable,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the A/P aging summary sheet in XLSX format.
|
||||
* @param {IAPAgingSummaryQuery} query
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public async xlsx(query: IAPAgingSummaryQuery) {
|
||||
const table = await this.APAgingSummaryTable.table(query);
|
||||
|
||||
const tableSheet = new TableSheet(table.table);
|
||||
const tableCsv = tableSheet.convertToXLSX();
|
||||
|
||||
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the A/P aging summary sheet in CSV format.
|
||||
* @param {IAPAgingSummaryQuery} query
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public async csv(query: IAPAgingSummaryQuery): Promise<string> {
|
||||
const table = await this.APAgingSummaryTable.table(query);
|
||||
|
||||
const tableSheet = new TableSheet(table.table);
|
||||
const tableCsv = tableSheet.convertToCSV();
|
||||
|
||||
return tableCsv;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
IAgingSummaryMeta,
|
||||
IAgingSummaryQuery,
|
||||
} from '../AgingSummary/AgingSummary.types';
|
||||
import { AgingSummaryMeta } from '../AgingSummary/AgingSummaryMeta';
|
||||
|
||||
@Injectable()
|
||||
export class APAgingSummaryMeta {
|
||||
constructor(private readonly agingSummaryMeta: AgingSummaryMeta) {}
|
||||
|
||||
/**
|
||||
* Retrieve the aging summary meta.
|
||||
* @returns {IBalanceSheetMeta}
|
||||
*/
|
||||
public async meta(query: IAgingSummaryQuery): Promise<IAgingSummaryMeta> {
|
||||
const commonMeta = await this.agingSummaryMeta.meta(query);
|
||||
|
||||
return {
|
||||
...commonMeta,
|
||||
sheetName: 'A/P Aging Summary',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { TableSheetPdf } from '../../common/TableSheetPdf';
|
||||
import { HtmlTableCss } from '../AgingSummary/_constants';
|
||||
import { IAPAgingSummaryQuery } from './APAgingSummary.types';
|
||||
import { APAgingSummaryTableInjectable } from './APAgingSummaryTableInjectable';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class APAgingSummaryPdfInjectable {
|
||||
constructor(
|
||||
private readonly APAgingSummaryTable: APAgingSummaryTableInjectable,
|
||||
private readonly tableSheetPdf: TableSheetPdf,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Converts the given A/P aging summary sheet table to pdf.
|
||||
* @param {IAPAgingSummaryQuery} query - Balance sheet query.
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public async pdf(query: IAPAgingSummaryQuery): Promise<Buffer> {
|
||||
const table = await this.APAgingSummaryTable.table(query);
|
||||
|
||||
return this.tableSheetPdf.convertToPdf(
|
||||
table.table,
|
||||
table.meta.sheetName,
|
||||
table.meta.formattedAsDate,
|
||||
HtmlTableCss,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { isEmpty, groupBy } from 'lodash';
|
||||
import { Bill } from '@/modules/Bills/models/Bill';
|
||||
import { Vendor } from '@/modules/Vendors/models/Vendor';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
import { IAPAgingSummaryQuery } from './APAgingSummary.types';
|
||||
import { ModelObject } from 'objection';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
export class APAgingSummaryRepository {
|
||||
@Inject(Vendor.name)
|
||||
private readonly vendorModel: TenantModelProxy<typeof Vendor>;
|
||||
|
||||
@Inject(Bill.name)
|
||||
private readonly billModel: TenantModelProxy<typeof Bill>;
|
||||
|
||||
@Inject(TenancyContext)
|
||||
private readonly tenancyContext: TenancyContext;
|
||||
|
||||
/**
|
||||
* Filter.
|
||||
* @param {IAPAgingSummaryQuery} filter
|
||||
*/
|
||||
filter: IAPAgingSummaryQuery;
|
||||
|
||||
/**
|
||||
* Due bills.
|
||||
* @param {Bill[]} dueBills
|
||||
*/
|
||||
dueBills: Bill[];
|
||||
|
||||
/**
|
||||
* Due bills by vendor id.
|
||||
* @param {Record<string, Bill[]>} dueBillsByVendorId
|
||||
*/
|
||||
dueBillsByVendorId: Record<string, Bill[]>;
|
||||
|
||||
/**
|
||||
* Overdue bills.
|
||||
* @param {Bill[]} overdueBills - overdue bills.
|
||||
*/
|
||||
overdueBills: ModelObject<Bill>[];
|
||||
|
||||
/**
|
||||
* Overdue bills by vendor id.
|
||||
* @param {Record<string, Bill[]>} overdueBillsByVendorId - Overdue bills by vendor id.
|
||||
*/
|
||||
overdueBillsByVendorId: ModelObject<Bill>[];
|
||||
|
||||
/**
|
||||
* Vendors.
|
||||
* @param {Vendor[]} vendors
|
||||
*/
|
||||
vendors: Vendor[];
|
||||
|
||||
/**
|
||||
* Base currency.
|
||||
* @param {string} baseCurrency
|
||||
*/
|
||||
baseCurrency: string;
|
||||
|
||||
/**
|
||||
* Set the filter.
|
||||
* @param {IAPAgingSummaryQuery} filter
|
||||
*/
|
||||
setFilter(filter: IAPAgingSummaryQuery) {
|
||||
this.filter = filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the data.
|
||||
*/
|
||||
async load() {
|
||||
await this.asyncBaseCurrency();
|
||||
await this.asyncVendors();
|
||||
await this.asyncDueBills();
|
||||
await this.asyncOverdueBills();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the base currency.
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async asyncBaseCurrency() {
|
||||
const metadata = await this.tenancyContext.getTenantMetadata();
|
||||
|
||||
this.baseCurrency = metadata.baseCurrency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all vendors from the storage.
|
||||
*/
|
||||
async asyncVendors() {
|
||||
// Retrieve all vendors from the storage.
|
||||
const vendors =
|
||||
this.filter.vendorsIds.length > 0
|
||||
? await this.vendorModel().query().whereIn('id', this.filter.vendorsIds)
|
||||
: await this.vendorModel().query();
|
||||
|
||||
this.vendors = vendors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all overdue bills from the storage.
|
||||
*/
|
||||
async asyncOverdueBills() {
|
||||
const commonQuery = (query) => {
|
||||
if (!isEmpty(this.filter.branchesIds)) {
|
||||
query.modify('filterByBranches', this.filter.branchesIds);
|
||||
}
|
||||
};
|
||||
const overdueBills = await this.billModel()
|
||||
.query()
|
||||
.modify('overdueBillsFromDate', this.filter.asDate)
|
||||
.onBuild(commonQuery);
|
||||
|
||||
this.overdueBills = overdueBills;
|
||||
this.overdueBillsByVendorId = groupBy(overdueBills, 'vendorId');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all due bills from the storage.
|
||||
*/
|
||||
async asyncDueBills() {
|
||||
const commonQuery = (query) => {
|
||||
if (!isEmpty(this.filter.branchesIds)) {
|
||||
query.modify('filterByBranches', this.filter.branchesIds);
|
||||
}
|
||||
};
|
||||
// Retrieve all due vendors bills.
|
||||
const dueBills = await this.billModel()
|
||||
.query()
|
||||
.modify('dueBillsFromDate', this.filter.asDate)
|
||||
.onBuild(commonQuery);
|
||||
|
||||
this.dueBills = dueBills;
|
||||
this.dueBillsByVendorId = groupBy(dueBills, 'vendorId');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { events } from '@/common/events/events';
|
||||
import {
|
||||
IAPAgingSummaryQuery,
|
||||
IAPAgingSummarySheet,
|
||||
} from './APAgingSummary.types';
|
||||
import { APAgingSummarySheet } from './APAgingSummarySheet';
|
||||
import { APAgingSummaryMeta } from './APAgingSummaryMeta';
|
||||
import { getAPAgingSummaryDefaultQuery } from './utils';
|
||||
import { APAgingSummaryRepository } from './APAgingSummaryRepository';
|
||||
|
||||
@Injectable()
|
||||
export class APAgingSummaryService {
|
||||
constructor(
|
||||
private readonly APAgingSummaryMeta: APAgingSummaryMeta,
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
private readonly APAgingSummaryRepository: APAgingSummaryRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve A/P aging summary report.
|
||||
* @param {IAPAgingSummaryQuery} query - A/P aging summary query.
|
||||
* @returns {Promise<IAPAgingSummarySheet>}
|
||||
*/
|
||||
public async APAgingSummary(
|
||||
query: IAPAgingSummaryQuery,
|
||||
): Promise<IAPAgingSummarySheet> {
|
||||
|
||||
const filter = {
|
||||
...getAPAgingSummaryDefaultQuery(),
|
||||
...query,
|
||||
};
|
||||
// Load the data.
|
||||
this.APAgingSummaryRepository.setFilter(filter);
|
||||
await this.APAgingSummaryRepository.load();
|
||||
|
||||
// A/P aging summary report instance.
|
||||
const APAgingSummaryReport = new APAgingSummarySheet(
|
||||
filter,
|
||||
this.APAgingSummaryRepository,
|
||||
);
|
||||
// A/P aging summary report data and columns.
|
||||
const data = APAgingSummaryReport.reportData();
|
||||
const columns = APAgingSummaryReport.reportColumns();
|
||||
|
||||
// Retrieve the aging summary report meta.
|
||||
const meta = await this.APAgingSummaryMeta.meta(filter);
|
||||
|
||||
// Triggers `onPayableAgingViewed` event.
|
||||
await this.eventPublisher.emitAsync(events.reports.onPayableAgingViewed, {
|
||||
query,
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
columns,
|
||||
query: filter,
|
||||
meta,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import { sum, isEmpty } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
IAPAgingSummaryQuery,
|
||||
IAPAgingSummaryData,
|
||||
IAPAgingSummaryVendor,
|
||||
IAPAgingSummaryColumns,
|
||||
IAPAgingSummaryTotal,
|
||||
} from './APAgingSummary.types';
|
||||
import { AgingSummaryReport } from '../AgingSummary/AgingSummary';
|
||||
import { IAgingPeriod } from '../AgingSummary/AgingSummary.types';
|
||||
import { ModelObject } from 'objection';
|
||||
import { Vendor } from '@/modules/Vendors/models/Vendor';
|
||||
import { allPassedConditionsPass } from '@/utils/all-conditions-passed';
|
||||
import { APAgingSummaryRepository } from './APAgingSummaryRepository';
|
||||
|
||||
export class APAgingSummarySheet extends AgingSummaryReport {
|
||||
readonly repository: APAgingSummaryRepository;
|
||||
readonly query: IAPAgingSummaryQuery;
|
||||
readonly agingPeriods: IAgingPeriod[];
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {IAPAgingSummaryQuery} query - Report query.
|
||||
* @param {ModelObject<Vendor>[]} vendors - Unpaid bills.
|
||||
* @param {string} baseCurrency - Base currency of the organization.
|
||||
*/
|
||||
constructor(
|
||||
query: IAPAgingSummaryQuery,
|
||||
repository: APAgingSummaryRepository,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.query = query;
|
||||
this.repository = repository;
|
||||
this.numberFormat = this.query.numberFormat;
|
||||
|
||||
// Initializes the aging periods.
|
||||
this.agingPeriods = this.agingRangePeriods(
|
||||
this.query.asDate,
|
||||
this.query.agingDaysBefore,
|
||||
this.query.agingPeriods,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the vendors aging and current total.
|
||||
* @param {IAPAgingSummaryTotal} vendorsAgingPeriods
|
||||
* @return {IAPAgingSummaryTotal}
|
||||
*/
|
||||
private getVendorsTotal = (vendorsAgingPeriods): IAPAgingSummaryTotal => {
|
||||
const totalAgingPeriods = this.getTotalAgingPeriods(vendorsAgingPeriods);
|
||||
const totalCurrent = this.getTotalCurrent(vendorsAgingPeriods);
|
||||
const totalVendorsTotal = this.getTotalContactsTotals(vendorsAgingPeriods);
|
||||
|
||||
return {
|
||||
current: this.formatTotalAmount(totalCurrent),
|
||||
aging: totalAgingPeriods,
|
||||
total: this.formatTotalAmount(totalVendorsTotal),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the vendor section data.
|
||||
* @param {ModelObject<Vendor>} vendor
|
||||
* @return {IAPAgingSummaryVendor}
|
||||
*/
|
||||
private vendorTransformer = (
|
||||
vendor: ModelObject<Vendor>,
|
||||
): IAPAgingSummaryVendor => {
|
||||
const agingPeriods = this.getContactAgingPeriods(vendor.id);
|
||||
const currentTotal = this.getContactCurrentTotal(vendor.id);
|
||||
const agingPeriodsTotal = this.getAgingPeriodsTotal(agingPeriods);
|
||||
|
||||
const amount = sum([agingPeriodsTotal, currentTotal]);
|
||||
|
||||
return {
|
||||
vendorName: vendor.displayName,
|
||||
current: this.formatAmount(currentTotal),
|
||||
aging: agingPeriods,
|
||||
total: this.formatTotalAmount(amount),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Mappes the given vendor objects to vendor report node.
|
||||
* @param {ModelObject<Vendor>[]} vendors
|
||||
* @returns {IAPAgingSummaryVendor[]}
|
||||
*/
|
||||
private vendorsMapper = (
|
||||
vendors: ModelObject<Vendor>[],
|
||||
): IAPAgingSummaryVendor[] => {
|
||||
return vendors.map(this.vendorTransformer);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether the given vendor node is none zero.
|
||||
* @param {IAPAgingSummaryVendor} vendorNode
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private filterNoneZeroVendorNode = (
|
||||
vendorNode: IAPAgingSummaryVendor,
|
||||
): boolean => {
|
||||
return vendorNode.total.amount !== 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters vendors report nodes based on the given report query.
|
||||
* @param {IAPAgingSummaryVendor} vendorNode
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private vendorNodeFilter = (vendorNode: IAPAgingSummaryVendor): boolean => {
|
||||
const { noneZero } = this.query;
|
||||
const conditions = [[noneZero, this.filterNoneZeroVendorNode]];
|
||||
|
||||
return allPassedConditionsPass(conditions)(vendorNode);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filtesr the given report vendors nodes.
|
||||
* @param {IAPAgingSummaryVendor[]} vendorNodes
|
||||
* @returns {IAPAgingSummaryVendor[]}
|
||||
*/
|
||||
private vendorsFilter = (
|
||||
vendorNodes: IAPAgingSummaryVendor[],
|
||||
): IAPAgingSummaryVendor[] => {
|
||||
return vendorNodes.filter(this.vendorNodeFilter);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether vendors nodes filter enabled.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private isVendorNodesFilter = (): boolean => {
|
||||
return isEmpty(this.query.vendorsIds);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve vendors aging periods.
|
||||
* @return {IAPAgingSummaryVendor[]}
|
||||
*/
|
||||
private vendorsSection = (
|
||||
vendors: ModelObject<Vendor>[],
|
||||
): IAPAgingSummaryVendor[] => {
|
||||
return R.compose(
|
||||
R.when(this.isVendorNodesFilter, this.vendorsFilter),
|
||||
this.vendorsMapper,
|
||||
)(vendors);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the A/P aging summary report data.
|
||||
* @return {IAPAgingSummaryData}
|
||||
*/
|
||||
public reportData = (): IAPAgingSummaryData => {
|
||||
const vendorsAgingPeriods = this.vendorsSection(this.repository.vendors);
|
||||
const vendorsTotal = this.getVendorsTotal(vendorsAgingPeriods);
|
||||
|
||||
return {
|
||||
vendors: vendorsAgingPeriods,
|
||||
total: vendorsTotal,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the A/P aging summary report columns.
|
||||
*/
|
||||
public reportColumns = (): IAPAgingSummaryColumns => {
|
||||
return this.agingPeriods;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { I18nService } from 'nestjs-i18n';
|
||||
import { IAPAgingSummaryData } from './APAgingSummary.types';
|
||||
import { AgingSummaryTable } from '../AgingSummary/AgingSummaryTable';
|
||||
import { ITableColumnAccessor } from '../../types/Table.types';
|
||||
import { IAgingSummaryQuery } from '../AgingSummary/AgingSummary.types';
|
||||
import { ITableColumn } from '../../types/Table.types';
|
||||
import { ITableRow } from '../../types/Table.types';
|
||||
|
||||
export class APAgingSummaryTable extends AgingSummaryTable {
|
||||
readonly report: IAPAgingSummaryData;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {IARAgingSummaryData} data
|
||||
* @param {IAgingSummaryQuery} query
|
||||
* @param {any} i18n
|
||||
*/
|
||||
constructor(
|
||||
data: IAPAgingSummaryData,
|
||||
query: IAgingSummaryQuery,
|
||||
i18n: I18nService,
|
||||
) {
|
||||
super(data, query, i18n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the contacts table rows.
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
get contactsRows(): ITableRow[] {
|
||||
return this.contactsNodes(this.report.vendors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Contact name node accessor.
|
||||
* @returns {ITableColumnAccessor}
|
||||
*/
|
||||
get contactNameNodeAccessor(): ITableColumnAccessor {
|
||||
return { key: 'vendor_name', accessor: 'vendorName' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the contact name table column.
|
||||
* @returns {ITableColumn}
|
||||
*/
|
||||
contactNameTableColumn = (): ITableColumn => {
|
||||
return { label: 'Vendor name', key: 'vendor_name' };
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { I18nService } from 'nestjs-i18n';
|
||||
import {
|
||||
IAPAgingSummaryQuery,
|
||||
IAPAgingSummaryTable,
|
||||
} from './APAgingSummary.types';
|
||||
import { APAgingSummaryService } from './APAgingSummaryService';
|
||||
import { APAgingSummaryTable } from './APAgingSummaryTable';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class APAgingSummaryTableInjectable {
|
||||
constructor(
|
||||
private readonly APAgingSummarySheet: APAgingSummaryService,
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves A/P aging summary in table format.
|
||||
* @param {IAPAgingSummaryQuery} query -
|
||||
* @returns {Promise<IAPAgingSummaryTable>}
|
||||
*/
|
||||
public async table(
|
||||
query: IAPAgingSummaryQuery,
|
||||
): Promise<IAPAgingSummaryTable> {
|
||||
const report = await this.APAgingSummarySheet.APAgingSummary(query);
|
||||
const table = new APAgingSummaryTable(report.data, query, this.i18nService);
|
||||
|
||||
return {
|
||||
table: {
|
||||
columns: table.tableColumns(),
|
||||
rows: table.tableRows(),
|
||||
},
|
||||
meta: report.meta,
|
||||
query: report.query,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export const getAPAgingSummaryDefaultQuery = () => {
|
||||
return {
|
||||
asDate: moment().format('YYYY-MM-DD'),
|
||||
agingDaysBefore: 30,
|
||||
agingPeriods: 3,
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
divideOn1000: false,
|
||||
showZero: false,
|
||||
formatMoney: 'total',
|
||||
negativeFormat: 'mines',
|
||||
},
|
||||
vendorsIds: [],
|
||||
branchesIds: [],
|
||||
noneZero: false,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import { IARAgingSummaryQuery } from './ARAgingSummary.types';
|
||||
import { Controller, Get, Headers } from '@nestjs/common';
|
||||
import { Query, Res } from '@nestjs/common';
|
||||
import { ARAgingSummaryApplication } from './ARAgingSummaryApplication';
|
||||
import { AcceptType } from '@/constants/accept-type';
|
||||
import { Response } from 'express';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
@Controller('reports/receivable-aging-summary')
|
||||
@ApiTags('reports')
|
||||
export class ARAgingSummaryController {
|
||||
constructor(private readonly ARAgingSummaryApp: ARAgingSummaryApplication) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get receivable aging summary' })
|
||||
public async get(
|
||||
@Query() filter: IARAgingSummaryQuery,
|
||||
@Res() res: Response,
|
||||
@Headers('accept') acceptHeader: string,
|
||||
) {
|
||||
// Retrieves the xlsx format.
|
||||
if (acceptHeader.includes(AcceptType.ApplicationXlsx)) {
|
||||
const buffer = await this.ARAgingSummaryApp.xlsx(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 table format.
|
||||
} else if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) {
|
||||
const table = await this.ARAgingSummaryApp.table(filter);
|
||||
|
||||
return res.status(200).send(table);
|
||||
// Retrieves the csv format.
|
||||
} else if (acceptHeader.includes(AcceptType.ApplicationCsv)) {
|
||||
const buffer = await this.ARAgingSummaryApp.csv(filter);
|
||||
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
|
||||
return res.send(buffer);
|
||||
// Retrieves the pdf format.
|
||||
} else if (acceptHeader.includes(AcceptType.ApplicationPdf)) {
|
||||
const pdfContent = await this.ARAgingSummaryApp.pdf(filter);
|
||||
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
return res.send(pdfContent);
|
||||
// Retrieves the json format.
|
||||
} else {
|
||||
const sheet = await this.ARAgingSummaryApp.sheet(filter);
|
||||
|
||||
return res.status(200).send(sheet);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ARAgingSummaryTableInjectable } from './ARAgingSummaryTableInjectable';
|
||||
import { ARAgingSummaryExportInjectable } from './ARAgingSummaryExportInjectable';
|
||||
import { ARAgingSummaryService } from './ARAgingSummaryService';
|
||||
import { ARAgingSummaryPdfInjectable } from './ARAgingSummaryPdfInjectable';
|
||||
import { AgingSummaryModule } from '../AgingSummary/AgingSummary.module';
|
||||
import { ARAgingSummaryRepository } from './ARAgingSummaryRepository';
|
||||
import { ARAgingSummaryApplication } from './ARAgingSummaryApplication';
|
||||
import { ARAgingSummaryController } from './ARAgingSummary.controller';
|
||||
import { ARAgingSummaryMeta } from './ARAgingSummaryMeta';
|
||||
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
|
||||
@Module({
|
||||
imports: [AgingSummaryModule, FinancialSheetCommonModule],
|
||||
controllers: [ARAgingSummaryController],
|
||||
providers: [
|
||||
ARAgingSummaryTableInjectable,
|
||||
ARAgingSummaryExportInjectable,
|
||||
ARAgingSummaryService,
|
||||
ARAgingSummaryPdfInjectable,
|
||||
ARAgingSummaryRepository,
|
||||
ARAgingSummaryApplication,
|
||||
ARAgingSummaryMeta,
|
||||
TenancyContext,
|
||||
],
|
||||
})
|
||||
export class ARAgingSummaryModule {}
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
IAgingPeriod,
|
||||
IAgingSummaryQuery,
|
||||
IAgingSummaryTotal,
|
||||
IAgingSummaryContact,
|
||||
IAgingSummaryData,
|
||||
IAgingSummaryMeta,
|
||||
} from '../AgingSummary/AgingSummary.types';
|
||||
import { IFinancialTable } from '../../types/Table.types';
|
||||
|
||||
export interface IARAgingSummaryQuery extends IAgingSummaryQuery {
|
||||
customersIds: number[];
|
||||
}
|
||||
|
||||
export interface IARAgingSummaryCustomer extends IAgingSummaryContact {
|
||||
customerName: string;
|
||||
}
|
||||
|
||||
export interface IARAgingSummaryTotal extends IAgingSummaryTotal {}
|
||||
|
||||
export interface IARAgingSummaryData extends IAgingSummaryData {
|
||||
customers: IARAgingSummaryCustomer[];
|
||||
}
|
||||
|
||||
export type IARAgingSummaryColumns = IAgingPeriod[];
|
||||
|
||||
export interface IARAgingSummaryMeta extends IAgingSummaryMeta {
|
||||
organizationName: string;
|
||||
baseCurrency: string;
|
||||
}
|
||||
|
||||
export interface IARAgingSummaryTable extends IFinancialTable {
|
||||
meta: IARAgingSummaryMeta;
|
||||
query: IARAgingSummaryQuery;
|
||||
}
|
||||
|
||||
export interface IARAgingSummarySheet {
|
||||
data: IARAgingSummaryData;
|
||||
meta: IARAgingSummaryMeta;
|
||||
query: IARAgingSummaryQuery;
|
||||
columns: IARAgingSummaryColumns;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { ARAgingSummaryTableInjectable } from './ARAgingSummaryTableInjectable';
|
||||
import { ARAgingSummaryExportInjectable } from './ARAgingSummaryExportInjectable';
|
||||
import { ARAgingSummaryService } from './ARAgingSummaryService';
|
||||
import { ARAgingSummaryPdfInjectable } from './ARAgingSummaryPdfInjectable';
|
||||
import { IARAgingSummaryQuery } from './ARAgingSummary.types';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class ARAgingSummaryApplication {
|
||||
constructor(
|
||||
private readonly ARAgingSummaryTable: ARAgingSummaryTableInjectable,
|
||||
private readonly ARAgingSummaryExport: ARAgingSummaryExportInjectable,
|
||||
private readonly ARAgingSummarySheet: ARAgingSummaryService,
|
||||
private readonly ARAgingSummaryPdf: ARAgingSummaryPdfInjectable,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve the A/R aging summary sheet.
|
||||
* @param {IARAgingSummaryQuery} query
|
||||
*/
|
||||
public sheet(query: IARAgingSummaryQuery) {
|
||||
return this.ARAgingSummarySheet.ARAgingSummary(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the A/R aging summary in table format.
|
||||
* @param {IAPAgingSummaryQuery} query
|
||||
*/
|
||||
public table(query: IARAgingSummaryQuery) {
|
||||
return this.ARAgingSummaryTable.table(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the A/R aging summary in XLSX format.
|
||||
* @param {IAPAgingSummaryQuery} query
|
||||
*/
|
||||
public xlsx(query: IARAgingSummaryQuery) {
|
||||
return this.ARAgingSummaryExport.xlsx(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the A/R aging summary in CSV format.
|
||||
* @param {IAPAgingSummaryQuery} query
|
||||
*/
|
||||
public csv(query: IARAgingSummaryQuery) {
|
||||
return this.ARAgingSummaryExport.csv(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the A/R aging summary in pdf format.
|
||||
* @param {IARAgingSummaryQuery} query
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public pdf(query: IARAgingSummaryQuery) {
|
||||
return this.ARAgingSummaryPdf.pdf(query);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { ARAgingSummaryTableInjectable } from './ARAgingSummaryTableInjectable';
|
||||
import { IARAgingSummaryQuery } from './ARAgingSummary.types';
|
||||
import { TableSheet } from '../../common/TableSheet';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class ARAgingSummaryExportInjectable {
|
||||
constructor(
|
||||
private readonly ARAgingSummaryTable: ARAgingSummaryTableInjectable,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the A/R aging summary sheet in XLSX format.
|
||||
* @param {IARAgingSummaryQuery} query - A/R aging summary query.
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public async xlsx(
|
||||
query: IARAgingSummaryQuery
|
||||
): Promise<Buffer> {
|
||||
const table = await this.ARAgingSummaryTable.table(query);
|
||||
|
||||
const tableSheet = new TableSheet(table.table);
|
||||
const tableCsv = tableSheet.convertToXLSX();
|
||||
|
||||
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the A/R aging summary sheet in CSV format.
|
||||
* @param {IARAgingSummaryQuery} query - A/R aging summary query.
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
public async csv(
|
||||
query: IARAgingSummaryQuery
|
||||
): Promise<string> {
|
||||
const table = await this.ARAgingSummaryTable.table(query);
|
||||
|
||||
const tableSheet = new TableSheet(table.table);
|
||||
const tableCsv = tableSheet.convertToCSV();
|
||||
|
||||
return tableCsv;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AgingSummaryMeta } from '../AgingSummary/AgingSummaryMeta';
|
||||
import { IAgingSummaryMeta, IAgingSummaryQuery } from '../AgingSummary/AgingSummary.types';
|
||||
|
||||
@Injectable()
|
||||
export class ARAgingSummaryMeta {
|
||||
constructor(
|
||||
private readonly agingSummaryMeta: AgingSummaryMeta,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve the aging summary meta.
|
||||
* @param {IAgingSummaryQuery} query - Aging summary query.
|
||||
* @returns {IAgingSummaryMeta}
|
||||
*/
|
||||
public async meta(
|
||||
query: IAgingSummaryQuery
|
||||
): Promise<IAgingSummaryMeta> {
|
||||
const commonMeta = await this.agingSummaryMeta.meta(query);
|
||||
|
||||
return {
|
||||
...commonMeta,
|
||||
sheetName: 'A/R Aging Summary',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ARAgingSummaryTableInjectable } from './ARAgingSummaryTableInjectable';
|
||||
import { IARAgingSummaryQuery } from './ARAgingSummary.types';
|
||||
import { TableSheetPdf } from '../../common/TableSheetPdf';
|
||||
import { HtmlTableCss } from '../AgingSummary/_constants';
|
||||
|
||||
@Injectable()
|
||||
export class ARAgingSummaryPdfInjectable {
|
||||
constructor(
|
||||
private readonly ARAgingSummaryTable: ARAgingSummaryTableInjectable,
|
||||
private readonly tableSheetPdf: TableSheetPdf,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Converts the given balance sheet table to pdf.
|
||||
* @param {IBalanceSheetQuery} query - Balance sheet query.
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public async pdf(query: IARAgingSummaryQuery): Promise<Buffer> {
|
||||
const table = await this.ARAgingSummaryTable.table(query);
|
||||
|
||||
return this.tableSheetPdf.convertToPdf(
|
||||
table.table,
|
||||
table.meta.sheetName,
|
||||
table.meta.formattedDateRange,
|
||||
HtmlTableCss,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { isEmpty, groupBy } from 'lodash';
|
||||
import { Customer } from '@/modules/Customers/models/Customer';
|
||||
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
|
||||
import { ModelObject } from 'objection';
|
||||
import { IARAgingSummaryQuery } from './ARAgingSummary.types';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
export class ARAgingSummaryRepository {
|
||||
@Inject(TenancyContext)
|
||||
private tenancyContext: TenancyContext;
|
||||
|
||||
@Inject(Customer.name)
|
||||
private customerModel: TenantModelProxy<typeof Customer>;
|
||||
|
||||
@Inject(SaleInvoice.name)
|
||||
private saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>;
|
||||
|
||||
/**
|
||||
* Filter.
|
||||
* @param {IARAgingSummaryQuery} filter
|
||||
*/
|
||||
filter: IARAgingSummaryQuery;
|
||||
|
||||
/**
|
||||
* Base currency.
|
||||
* @param {string} baseCurrency
|
||||
*/
|
||||
baseCurrency: string;
|
||||
|
||||
/**
|
||||
* Customers.
|
||||
* @param {ModelObject<Customer>[]} customers
|
||||
*/
|
||||
customers: ModelObject<Customer>[];
|
||||
|
||||
/**
|
||||
* Overdue sale invoices.
|
||||
* @param {ModelObject<SaleInvoice>[]} overdueSaleInvoices
|
||||
*/
|
||||
overdueSaleInvoices: ModelObject<SaleInvoice>[];
|
||||
|
||||
/**
|
||||
* Current sale invoices.
|
||||
* @param {ModelObject<SaleInvoice>[]} currentInvoices
|
||||
*/
|
||||
currentInvoices: ModelObject<SaleInvoice>[];
|
||||
|
||||
/**
|
||||
* Current sale invoices by contact id.
|
||||
* @param {Record<string, ModelObject<SaleInvoice>[]>} currentInvoicesByContactId
|
||||
*/
|
||||
currentInvoicesByContactId: Record<string, ModelObject<SaleInvoice>[]>;
|
||||
|
||||
/**
|
||||
* Overdue sale invoices by contact id.
|
||||
* @param {Record<string, ModelObject<SaleInvoice>[]>} overdueInvoicesByContactId
|
||||
*/
|
||||
overdueInvoicesByContactId: Record<string, ModelObject<SaleInvoice>[]>;
|
||||
|
||||
/**
|
||||
* Set the filter.
|
||||
* @param {IARAgingSummaryQuery} filter
|
||||
*/
|
||||
setFilter(filter: IARAgingSummaryQuery) {
|
||||
this.filter = filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the repository.
|
||||
*/
|
||||
async load() {
|
||||
await this.initBaseCurrency();
|
||||
await this.initCustomers();
|
||||
await this.initOverdueSaleInvoices();
|
||||
await this.initCurrentInvoices();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the base currency.
|
||||
*/
|
||||
async initBaseCurrency() {
|
||||
const tenantMetadata = await this.tenancyContext.getTenantMetadata();
|
||||
|
||||
this.baseCurrency = tenantMetadata.baseCurrency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the customers.
|
||||
*/
|
||||
async initCustomers() {
|
||||
// Retrieve all customers from the storage.
|
||||
const customers =
|
||||
this.filter.customersIds.length > 0
|
||||
? await this.customerModel()
|
||||
.query()
|
||||
.whereIn('id', this.filter.customersIds)
|
||||
: await this.customerModel().query();
|
||||
|
||||
this.customers = customers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the overdue sale invoices.
|
||||
*/
|
||||
async initOverdueSaleInvoices() {
|
||||
const commonQuery = (query) => {
|
||||
if (!isEmpty(this.filter.branchesIds)) {
|
||||
query.modify('filterByBranches', this.filter.branchesIds);
|
||||
}
|
||||
};
|
||||
// Retrieve all overdue sale invoices.
|
||||
const overdueSaleInvoices = await this.saleInvoiceModel()
|
||||
.query()
|
||||
.modify('overdueInvoicesFromDate', this.filter.asDate)
|
||||
.onBuild(commonQuery);
|
||||
|
||||
this.overdueSaleInvoices = overdueSaleInvoices;
|
||||
this.overdueInvoicesByContactId = groupBy(
|
||||
overdueSaleInvoices,
|
||||
'customerId',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the current sale invoices.
|
||||
*/
|
||||
async initCurrentInvoices() {
|
||||
const commonQuery = (query) => {
|
||||
if (!isEmpty(this.filter.branchesIds)) {
|
||||
query.modify('filterByBranches', this.filter.branchesIds);
|
||||
}
|
||||
};
|
||||
// Retrieve all due sale invoices.
|
||||
const currentInvoices = await this.saleInvoiceModel()
|
||||
.query()
|
||||
.modify('dueInvoicesFromDate', this.filter.asDate)
|
||||
.onBuild(commonQuery);
|
||||
|
||||
this.currentInvoices = currentInvoices;
|
||||
this.currentInvoicesByContactId = groupBy(
|
||||
currentInvoices,
|
||||
'customerId',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { ARAgingSummarySheet } from './ARAgingSummarySheet';
|
||||
import { ARAgingSummaryMeta } from './ARAgingSummaryMeta';
|
||||
import { getARAgingSummaryDefaultQuery } from './utils';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { IARAgingSummaryQuery } from './ARAgingSummary.types';
|
||||
import { events } from '@/common/events/events';
|
||||
import { ARAgingSummaryRepository } from './ARAgingSummaryRepository';
|
||||
|
||||
@Injectable()
|
||||
export class ARAgingSummaryService {
|
||||
constructor(
|
||||
private readonly ARAgingSummaryMeta: ARAgingSummaryMeta,
|
||||
private readonly ARAgingSummaryRepository: ARAgingSummaryRepository,
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve A/R aging summary report.
|
||||
* @param {IARAgingSummaryQuery} query -
|
||||
*/
|
||||
async ARAgingSummary(query: IARAgingSummaryQuery) {
|
||||
const filter = {
|
||||
...getARAgingSummaryDefaultQuery(),
|
||||
...query,
|
||||
};
|
||||
// Load the A/R aging summary repository.
|
||||
this.ARAgingSummaryRepository.setFilter(filter);
|
||||
await this.ARAgingSummaryRepository.load();
|
||||
|
||||
// AR aging summary report instance.
|
||||
const ARAgingSummaryReport = new ARAgingSummarySheet(
|
||||
filter,
|
||||
this.ARAgingSummaryRepository,
|
||||
);
|
||||
// AR aging summary report data and columns.
|
||||
const data = ARAgingSummaryReport.reportData();
|
||||
const columns = ARAgingSummaryReport.reportColumns();
|
||||
|
||||
// Retrieve the aging summary report meta.
|
||||
const meta = await this.ARAgingSummaryMeta.meta(filter);
|
||||
|
||||
// Triggers `onReceivableAgingViewed` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.reports.onReceivableAgingViewed,
|
||||
{ query },
|
||||
);
|
||||
|
||||
return {
|
||||
data,
|
||||
columns,
|
||||
query: filter,
|
||||
meta,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import * as R from 'ramda';
|
||||
import { isEmpty, sum } from 'lodash';
|
||||
import { IAgingPeriod } from '../AgingSummary/AgingSummary.types';
|
||||
import {
|
||||
IARAgingSummaryQuery,
|
||||
IARAgingSummaryCustomer,
|
||||
IARAgingSummaryData,
|
||||
IARAgingSummaryColumns,
|
||||
IARAgingSummaryTotal,
|
||||
} from './ARAgingSummary.types';
|
||||
import { AgingSummaryReport } from '../AgingSummary/AgingSummary';
|
||||
import { allPassedConditionsPass } from '@/utils/all-conditions-passed';
|
||||
import { ModelObject } from 'objection';
|
||||
import { Customer } from '@/modules/Customers/models/Customer';
|
||||
import { ARAgingSummaryRepository } from './ARAgingSummaryRepository';
|
||||
|
||||
export class ARAgingSummarySheet extends AgingSummaryReport {
|
||||
readonly query: IARAgingSummaryQuery;
|
||||
readonly agingPeriods: IAgingPeriod[];
|
||||
readonly repository: ARAgingSummaryRepository;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId
|
||||
* @param {IARAgingSummaryQuery} query
|
||||
* @param {ICustomer[]} customers
|
||||
* @param {IJournalPoster} journal
|
||||
*/
|
||||
constructor(
|
||||
query: IARAgingSummaryQuery,
|
||||
repository: ARAgingSummaryRepository,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.query = query;
|
||||
this.repository = repository;
|
||||
this.numberFormat = this.query.numberFormat;
|
||||
|
||||
// Initializes the aging periods.
|
||||
this.agingPeriods = this.agingRangePeriods(
|
||||
this.query.asDate,
|
||||
this.query.agingDaysBefore,
|
||||
this.query.agingPeriods,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping aging customer.
|
||||
* @param {ICustomer} customer -
|
||||
* @return {IARAgingSummaryCustomer[]}
|
||||
*/
|
||||
private customerTransformer = (
|
||||
customer: ModelObject<Customer>,
|
||||
): IARAgingSummaryCustomer => {
|
||||
const agingPeriods = this.getContactAgingPeriods(customer.id);
|
||||
const currentTotal = this.getContactCurrentTotal(customer.id);
|
||||
const agingPeriodsTotal = this.getAgingPeriodsTotal(agingPeriods);
|
||||
const amount = sum([agingPeriodsTotal, currentTotal]);
|
||||
|
||||
return {
|
||||
customerName: customer.displayName,
|
||||
current: this.formatAmount(currentTotal),
|
||||
aging: agingPeriods,
|
||||
total: this.formatTotalAmount(amount),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Mappes the customers objects to report accounts nodes.
|
||||
* @param {ICustomer[]} customers
|
||||
* @returns {IARAgingSummaryCustomer[]}
|
||||
*/
|
||||
private customersMapper = (
|
||||
customers: ModelObject<Customer>[],
|
||||
): IARAgingSummaryCustomer[] => {
|
||||
return customers.map(this.customerTransformer);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters the none-zero account report node.
|
||||
* @param {IARAgingSummaryCustomer} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private filterNoneZeroAccountNode = (
|
||||
node: IARAgingSummaryCustomer,
|
||||
): boolean => {
|
||||
return node.total.amount !== 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters customer report node based on the given report query.
|
||||
* @param {IARAgingSummaryCustomer} customerNode
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private customerNodeFilter = (
|
||||
customerNode: IARAgingSummaryCustomer,
|
||||
): boolean => {
|
||||
const { noneZero } = this.query;
|
||||
|
||||
const conditions = [[noneZero, this.filterNoneZeroAccountNode]];
|
||||
|
||||
return allPassedConditionsPass(conditions)(customerNode);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters customers report nodes.
|
||||
* @param {IARAgingSummaryCustomer[]} customers
|
||||
* @returns {IARAgingSummaryCustomer[]}
|
||||
*/
|
||||
private customersFilter = (
|
||||
customers: IARAgingSummaryCustomer[],
|
||||
): IARAgingSummaryCustomer[] => {
|
||||
return customers.filter(this.customerNodeFilter);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines the customers nodes filter is enabled.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private isCustomersFilterEnabled = (): boolean => {
|
||||
return isEmpty(this.query.customersIds);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve customers report.
|
||||
* @param {ICustomer[]} customers
|
||||
* @return {IARAgingSummaryCustomer[]}
|
||||
*/
|
||||
private customersWalker = (
|
||||
customers: ModelObject<Customer>[],
|
||||
): IARAgingSummaryCustomer[] => {
|
||||
return R.compose(
|
||||
R.when(this.isCustomersFilterEnabled, this.customersFilter),
|
||||
this.customersMapper,
|
||||
)(customers);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the customers aging and current total.
|
||||
* @param {IARAgingSummaryCustomer} customersAgingPeriods
|
||||
*/
|
||||
private getCustomersTotal = (
|
||||
customersAgingPeriods: IARAgingSummaryCustomer[],
|
||||
): IARAgingSummaryTotal => {
|
||||
const totalAgingPeriods = this.getTotalAgingPeriods(customersAgingPeriods);
|
||||
const totalCurrent = this.getTotalCurrent(customersAgingPeriods);
|
||||
const totalCustomersTotal = this.getTotalContactsTotals(
|
||||
customersAgingPeriods,
|
||||
);
|
||||
|
||||
return {
|
||||
current: this.formatTotalAmount(totalCurrent),
|
||||
aging: totalAgingPeriods,
|
||||
total: this.formatTotalAmount(totalCustomersTotal),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve A/R aging summary report data.
|
||||
* @return {IARAgingSummaryData}
|
||||
*/
|
||||
public reportData = (): IARAgingSummaryData => {
|
||||
const customersAgingPeriods = this.customersWalker(
|
||||
this.repository.customers,
|
||||
);
|
||||
const customersTotal = this.getCustomersTotal(customersAgingPeriods);
|
||||
|
||||
return {
|
||||
customers: customersAgingPeriods,
|
||||
total: customersTotal,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve A/R aging summary report columns.
|
||||
* @return {IARAgingSummaryColumns}
|
||||
*/
|
||||
public reportColumns(): IARAgingSummaryColumns {
|
||||
return this.agingPeriods;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { IARAgingSummaryData } from './ARAgingSummary.types';
|
||||
import { AgingSummaryTable } from '../AgingSummary/AgingSummaryTable';
|
||||
import { IAgingSummaryQuery } from '../AgingSummary/AgingSummary.types';
|
||||
import { ITableColumnAccessor, ITableRow } from '../../types/Table.types';
|
||||
|
||||
export class ARAgingSummaryTable extends AgingSummaryTable {
|
||||
readonly report: IARAgingSummaryData;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {IARAgingSummaryData} data
|
||||
* @param {IAgingSummaryQuery} query
|
||||
* @param {any} i18n
|
||||
*/
|
||||
constructor(data: IARAgingSummaryData, query: IAgingSummaryQuery, i18n: any) {
|
||||
super(data, query, i18n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the contacts table rows.
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
get contactsRows(): ITableRow[] {
|
||||
return this.contactsNodes(this.report.customers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Contact name node accessor.
|
||||
* @returns {ITableColumnAccessor}
|
||||
*/
|
||||
get contactNameNodeAccessor(): ITableColumnAccessor {
|
||||
return { key: 'customer_name', accessor: 'customerName' };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ARAgingSummaryTable } from './ARAgingSummaryTable';
|
||||
import { ARAgingSummaryService } from './ARAgingSummaryService';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
IARAgingSummaryQuery,
|
||||
IARAgingSummaryTable,
|
||||
} from './ARAgingSummary.types';
|
||||
|
||||
@Injectable()
|
||||
export class ARAgingSummaryTableInjectable {
|
||||
constructor(private readonly ARAgingSummarySheet: ARAgingSummaryService) {}
|
||||
|
||||
/**
|
||||
* Retrieves A/R aging summary in table format.
|
||||
* @param {IARAgingSummaryQuery} query - Aging summary query.
|
||||
* @returns {Promise<IARAgingSummaryTable>}
|
||||
*/
|
||||
public async table(
|
||||
query: IARAgingSummaryQuery,
|
||||
): Promise<IARAgingSummaryTable> {
|
||||
const report = await this.ARAgingSummarySheet.ARAgingSummary(query);
|
||||
const table = new ARAgingSummaryTable(report.data, query, {});
|
||||
|
||||
return {
|
||||
table: {
|
||||
columns: table.tableColumns(),
|
||||
rows: table.tableRows(),
|
||||
},
|
||||
meta: report.meta,
|
||||
query,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as moment from 'moment';
|
||||
|
||||
|
||||
export const getARAgingSummaryDefaultQuery = () => {
|
||||
return {
|
||||
asDate: moment().format('YYYY-MM-DD'),
|
||||
agingDaysBefore: 30,
|
||||
agingPeriods: 3,
|
||||
numberFormat: {
|
||||
divideOn1000: false,
|
||||
negativeFormat: 'mines',
|
||||
showZero: false,
|
||||
formatMoney: 'total',
|
||||
precision: 2,
|
||||
},
|
||||
customersIds: [],
|
||||
branchesIds: [],
|
||||
noneZero: false,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import * as moment from 'moment';
|
||||
import { IAgingPeriod } from './AgingSummary.types';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
|
||||
export class AgingReport extends FinancialSheet {
|
||||
/**
|
||||
* Retrieve the aging periods range.
|
||||
* @param {string} asDay
|
||||
* @param {number} agingDaysBefore
|
||||
* @param {number} agingPeriodsFreq
|
||||
*/
|
||||
public agingRangePeriods(
|
||||
asDay: Date | string,
|
||||
agingDaysBefore: number,
|
||||
agingPeriodsFreq: number,
|
||||
): IAgingPeriod[] {
|
||||
const totalAgingDays = agingDaysBefore * agingPeriodsFreq;
|
||||
const startAging = moment(asDay).startOf('day');
|
||||
const endAging = startAging
|
||||
.clone()
|
||||
.subtract(totalAgingDays, 'days')
|
||||
.endOf('day');
|
||||
|
||||
const agingPeriods: IAgingPeriod[] = [];
|
||||
const startingAging = startAging.clone();
|
||||
|
||||
let beforeDays = 1;
|
||||
let toDays = 0;
|
||||
|
||||
while (startingAging > endAging) {
|
||||
const currentAging = startingAging.clone();
|
||||
startingAging.subtract(agingDaysBefore, 'days').endOf('day');
|
||||
toDays += agingDaysBefore;
|
||||
|
||||
agingPeriods.push({
|
||||
fromPeriod: moment(currentAging).format('YYYY-MM-DD'),
|
||||
toPeriod: moment(startingAging).format('YYYY-MM-DD'),
|
||||
beforeDays: beforeDays === 1 ? 0 : beforeDays,
|
||||
toDays: toDays,
|
||||
...(startingAging.valueOf() === endAging.valueOf()
|
||||
? {
|
||||
toPeriod: null,
|
||||
toDays: null,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
beforeDays += agingDaysBefore;
|
||||
}
|
||||
return agingPeriods;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AgingSummaryMeta } from './AgingSummaryMeta';
|
||||
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
|
||||
|
||||
@Module({
|
||||
imports: [FinancialSheetCommonModule],
|
||||
exports: [AgingSummaryMeta],
|
||||
providers: [AgingSummaryMeta],
|
||||
})
|
||||
export class AgingSummaryModule {}
|
||||
@@ -0,0 +1,233 @@
|
||||
import { ModelObject } from 'objection';
|
||||
import { defaultTo, sumBy, get } from 'lodash';
|
||||
import {
|
||||
IAgingPeriod,
|
||||
IAgingPeriodTotal,
|
||||
IAgingAmount,
|
||||
IAgingSummaryContact,
|
||||
IAgingSummaryQuery,
|
||||
} from './AgingSummary.types';
|
||||
import { AgingReport } from './AgingReport';
|
||||
import { Customer } from '@/modules/Customers/models/Customer';
|
||||
import { Vendor } from '@/modules/Vendors/models/Vendor';
|
||||
import { Bill } from '@/modules/Bills/models/Bill';
|
||||
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
|
||||
import { IFormatNumberSettings } from '@/utils/format-number';
|
||||
import { IARAgingSummaryCustomer } from '../ARAgingSummary/ARAgingSummary.types';
|
||||
|
||||
export abstract class AgingSummaryReport extends AgingReport {
|
||||
readonly contacts: ModelObject<Customer | Vendor>[];
|
||||
readonly agingPeriods: IAgingPeriod[] = [];
|
||||
readonly baseCurrency: string;
|
||||
readonly query: IAgingSummaryQuery;
|
||||
readonly overdueInvoicesByContactId: Record<
|
||||
number,
|
||||
Array<ModelObject<Bill | SaleInvoice>>
|
||||
>;
|
||||
readonly currentInvoicesByContactId: Record<
|
||||
number,
|
||||
Array<ModelObject<Bill | SaleInvoice>>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Setes initial aging periods to the contact.
|
||||
* @return {IAgingPeriodTotal[]}
|
||||
*/
|
||||
protected getInitialAgingPeriodsTotal(): IAgingPeriodTotal[] {
|
||||
return this.agingPeriods.map((agingPeriod) => ({
|
||||
...agingPeriod,
|
||||
total: this.formatAmount(0),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the given contact aging periods.
|
||||
* @param {number} contactId - Contact id.
|
||||
* @return {IAgingPeriodTotal[]}
|
||||
*/
|
||||
protected getContactAgingPeriods(contactId: number): IAgingPeriodTotal[] {
|
||||
const unpaidInvoices = this.getUnpaidInvoicesByContactId(contactId);
|
||||
const initialAgingPeriods = this.getInitialAgingPeriodsTotal();
|
||||
|
||||
return unpaidInvoices.reduce(
|
||||
(agingPeriods: IAgingPeriodTotal[], unpaidInvoice) => {
|
||||
const newAgingPeriods = this.getContactAgingDueAmount(
|
||||
agingPeriods,
|
||||
unpaidInvoice.dueAmount,
|
||||
unpaidInvoice.overdueDays,
|
||||
);
|
||||
return newAgingPeriods;
|
||||
},
|
||||
initialAgingPeriods,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the contact aging due amount to the table.
|
||||
* @param {IAgingPeriodTotal} agingPeriods - Aging periods.
|
||||
* @param {number} dueAmount - Due amount.
|
||||
* @param {number} overdueDays - Overdue days.
|
||||
* @return {IAgingPeriodTotal[]}
|
||||
*/
|
||||
protected getContactAgingDueAmount(
|
||||
agingPeriods: IAgingPeriodTotal[],
|
||||
dueAmount: number,
|
||||
overdueDays: number,
|
||||
): IAgingPeriodTotal[] {
|
||||
const newAgingPeriods = agingPeriods.map((agingPeriod) => {
|
||||
const isInAgingPeriod =
|
||||
agingPeriod.beforeDays <= overdueDays &&
|
||||
(agingPeriod.toDays > overdueDays || !agingPeriod.toDays);
|
||||
|
||||
const total: number = isInAgingPeriod
|
||||
? agingPeriod.total.amount + dueAmount
|
||||
: agingPeriod.total.amount;
|
||||
|
||||
return {
|
||||
...agingPeriod,
|
||||
total: this.formatAmount(total),
|
||||
};
|
||||
});
|
||||
return newAgingPeriods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the aging period total object.
|
||||
* @param {number} amount
|
||||
* @param {IFormatNumberSettings} settings - Override the format number settings.
|
||||
* @return {IAgingAmount}
|
||||
*/
|
||||
protected formatAmount(
|
||||
amount: number,
|
||||
settings: IFormatNumberSettings = {},
|
||||
): IAgingAmount {
|
||||
return {
|
||||
amount,
|
||||
// @ts-ignore
|
||||
formattedAmount: this.formatNumber(amount, settings),
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the aging period total object.
|
||||
* @param {number} amount
|
||||
* @param {IFormatNumberSettings} settings - Override the format number settings.
|
||||
* @return {IAgingPeriodTotal}
|
||||
*/
|
||||
protected formatTotalAmount(
|
||||
amount: number,
|
||||
settings: IFormatNumberSettings = {},
|
||||
): IAgingAmount {
|
||||
return this.formatAmount(amount, {
|
||||
money: true,
|
||||
excerptZero: false,
|
||||
...settings,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the total of the aging period by the given index.
|
||||
* @param {number} index
|
||||
* @return {number}
|
||||
*/
|
||||
protected getTotalAgingPeriodByIndex(
|
||||
contactsAgingPeriods: any,
|
||||
index: number,
|
||||
): number {
|
||||
return this.contacts.reduce((acc, contact) => {
|
||||
const totalPeriod = contactsAgingPeriods[index]
|
||||
? contactsAgingPeriods[index].total
|
||||
: 0;
|
||||
|
||||
return acc + totalPeriod;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the due invoices by the given contact id.
|
||||
* @param {number} contactId -
|
||||
* @return {(ISaleInvoice | IBill)[]}
|
||||
*/
|
||||
protected getUnpaidInvoicesByContactId(
|
||||
contactId: number,
|
||||
): (ModelObject<SaleInvoice> | ModelObject<Bill>)[] {
|
||||
return defaultTo(this.overdueInvoicesByContactId[contactId], []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve total aging periods of the report.
|
||||
* @return {(IAgingPeriodTotal & IAgingPeriod)[]}
|
||||
*/
|
||||
protected getTotalAgingPeriods(
|
||||
contactsAgingPeriods: IARAgingSummaryCustomer[],
|
||||
): IAgingPeriodTotal[] {
|
||||
return this.agingPeriods.map((agingPeriod, index) => {
|
||||
const total = sumBy(
|
||||
contactsAgingPeriods,
|
||||
(summary: IARAgingSummaryCustomer) => {
|
||||
const aging = summary.aging[index];
|
||||
|
||||
if (!aging) {
|
||||
return 0;
|
||||
}
|
||||
return aging.total.amount;
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
...agingPeriod,
|
||||
total: this.formatTotalAmount(total),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current invoices by the given contact id.
|
||||
* @param {number} contactId - Specific contact id.
|
||||
* @return {(ISaleInvoice | IBill)[]}
|
||||
*/
|
||||
protected getCurrentInvoicesByContactId(
|
||||
contactId: number,
|
||||
): (ModelObject<SaleInvoice> | ModelObject<Bill>)[] {
|
||||
return get(this.currentInvoicesByContactId, contactId, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the contact total due amount.
|
||||
* @param {number} contactId - Specific contact id.
|
||||
* @return {number}
|
||||
*/
|
||||
protected getContactCurrentTotal(contactId: number): number {
|
||||
const currentInvoices = this.getCurrentInvoicesByContactId(contactId);
|
||||
return sumBy(currentInvoices, (invoice) => invoice.dueAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve to total sumation of the given contacts summeries sections.
|
||||
* @param {IARAgingSummaryCustomer[]} contactsSections -
|
||||
* @return {number}
|
||||
*/
|
||||
protected getTotalCurrent(contactsSummaries: IAgingSummaryContact[]): number {
|
||||
return sumBy(contactsSummaries, (summary) => summary.current.amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the total of the given aging periods.
|
||||
* @param {IAgingPeriodTotal[]} agingPeriods
|
||||
* @return {number}
|
||||
*/
|
||||
protected getAgingPeriodsTotal(agingPeriods: IAgingPeriodTotal[]): number {
|
||||
return sumBy(agingPeriods, (period) => period.total.amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve total of contacts totals.
|
||||
* @param {IAgingSummaryContact[]} contactsSummaries
|
||||
*/
|
||||
protected getTotalContactsTotals(
|
||||
contactsSummaries: IAgingSummaryContact[],
|
||||
): number {
|
||||
return sumBy(contactsSummaries, (summary) => summary.total.amount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { IFinancialSheetCommonMeta } from '../../types/Report.types';
|
||||
import { INumberFormatQuery } from '../../types/Report.types';
|
||||
|
||||
export interface IAgingPeriodTotal extends IAgingPeriod {
|
||||
total: IAgingAmount;
|
||||
}
|
||||
|
||||
export interface IAgingAmount {
|
||||
amount: number;
|
||||
formattedAmount: string;
|
||||
currencyCode: string;
|
||||
}
|
||||
|
||||
export interface IAgingPeriod {
|
||||
fromPeriod: Date | string;
|
||||
toPeriod: Date | string;
|
||||
beforeDays: number;
|
||||
toDays: number;
|
||||
}
|
||||
|
||||
export interface IAgingSummaryContact {
|
||||
current: IAgingAmount;
|
||||
aging: IAgingPeriodTotal[];
|
||||
total: IAgingAmount;
|
||||
}
|
||||
|
||||
export interface IAgingSummaryQuery {
|
||||
asDate: Date | string;
|
||||
agingDaysBefore: number;
|
||||
agingPeriods: number;
|
||||
numberFormat: INumberFormatQuery;
|
||||
branchesIds: number[];
|
||||
noneZero: boolean;
|
||||
}
|
||||
|
||||
export interface IAgingSummaryTotal {
|
||||
current: IAgingAmount;
|
||||
aging: IAgingPeriodTotal[];
|
||||
total: IAgingAmount;
|
||||
}
|
||||
|
||||
export interface IAgingSummaryData {
|
||||
total: IAgingSummaryTotal;
|
||||
}
|
||||
|
||||
export interface IAgingSummaryMeta extends IFinancialSheetCommonMeta {
|
||||
formattedAsDate: string;
|
||||
formattedDateRange: string;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as moment from 'moment';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
|
||||
import { IAgingSummaryMeta, IAgingSummaryQuery } from './AgingSummary.types';
|
||||
|
||||
@Injectable()
|
||||
export class AgingSummaryMeta {
|
||||
constructor(private readonly financialSheetMeta: FinancialSheetMeta) {}
|
||||
|
||||
/**
|
||||
* Retrieve the aging summary meta.
|
||||
* @returns {IBalanceSheetMeta}
|
||||
*/
|
||||
public async meta(query: IAgingSummaryQuery): Promise<IAgingSummaryMeta> {
|
||||
const commonMeta = await this.financialSheetMeta.meta();
|
||||
const formattedAsDate = moment(query.asDate).format('YYYY/MM/DD');
|
||||
const formattedDateRange = `As ${formattedAsDate}`;
|
||||
|
||||
return {
|
||||
...commonMeta,
|
||||
sheetName: 'A/P Aging Summary',
|
||||
formattedAsDate,
|
||||
formattedDateRange,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
import { I18nService } from 'nestjs-i18n';
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
IAgingPeriod,
|
||||
IAgingSummaryContact,
|
||||
IAgingSummaryData,
|
||||
IAgingSummaryQuery,
|
||||
IAgingSummaryTotal,
|
||||
} from './AgingSummary.types';
|
||||
import { AgingReport } from './AgingReport';
|
||||
import { AgingSummaryRowType } from './_constants';
|
||||
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
|
||||
import { FinancialTable } from '../../common/FinancialTable';
|
||||
import {
|
||||
ITableColumn,
|
||||
ITableColumnAccessor,
|
||||
ITableRow,
|
||||
} from '../../types/Table.types';
|
||||
import { tableRowMapper } from '../../utils/Table.utils';
|
||||
|
||||
export abstract class AgingSummaryTable extends R.pipe(
|
||||
FinancialSheetStructure,
|
||||
FinancialTable,
|
||||
)(AgingReport) {
|
||||
readonly report: IAgingSummaryData;
|
||||
readonly query: IAgingSummaryQuery;
|
||||
readonly agingPeriods: IAgingPeriod[];
|
||||
readonly i18n: I18nService;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {IARAgingSummaryData} data - Aging summary data.
|
||||
* @param {IAgingSummaryQuery} query - Aging summary query.
|
||||
* @param {I18nService} i18n - Internationalization service.
|
||||
*/
|
||||
constructor(
|
||||
data: IAgingSummaryData,
|
||||
query: IAgingSummaryQuery,
|
||||
i18n: I18nService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.report = data;
|
||||
this.i18n = i18n;
|
||||
this.query = query;
|
||||
|
||||
this.agingPeriods = this.agingRangePeriods(
|
||||
this.query.asDate,
|
||||
this.query.agingDaysBefore,
|
||||
this.query.agingPeriods,
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// # Accessors.
|
||||
// -------------------------
|
||||
/**
|
||||
* Aging accessors of contact and total nodes.
|
||||
* @param {IAgingSummaryContact | IAgingSummaryTotal} node
|
||||
* @returns {ITableColumnAccessor[]}
|
||||
*/
|
||||
protected agingNodeAccessors = (
|
||||
node: IAgingSummaryContact | IAgingSummaryTotal,
|
||||
): ITableColumnAccessor[] => {
|
||||
return node.aging.map((aging, index) => ({
|
||||
key: 'aging_period',
|
||||
accessor: `aging[${index}].total.formattedAmount`,
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Contact name node accessor.
|
||||
* @returns {ITableColumnAccessor}
|
||||
*/
|
||||
protected get contactNameNodeAccessor(): ITableColumnAccessor {
|
||||
return { key: 'customer_name', accessor: 'customerName' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the common columns for all report nodes.
|
||||
* @param {IAgingSummaryContact}
|
||||
* @returns {ITableColumnAccessor[]}
|
||||
*/
|
||||
protected contactNodeAccessors = (
|
||||
node: IAgingSummaryContact,
|
||||
): ITableColumnAccessor[] => {
|
||||
return R.compose(
|
||||
R.concat([
|
||||
this.contactNameNodeAccessor,
|
||||
{ key: 'current', accessor: 'current.formattedAmount' },
|
||||
...this.agingNodeAccessors(node),
|
||||
{ key: 'total', accessor: 'total.formattedAmount' },
|
||||
]),
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the contact name table row.
|
||||
* @param {IAgingSummaryContact} node -
|
||||
* @return {ITableRow}
|
||||
*/
|
||||
protected contactNameNode = (node: IAgingSummaryContact): ITableRow => {
|
||||
const columns = this.contactNodeAccessors(node);
|
||||
const meta = {
|
||||
rowTypes: [AgingSummaryRowType.Contact],
|
||||
};
|
||||
return tableRowMapper(node, columns, meta);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps the customers nodes to table rows.
|
||||
* @param {IAgingSummaryContact[]} nodes
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
protected contactsNodes = (nodes: IAgingSummaryContact[]): ITableRow[] => {
|
||||
return nodes.map(this.contactNameNode);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the common columns for all report nodes.
|
||||
* @param {IAgingSummaryTotal}
|
||||
* @returns {ITableColumnAccessor[]}
|
||||
*/
|
||||
protected totalNodeAccessors = (
|
||||
node: IAgingSummaryTotal,
|
||||
): ITableColumnAccessor[] => {
|
||||
// @ts-ignore
|
||||
return R.compose(
|
||||
R.concat([
|
||||
{ key: 'blank', value: '' },
|
||||
{ key: 'current', accessor: 'current.formattedAmount' },
|
||||
...this.agingNodeAccessors(node),
|
||||
{ key: 'total', accessor: 'total.formattedAmount' },
|
||||
]),
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the total row of the given report total node.
|
||||
* @param {IAgingSummaryTotal} node
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
protected totalNode = (node: IAgingSummaryTotal): ITableRow => {
|
||||
const columns = this.totalNodeAccessors(node);
|
||||
const meta = {
|
||||
rowTypes: [AgingSummaryRowType.Total],
|
||||
};
|
||||
return tableRowMapper(node, columns, meta);
|
||||
};
|
||||
|
||||
// -------------------------
|
||||
// # Computed Rows.
|
||||
// -------------------------
|
||||
/**
|
||||
* Retrieves the contacts table rows.
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
protected get contactsRows(): ITableRow[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Table total row.
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
protected get totalRow(): ITableRow {
|
||||
return this.totalNode(this.report.total);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the table rows.
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
public tableRows = (): ITableRow[] => {
|
||||
return R.compose(
|
||||
R.unless(R.isEmpty, R.append(this.totalRow)),
|
||||
R.concat(this.contactsRows),
|
||||
)([]);
|
||||
};
|
||||
|
||||
// -------------------------
|
||||
// # Columns.
|
||||
// -------------------------
|
||||
/**
|
||||
* Retrieves the aging table columns.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected agingTableColumns = (): ITableColumn[] => {
|
||||
return this.agingPeriods.map((agingPeriod) => {
|
||||
return {
|
||||
label: `${agingPeriod.beforeDays} - ${
|
||||
agingPeriod.toDays || 'And Over'
|
||||
}`,
|
||||
key: 'aging_period',
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the contact name table column.
|
||||
* @returns {ITableColumn}
|
||||
*/
|
||||
protected contactNameTableColumn = (): ITableColumn => {
|
||||
return { label: 'Customer name', key: 'customer_name' };
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the report columns.
|
||||
* @returns {ITableColumn}
|
||||
*/
|
||||
public tableColumns = (): ITableColumn[] => {
|
||||
return R.compose(this.tableColumnsCellIndexing)([
|
||||
this.contactNameTableColumn(),
|
||||
{ label: 'Current', key: 'current' },
|
||||
...this.agingTableColumns(),
|
||||
{ label: 'Total', key: 'total' },
|
||||
]);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export enum AgingSummaryRowType {
|
||||
Contact = 'contact',
|
||||
Total = 'total',
|
||||
}
|
||||
|
||||
export const HtmlTableCss = `
|
||||
table tr.row-type--total td{
|
||||
font-weight: 600;
|
||||
border-top: 1px solid #bbb;
|
||||
border-bottom: 3px double #333;
|
||||
}
|
||||
|
||||
table .column--current,
|
||||
table .column--aging_period,
|
||||
table .column--total,
|
||||
table .cell--current,
|
||||
table .cell--aging_period,
|
||||
table .cell--total {
|
||||
text-align: right;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Response } from 'express';
|
||||
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
|
||||
import { IBalanceSheetQuery } from './BalanceSheet.types';
|
||||
import { AcceptType } from '@/constants/accept-type';
|
||||
import { BalanceSheetApplication } from './BalanceSheetApplication';
|
||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
@Controller('/reports/balance-sheet')
|
||||
@ApiTags('reports')
|
||||
export class BalanceSheetStatementController {
|
||||
constructor(private readonly balanceSheetApp: BalanceSheetApplication) {}
|
||||
|
||||
/**
|
||||
* Retrieve the balance sheet.
|
||||
* @param {IBalanceSheetQuery} query - Balance sheet query.
|
||||
* @param {Response} res - Response.
|
||||
* @param {string} acceptHeader - Accept header.
|
||||
*/
|
||||
@Get('')
|
||||
@ApiOperation({ summary: 'Get balance sheet statement' })
|
||||
@ApiResponse({ status: 200, description: 'Balance sheet statement' })
|
||||
public async balanceSheet(
|
||||
@Query() query: IBalanceSheetQuery,
|
||||
@Res() res: Response,
|
||||
@Headers('accept') acceptHeader: string,
|
||||
) {
|
||||
// Retrieves the json table format.
|
||||
if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) {
|
||||
const table = await this.balanceSheetApp.table(query);
|
||||
|
||||
return res.status(200).send(table);
|
||||
// Retrieves the csv format.
|
||||
} else if (acceptHeader.includes(AcceptType.ApplicationCsv)) {
|
||||
const buffer = await this.balanceSheetApp.csv(query);
|
||||
|
||||
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(query);
|
||||
|
||||
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(query);
|
||||
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
res.send(pdfContent);
|
||||
} else {
|
||||
const sheet = await this.balanceSheetApp.sheet(query);
|
||||
|
||||
return res.status(200).send(sheet);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { BalanceSheetInjectable } from './BalanceSheetInjectable';
|
||||
import { BalanceSheetApplication } from './BalanceSheetApplication';
|
||||
import { BalanceSheetTableInjectable } from './BalanceSheetTableInjectable';
|
||||
import { BalanceSheetExportInjectable } from './BalanceSheetExportInjectable';
|
||||
import { BalanceSheetMetaInjectable } from './BalanceSheetMeta';
|
||||
import { BalanceSheetPdfInjectable } from './BalanceSheetPdfInjectable';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
|
||||
import { BalanceSheetStatementController } from './BalanceSheet.controller';
|
||||
import { BalanceSheetRepository } from './BalanceSheetRepository';
|
||||
import { AccountsModule } from '@/modules/Accounts/Accounts.module';
|
||||
|
||||
@Module({
|
||||
imports: [FinancialSheetCommonModule, AccountsModule],
|
||||
controllers: [BalanceSheetStatementController],
|
||||
providers: [
|
||||
BalanceSheetRepository,
|
||||
BalanceSheetInjectable,
|
||||
BalanceSheetTableInjectable,
|
||||
BalanceSheetExportInjectable,
|
||||
BalanceSheetMetaInjectable,
|
||||
BalanceSheetApplication,
|
||||
BalanceSheetPdfInjectable,
|
||||
TenancyContext,
|
||||
],
|
||||
exports: [BalanceSheetInjectable],
|
||||
})
|
||||
export class BalanceSheetModule {}
|
||||
@@ -0,0 +1,115 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { I18nService } from 'nestjs-i18n';
|
||||
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 { INumberFormatQuery } from '../../types/Report.types';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
|
||||
export class BalanceSheet extends R.pipe(
|
||||
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: I18nService;
|
||||
|
||||
/**
|
||||
* Balance sheet repository.
|
||||
*/
|
||||
readonly repository: BalanceSheetRepository;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {IBalanceSheetQuery} query -
|
||||
* @param {IAccount[]} accounts -
|
||||
* @param {string} baseCurrency -
|
||||
*/
|
||||
constructor(
|
||||
query: IBalanceSheetQuery,
|
||||
repository: BalanceSheetRepository,
|
||||
baseCurrency: string,
|
||||
i18n: I18nService,
|
||||
) {
|
||||
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);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
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: string;
|
||||
toDate: string;
|
||||
|
||||
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<IBalanceSheetDOO>;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
// @ts-nocheck
|
||||
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 { BalanceSheetRepository } from './BalanceSheetRepository';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { INumberFormatQuery } from '../../types/Report.types';
|
||||
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||
import { flatToNestedArray } from '@/utils/flat-to-nested-array';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
|
||||
export const BalanceSheetAccounts = <T extends GConstructor<FinancialSheet>>(
|
||||
Base: T,
|
||||
) =>
|
||||
class extends R.pipe(
|
||||
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);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,145 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
BALANCE_SHEET_SCHEMA_NODE_TYPE,
|
||||
IBalanceSheetAggregateNode,
|
||||
IBalanceSheetDataNode,
|
||||
IBalanceSheetSchemaAggregateNode,
|
||||
IBalanceSheetSchemaNode,
|
||||
} from './BalanceSheet.types';
|
||||
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 { GConstructor } from '@/common/types/Constructor';
|
||||
import { INumberFormatQuery } from '../../types/Report.types';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
|
||||
export const BalanceSheetAggregators = <T extends GConstructor<FinancialSheet>>(
|
||||
Base: T,
|
||||
) =>
|
||||
class extends R.pipe(
|
||||
BalanceSheetDatePeriods,
|
||||
BalanceSheetComparsionPreviousPeriod,
|
||||
BalanceSheetComparsionPreviousYear,
|
||||
BalanceSheetPercentage,
|
||||
BalanceSheetSchema,
|
||||
FinancialSheetStructure,
|
||||
BalanceSheetBase,
|
||||
)(Base) {
|
||||
/**
|
||||
* Balance sheet query.
|
||||
* @param {BalanceSheetQuery}
|
||||
*/
|
||||
public 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);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { IBalanceSheetQuery } from './BalanceSheet.types';
|
||||
import { BalanceSheetExportInjectable } from './BalanceSheetExportInjectable';
|
||||
import { BalanceSheetTableInjectable } from './BalanceSheetTableInjectable';
|
||||
import { BalanceSheetInjectable } from './BalanceSheetInjectable';
|
||||
|
||||
@Injectable()
|
||||
export class BalanceSheetApplication {
|
||||
/**
|
||||
* @param {BalanceSheetExportInjectable} balanceSheetExportService - The balance sheet export service.
|
||||
* @param {BalanceSheetTableInjectable} balanceSheetTableService - The balance sheet table service.
|
||||
* @param {BalanceSheetInjectable} balanceSheetService - The balance sheet service.
|
||||
*/
|
||||
constructor(
|
||||
private readonly balanceSheetExportService: BalanceSheetExportInjectable,
|
||||
private readonly balanceSheetTableService: BalanceSheetTableInjectable,
|
||||
private readonly balanceSheetService: BalanceSheetInjectable,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the balnace sheet in json format.
|
||||
* @param {IBalanceSheetQuery} query
|
||||
* @returns {Promise<IBalanceSheetStatement>}
|
||||
*/
|
||||
public sheet(query: IBalanceSheetQuery) {
|
||||
return this.balanceSheetService.balanceSheet(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the balance sheet in table format.
|
||||
* @param {IBalanceSheetQuery} query
|
||||
* @returns {Promise<IBalanceSheetTable>}
|
||||
*/
|
||||
public table(query: IBalanceSheetQuery) {
|
||||
return this.balanceSheetTableService.table(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the balance sheet in XLSX format.
|
||||
* @param {IBalanceSheetQuery} query
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public xlsx(query: IBalanceSheetQuery) {
|
||||
return this.balanceSheetExportService.xlsx(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the balance sheet in CSV format.
|
||||
* @param {IBalanceSheetQuery} query
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public csv(query: IBalanceSheetQuery): Promise<string> {
|
||||
return this.balanceSheetExportService.csv(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the balance sheet in pdf format.
|
||||
* @param {IBalanceSheetQuery} query
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public pdf(query: IBalanceSheetQuery) {
|
||||
return this.balanceSheetExportService.pdf(query);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
IBalanceSheetDataNode,
|
||||
IBalanceSheetSchemaNode,
|
||||
} from './BalanceSheet.types';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
import { BalanceSheetQuery } from './BalanceSheetQuery';
|
||||
|
||||
export const BalanceSheetBase = <T extends GConstructor<FinancialSheet>>(
|
||||
Base: T,
|
||||
) =>
|
||||
class BalanceSheetBase extends Base {
|
||||
query: BalanceSheetQuery;
|
||||
|
||||
/**
|
||||
* Determines 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;
|
||||
},
|
||||
);
|
||||
/**
|
||||
* Determines the node type of the given schema node.
|
||||
* @param {IBalanceSheetStructureSection} node -
|
||||
* @param {string} type -
|
||||
* @return {boolean}
|
||||
*/
|
||||
public isNodeType = R.curry(
|
||||
(type: string, node: IBalanceSheetDataNode): boolean => {
|
||||
return node.nodeType === type;
|
||||
},
|
||||
);
|
||||
/**
|
||||
* Determines the given display columns by type.
|
||||
* @param {string} displayColumnsBy
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isDisplayColumnsBy = (displayColumnsBy: string): boolean => {
|
||||
return this.query.displayColumnsType === displayColumnsBy;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,279 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { sumBy } from 'lodash';
|
||||
import {
|
||||
IBalanceSheetAccountNode,
|
||||
IBalanceSheetDataNode,
|
||||
IBalanceSheetAggregateNode,
|
||||
IBalanceSheetTotal,
|
||||
IBalanceSheetCommonNode,
|
||||
} from './BalanceSheet.types';
|
||||
import { FinancialPreviousPeriod } from '../../common/FinancialPreviousPeriod';
|
||||
import { FinancialHorizTotals } from '../../common/FinancialHorizTotals';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
import { BalanceSheetQuery } from './BalanceSheetQuery';
|
||||
import { BalanceSheetRepository } from './BalanceSheetRepository';
|
||||
|
||||
export const BalanceSheetComparsionPreviousPeriod = <
|
||||
T extends GConstructor<FinancialSheet>,
|
||||
>(
|
||||
Base: T,
|
||||
) =>
|
||||
class BalanceSheetComparsionPreviousPeriod extends R.pipe(
|
||||
FinancialHorizTotals,
|
||||
FinancialPreviousPeriod,
|
||||
)(Base) {
|
||||
query: BalanceSheetQuery;
|
||||
repository: BalanceSheetRepository;
|
||||
|
||||
// ------------------------------
|
||||
// # 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);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,279 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { sumBy, isEmpty } from 'lodash';
|
||||
import {
|
||||
IBalanceSheetAccountNode,
|
||||
IBalanceSheetCommonNode,
|
||||
IBalanceSheetDataNode,
|
||||
IBalanceSheetTotal,
|
||||
} from './BalanceSheet.types';
|
||||
import { FinancialPreviousYear } from '../../common/FinancialPreviousYear';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
import { BalanceSheetQuery } from './BalanceSheetQuery';
|
||||
import { BalanceSheetRepository } from './BalanceSheetRepository';
|
||||
|
||||
export const BalanceSheetComparsionPreviousYear = <
|
||||
T extends GConstructor<FinancialSheet>,
|
||||
>(
|
||||
Base: T,
|
||||
) =>
|
||||
class BalanceSheetComparsionPreviousYear extends R.pipe(
|
||||
FinancialPreviousYear,
|
||||
)(Base) {
|
||||
query: BalanceSheetQuery;
|
||||
repository: BalanceSheetRepository;
|
||||
|
||||
// ------------------------------
|
||||
// # 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);
|
||||
};
|
||||
@@ -0,0 +1,211 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { sumBy } from 'lodash';
|
||||
import {
|
||||
IBalanceSheetQuery,
|
||||
IBalanceSheetAccountNode,
|
||||
IBalanceSheetTotalPeriod,
|
||||
IBalanceSheetCommonNode,
|
||||
} from './BalanceSheet.types';
|
||||
import { FinancialDatePeriods } from '../../common/FinancialDatePeriods';
|
||||
import { IDateRange, IFormatNumberSettings } from '../../types/Report.types';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
import { BalanceSheetQuery } from './BalanceSheetQuery';
|
||||
|
||||
/**
|
||||
* Balance sheet date periods.
|
||||
*/
|
||||
export const BalanceSheetDatePeriods = <T extends GConstructor<FinancialSheet>>(
|
||||
Base: T,
|
||||
) =>
|
||||
class BalanceSheetDatePeriods extends R.pipe(FinancialDatePeriods)(Base) {
|
||||
/**
|
||||
* @param {IBalanceSheetQuery}
|
||||
*/
|
||||
public readonly query: BalanceSheetQuery;
|
||||
|
||||
/**
|
||||
* 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 {}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
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 {ITrialBalanceSheetQuery} query
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
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 {ITrialBalanceSheetQuery} query
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public async csv(
|
||||
query: IBalanceSheetQuery,
|
||||
): Promise<string> {
|
||||
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 {IBalanceSheetQuery} query
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public async pdf(
|
||||
query: IBalanceSheetQuery,
|
||||
): Promise<Buffer> {
|
||||
return this.balanceSheetPdf.pdf(query);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { get } from 'lodash';
|
||||
import {
|
||||
IBalanceSheetDataNode,
|
||||
BALANCE_SHEET_NODE_TYPE,
|
||||
} from './BalanceSheet.types';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialFilter } from '../../common/FinancialFilter';
|
||||
import { BalanceSheetBase } from './BalanceSheetBase';
|
||||
import { BalanceSheetRepository } from './BalanceSheetRepository';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
|
||||
|
||||
export const BalanceSheetFiltering = <T extends GConstructor<FinancialSheet>>(
|
||||
Base: T,
|
||||
) =>
|
||||
class extends R.pipe(
|
||||
FinancialFilter,
|
||||
FinancialSheetStructure,
|
||||
BalanceSheetBase,
|
||||
)(Base) {
|
||||
/**
|
||||
* @description Repository.
|
||||
*/
|
||||
readonly repository: BalanceSheetRepository;
|
||||
|
||||
// -----------------------
|
||||
// # 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);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
// @ts-nocheck
|
||||
import {
|
||||
IBalanceSheetDOO,
|
||||
IBalanceSheetQuery,
|
||||
} from './BalanceSheet.types';
|
||||
import { BalanceSheetRepository } from './BalanceSheetRepository';
|
||||
import { BalanceSheetMetaInjectable } from './BalanceSheetMeta';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { BalanceSheet } from './BalanceSheet';
|
||||
import { getBalanceSheetDefaultQuery } from './constants';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
import { I18nService } from 'nestjs-i18n';
|
||||
|
||||
@Injectable()
|
||||
export class BalanceSheetInjectable {
|
||||
constructor(
|
||||
private readonly balanceSheetMeta: BalanceSheetMetaInjectable,
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
private readonly tenancyContext: TenancyContext,
|
||||
private readonly i18n: I18nService,
|
||||
private readonly balanceSheetRepository: BalanceSheetRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve balance sheet statement.
|
||||
* @param {IBalanceSheetQuery} query - Balance sheet query.
|
||||
* @return {Promise<IBalanceSheetStatement>}
|
||||
*/
|
||||
public async balanceSheet(
|
||||
query: IBalanceSheetQuery,
|
||||
): Promise<IBalanceSheetDOO> {
|
||||
const filter = {
|
||||
...getBalanceSheetDefaultQuery(),
|
||||
...query,
|
||||
};
|
||||
const tenantMetadata = await this.tenancyContext.getTenantMetadata(true);
|
||||
|
||||
// Loads all resources.
|
||||
await this.balanceSheetRepository.asyncInitialize(filter);
|
||||
|
||||
// Balance sheet report instance.
|
||||
const balanceSheetInstanace = new BalanceSheet(
|
||||
filter,
|
||||
this.balanceSheetRepository,
|
||||
tenantMetadata.baseCurrency,
|
||||
this.i18n,
|
||||
);
|
||||
// Balance sheet data.
|
||||
const data = balanceSheetInstanace.reportData();
|
||||
|
||||
// Balance sheet meta.
|
||||
const meta = await this.balanceSheetMeta.meta(filter);
|
||||
|
||||
// Triggers `onBalanceSheetViewed` event.
|
||||
await this.eventPublisher.emitAsync(events.reports.onBalanceSheetViewed, {
|
||||
query,
|
||||
});
|
||||
return {
|
||||
query: filter,
|
||||
data,
|
||||
meta,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as moment from 'moment';
|
||||
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
|
||||
import { IBalanceSheetMeta, IBalanceSheetQuery } from './BalanceSheet.types';
|
||||
|
||||
@Injectable()
|
||||
export class BalanceSheetMetaInjectable {
|
||||
constructor(private readonly financialSheetMeta: FinancialSheetMeta) {}
|
||||
|
||||
/**
|
||||
* Retrieves the balance sheet meta.
|
||||
* @returns {IBalanceSheetMeta}
|
||||
*/
|
||||
public async meta(query: IBalanceSheetQuery): Promise<IBalanceSheetMeta> {
|
||||
const commonMeta = await this.financialSheetMeta.meta();
|
||||
const formattedAsDate = moment(query.toDate).format('YYYY/MM/DD');
|
||||
const formattedDateRange = `As ${formattedAsDate}`;
|
||||
const sheetName = 'Balance Sheet Statement';
|
||||
|
||||
return {
|
||||
...commonMeta,
|
||||
sheetName,
|
||||
formattedAsDate,
|
||||
formattedDateRange,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
BALANCE_SHEET_SCHEMA_NODE_TYPE,
|
||||
IBalanceSheetDataNode,
|
||||
IBalanceSheetNetIncomeNode,
|
||||
IBalanceSheetSchemaNetIncomeNode,
|
||||
IBalanceSheetSchemaNode,
|
||||
IBalanceSheetTotalPeriod,
|
||||
} from './BalanceSheet.types';
|
||||
import { BalanceSheetComparsionPreviousYear } from './BalanceSheetComparsionPreviousYear';
|
||||
import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod';
|
||||
import { FinancialPreviousPeriod } from '../../common/FinancialPreviousPeriod';
|
||||
import { FinancialHorizTotals } from '../../common/FinancialHorizTotals';
|
||||
import { BalanceSheetRepository } from './BalanceSheetRepository';
|
||||
import { BalanceSheetQuery } from './BalanceSheetQuery';
|
||||
import { BalanceSheetNetIncomePP } from './BalanceSheetNetIncomePP';
|
||||
import { BalanceSheetNetIncomePY } from './BalanceSheetNetIncomePY';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
|
||||
export const BalanceSheetNetIncome = <T extends GConstructor<FinancialSheet>>(
|
||||
Base: T,
|
||||
) =>
|
||||
class extends R.pipe(
|
||||
BalanceSheetNetIncomePP,
|
||||
BalanceSheetNetIncomePY,
|
||||
BalanceSheetComparsionPreviousYear,
|
||||
BalanceSheetComparsionPreviousPeriod,
|
||||
FinancialPreviousPeriod,
|
||||
FinancialHorizTotals,
|
||||
)(Base) {
|
||||
public repository: BalanceSheetRepository;
|
||||
public query: BalanceSheetQuery;
|
||||
|
||||
/**
|
||||
* Retrieves the closing balance of income accounts.
|
||||
* @returns {number}
|
||||
*/
|
||||
public getIncomeTotal = () => {
|
||||
const closeingBalance = this.repository.incomeLedger.getClosingBalance();
|
||||
return closeingBalance;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the closing balance of expenses accounts.
|
||||
* @returns {number}
|
||||
*/
|
||||
public getExpensesTotal = () => {
|
||||
const closingBalance = this.repository.expensesLedger.getClosingBalance();
|
||||
return closingBalance;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the total net income.
|
||||
* @returns {number}
|
||||
*/
|
||||
public getNetIncomeTotal = () => {
|
||||
const income = this.getIncomeTotal();
|
||||
const expenses = this.getExpensesTotal();
|
||||
|
||||
return income - expenses;
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps the aggregate schema node type.
|
||||
* @param {IBalanceSheetSchemaNetIncomeNode} node - Schema node.
|
||||
* @return {IBalanceSheetAggregateNode}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
IBalanceSheetNetIncomeNode,
|
||||
IBalanceSheetTotalPeriod,
|
||||
} from './BalanceSheet.types';
|
||||
import { BalanceSheetComparsionPreviousYear } from './BalanceSheetComparsionPreviousYear';
|
||||
import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod';
|
||||
import { FinancialPreviousPeriod } from '../../common/FinancialPreviousPeriod';
|
||||
import { FinancialHorizTotals } from '../../common/FinancialHorizTotals';
|
||||
import { BalanceSheetRepository } from './BalanceSheetRepository';
|
||||
import { BalanceSheetNetIncomePP } from './BalanceSheetNetIncomePP';
|
||||
import { BalanceSheetNetIncomePY } from './BalanceSheetNetIncomePY';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
|
||||
export const BalanceSheetNetIncomeDatePeriods = <
|
||||
T extends GConstructor<FinancialSheet>,
|
||||
>(
|
||||
Base: T,
|
||||
) =>
|
||||
class extends R.pipe(
|
||||
BalanceSheetNetIncomePP,
|
||||
BalanceSheetNetIncomePY,
|
||||
BalanceSheetComparsionPreviousYear,
|
||||
BalanceSheetComparsionPreviousPeriod,
|
||||
FinancialPreviousPeriod,
|
||||
FinancialHorizTotals,
|
||||
)(Base) {
|
||||
repository: BalanceSheetRepository;
|
||||
|
||||
// --------------------------------
|
||||
// # 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);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod';
|
||||
import { FinancialPreviousPeriod } from '../../common/FinancialPreviousPeriod';
|
||||
import { FinancialHorizTotals } from '../../common/FinancialHorizTotals';
|
||||
import {
|
||||
IBalanceSheetNetIncomeNode,
|
||||
IBalanceSheetTotal,
|
||||
} from './BalanceSheet.types';
|
||||
import { BalanceSheetQuery } from './BalanceSheetQuery';
|
||||
import { BalanceSheetRepository } from './BalanceSheetRepository';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
|
||||
export const BalanceSheetNetIncomeDatePeriodsPP = <
|
||||
T extends GConstructor<FinancialSheet>,
|
||||
>(
|
||||
Base: T,
|
||||
) =>
|
||||
class extends R.pipe(
|
||||
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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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 {}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { BalanceSheetComparsionPreviousYear } from './BalanceSheetComparsionPreviousYear';
|
||||
import { FinancialPreviousPeriod } from '../../common/FinancialPreviousPeriod';
|
||||
import { FinancialHorizTotals } from '../../common/FinancialHorizTotals';
|
||||
import {
|
||||
IBalanceSheetNetIncomeNode,
|
||||
IBalanceSheetTotal,
|
||||
} from './BalanceSheet.types';
|
||||
import { BalanceSheetQuery } from './BalanceSheetQuery';
|
||||
import { BalanceSheetRepository } from './BalanceSheetRepository';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
|
||||
export const BalanceSheetNetIncomeDatePeriodsPY = <
|
||||
T extends GConstructor<FinancialSheet>,
|
||||
>(
|
||||
Base: T,
|
||||
) =>
|
||||
class extends R.pipe(
|
||||
BalanceSheetComparsionPreviousYear,
|
||||
FinancialPreviousPeriod,
|
||||
FinancialHorizTotals,
|
||||
)(Base) {
|
||||
query: BalanceSheetQuery;
|
||||
repository: BalanceSheetRepository;
|
||||
|
||||
/**
|
||||
* Retrieves the PY total income of the given date period.
|
||||
* @param {Date} toDate -
|
||||
* @return {number}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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 {}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
IBalanceSheetDataNode,
|
||||
IBalanceSheetNetIncomeNode,
|
||||
} from './BalanceSheet.types';
|
||||
import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod';
|
||||
import { FinancialPreviousPeriod } from '../../common/FinancialPreviousPeriod';
|
||||
import { FinancialHorizTotals } from '../../common/FinancialHorizTotals';
|
||||
import { BalanceSheetRepository } from './BalanceSheetRepository';
|
||||
import { BalanceSheetQuery } from './BalanceSheetQuery';
|
||||
import { BalanceSheetNetIncomeDatePeriodsPP } from './BalanceSheetNetIncomeDatePeriodsPP';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
|
||||
export const BalanceSheetNetIncomePP = <T extends GConstructor<FinancialSheet>>(
|
||||
Base: T,
|
||||
) =>
|
||||
class extends R.pipe(
|
||||
BalanceSheetNetIncomeDatePeriodsPP,
|
||||
BalanceSheetComparsionPreviousPeriod,
|
||||
FinancialPreviousPeriod,
|
||||
FinancialHorizTotals,
|
||||
)(Base) {
|
||||
public repository: BalanceSheetRepository;
|
||||
public query: BalanceSheetQuery;
|
||||
|
||||
// -------------------------------
|
||||
// # Previous Period (PP)
|
||||
// -------------------------------
|
||||
/**
|
||||
* Retrieves the PP net income.
|
||||
* @returns {}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { IBalanceSheetNetIncomeNode } from './BalanceSheet.types';
|
||||
import { BalanceSheetComparsionPreviousYear } from './BalanceSheetComparsionPreviousYear';
|
||||
import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod';
|
||||
import { FinancialPreviousPeriod } from '../../common/FinancialPreviousPeriod';
|
||||
import { FinancialHorizTotals } from '../../common/FinancialHorizTotals';
|
||||
import { BalanceSheetNetIncomeDatePeriodsPY } from './BalanceSheetNetIncomeDatePeriodsPY';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
|
||||
export const BalanceSheetNetIncomePY = <T extends GConstructor<FinancialSheet>>(
|
||||
Base: T,
|
||||
) =>
|
||||
class extends R.pipe(
|
||||
BalanceSheetNetIncomeDatePeriodsPY,
|
||||
BalanceSheetComparsionPreviousYear,
|
||||
BalanceSheetComparsionPreviousPeriod,
|
||||
FinancialPreviousPeriod,
|
||||
FinancialHorizTotals,
|
||||
)(Base) {
|
||||
// public repository: BalanceSheetRepository;
|
||||
// public query: BalanceSheetQuery;
|
||||
|
||||
// ------------------------------
|
||||
// # Previous Year (PY)
|
||||
// ------------------------------
|
||||
/**
|
||||
* Retrieves the previous year (PY) net income.
|
||||
* @returns {number}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { TableSheetPdf } from '../../common/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 {IBalanceSheetQuery} query - Balance sheet query.
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public async pdf(query: IBalanceSheetQuery): Promise<Buffer> {
|
||||
const table = await this.balanceSheetTable.table(query);
|
||||
|
||||
return this.tableSheetPdf.convertToPdf(
|
||||
table.table,
|
||||
table.meta.sheetName,
|
||||
table.meta.formattedDateRange,
|
||||
HtmlTableCustomCss,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { get } from 'lodash';
|
||||
import { BalanceSheetQuery } from './BalanceSheetQuery';
|
||||
import { IBalanceSheetDataNode } from './BalanceSheet.types';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
|
||||
export const BalanceSheetPercentage = <T extends GConstructor<FinancialSheet>>(
|
||||
Base: T,
|
||||
) =>
|
||||
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)
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,186 @@
|
||||
// @ts-nocheck
|
||||
import { merge } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
IBalanceSheetQuery,
|
||||
IFinancialDatePeriodsUnit,
|
||||
} from './BalanceSheet.types';
|
||||
import { FinancialDateRanges } from '../../common/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;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
// @ts-nocheck
|
||||
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||
import * as R from 'ramda';
|
||||
import { Knex } from 'knex';
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
IAccountTransactionsGroupBy,
|
||||
IBalanceSheetQuery,
|
||||
} from './BalanceSheet.types';
|
||||
import { BalanceSheetQuery } from './BalanceSheetQuery';
|
||||
import { FinancialDatePeriods } from '../../common/FinancialDatePeriods';
|
||||
import { BalanceSheetRepositoryNetIncome } from './BalanceSheetRepositoryNetIncome';
|
||||
import { ILedger } from '@/modules/Ledger/types/Ledger.types';
|
||||
import { Ledger } from '@/modules/Ledger/Ledger';
|
||||
import { transformToMapBy } from '@/utils/transform-to-map-by';
|
||||
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable({ scope: Scope.TRANSIENT })
|
||||
export class BalanceSheetRepository extends R.compose(
|
||||
BalanceSheetRepositoryNetIncome,
|
||||
FinancialDatePeriods,
|
||||
)(class {}) {
|
||||
/**
|
||||
* Account model.
|
||||
*/
|
||||
@Inject(Account.name)
|
||||
public readonly accountModel: TenantModelProxy<typeof Account>;
|
||||
|
||||
/**
|
||||
* Account transaction model.
|
||||
*/
|
||||
@Inject(AccountTransaction.name)
|
||||
public readonly accountTransactionModel: TenantModelProxy<
|
||||
typeof AccountTransaction
|
||||
>;
|
||||
|
||||
/**
|
||||
* @description Balance sheet query.
|
||||
* @param {BalanceSheetQuery}
|
||||
*/
|
||||
public 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 {IBalanceSheetQuery} query
|
||||
*/
|
||||
public setQuery(query: IBalanceSheetQuery) {
|
||||
this.query = new BalanceSheetQuery(query);
|
||||
|
||||
this.transactionsGroupType = this.getGroupByFromDisplayColumnsBy(
|
||||
this.query.displayColumnsBy,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Async initialize.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public asyncInitialize = async (query: IBalanceSheetQuery) => {
|
||||
this.setQuery(query);
|
||||
|
||||
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 () => {
|
||||
this.accountsGraph = this.accountModel().toDependencyGraph(this.accounts);
|
||||
};
|
||||
|
||||
// ----------------------------
|
||||
// # Closing Total
|
||||
// ----------------------------
|
||||
/**
|
||||
* Initialize accounts closing total based on the given query.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public initAccountsTotalLedger = async (): Promise<void> => {
|
||||
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<void>}
|
||||
*/
|
||||
public initTotalDatePeriods = async (): Promise<void> => {
|
||||
// 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<void>}
|
||||
*/
|
||||
public initTotalPreviousYear = async (): Promise<void> => {
|
||||
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<void>}
|
||||
*/
|
||||
public initPeriodsPreviousYear = async (): Promise<void> => {
|
||||
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<void>}
|
||||
*/
|
||||
public initTotalPreviousPeriod = async (): Promise<void> => {
|
||||
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<void>}
|
||||
*/
|
||||
public initPeriodsPreviousPeriod = async (): Promise<void> => {
|
||||
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<IAccount[]>}
|
||||
*/
|
||||
public getAccounts = () => {
|
||||
return this.accountModel().query();
|
||||
};
|
||||
|
||||
/**
|
||||
* Closing accounts date periods.
|
||||
* @param {Date} fromDate
|
||||
* @param {Date} toDate
|
||||
* @param {string} datePeriodsType
|
||||
* @returns
|
||||
*/
|
||||
public accountsDatePeriods = async (
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
datePeriodsType: string,
|
||||
) => {
|
||||
return this.accountTransactionModel()
|
||||
.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) => {
|
||||
return this.accountTransactionModel()
|
||||
.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
|
||||
*/
|
||||
public commonFilterBranchesQuery = (query: Knex.QueryBuilder) => {
|
||||
if (!isEmpty(this.query.branchesIds)) {
|
||||
query.modify('filterByBranches', this.query.branchesIds);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
// @ts-nocheck
|
||||
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 { GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
|
||||
export const BalanceSheetRepositoryNetIncome = <
|
||||
T extends GConstructor<FinancialSheet>,
|
||||
>(
|
||||
Base: T,
|
||||
) =>
|
||||
class extends R.pipe(FinancialDatePeriods)(Base) {
|
||||
// -----------------------
|
||||
// # Net Income
|
||||
// -----------------------
|
||||
public incomeAccounts: ModelObject<Account>[];
|
||||
public incomeAccountsIds: number[];
|
||||
|
||||
public expenseAccounts: ModelObject<Account>[];
|
||||
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<void>}
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
public 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.
|
||||
*/
|
||||
public 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.
|
||||
*/
|
||||
public initIncomeTotalLedger = (): void => {
|
||||
// Inject to the repository.
|
||||
this.incomeLedger = this.totalAccountsLedger.whereAccountsIds(
|
||||
this.incomeAccountsIds,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize the expenses total ledger.
|
||||
*/
|
||||
public 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,
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import {
|
||||
BALANCE_SHEET_SCHEMA_NODE_ID,
|
||||
BALANCE_SHEET_SCHEMA_NODE_TYPE,
|
||||
} from './BalanceSheet.types';
|
||||
import { FinancialSchema } from '../../common/FinancialSchema';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { ACCOUNT_TYPE } from '@/constants/accounts';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
|
||||
export const BalanceSheetSchema = <T extends GConstructor<FinancialSheet>>(
|
||||
Base: T,
|
||||
) =>
|
||||
class extends 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,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,283 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
IBalanceSheetStatementData,
|
||||
IBalanceSheetQuery,
|
||||
BALANCE_SHEET_SCHEMA_NODE_TYPE,
|
||||
IBalanceSheetDataNode,
|
||||
IBalanceSheetSchemaNode,
|
||||
IBalanceSheetNetIncomeNode,
|
||||
IBalanceSheetAccountNode,
|
||||
IBalanceSheetAccountsNode,
|
||||
IBalanceSheetAggregateNode,
|
||||
} from './BalanceSheet.types';
|
||||
import {
|
||||
ITableColumnAccessor,
|
||||
ITableColumn,
|
||||
ITableRow,
|
||||
} from '../../types/Table.types';
|
||||
import { tableRowMapper } from '../../utils/Table.utils';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
import { BalanceSheetComparsionPreviousYear } from './BalanceSheetComparsionPreviousYear';
|
||||
import { IROW_TYPE, DISPLAY_COLUMNS_BY } from './constants';
|
||||
import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod';
|
||||
import { BalanceSheetPercentage } from './BalanceSheetPercentage';
|
||||
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
|
||||
import { BalanceSheetBase } from './BalanceSheetBase';
|
||||
import { BalanceSheetTablePercentage } from './BalanceSheetTablePercentage';
|
||||
import { BalanceSheetTablePreviousYear } from './BalanceSheetTablePreviousYear';
|
||||
import { BalanceSheetTablePreviousPeriod } from './BalanceSheetTablePreviousPeriod';
|
||||
import { FinancialTable } from '../../common/FinancialTable';
|
||||
import { BalanceSheetQuery } from './BalanceSheetQuery';
|
||||
import { BalanceSheetTableDatePeriods } from './BalanceSheetTableDatePeriods';
|
||||
|
||||
export class BalanceSheetTable extends R.pipe(
|
||||
BalanceSheetBase,
|
||||
FinancialTable,
|
||||
FinancialSheetStructure,
|
||||
BalanceSheetPercentage,
|
||||
BalanceSheetComparsionPreviousPeriod,
|
||||
BalanceSheetComparsionPreviousYear,
|
||||
BalanceSheetTablePercentage,
|
||||
BalanceSheetTableDatePeriods,
|
||||
BalanceSheetTablePreviousYear,
|
||||
BalanceSheetTablePreviousPeriod,
|
||||
)(FinancialSheet) {
|
||||
/**
|
||||
* Balance sheet data.
|
||||
* @param {IBalanceSheetStatementData}
|
||||
*/
|
||||
public reportData: IBalanceSheetStatementData;
|
||||
|
||||
/**
|
||||
* Balance sheet query.
|
||||
* @parma {BalanceSheetQuery}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public isNodeType = R.curry(
|
||||
(type: string, node: IBalanceSheetSchemaNode): boolean => {
|
||||
return node.nodeType === type;
|
||||
},
|
||||
);
|
||||
|
||||
// -------------------------
|
||||
// # Accessors.
|
||||
// -------------------------
|
||||
/**
|
||||
* Retrieve the common columns for all report nodes.
|
||||
* @param {ITableColumnAccessor[]}
|
||||
*/
|
||||
public 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[]}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public nodesToTableRowsMapper = (
|
||||
nodes: IBalanceSheetDataNode[],
|
||||
): ITableRow[] => {
|
||||
return this.mapNodesDeep(nodes, this.nodeToTableRowsMapper);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the total children columns.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
public 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[]}
|
||||
*/
|
||||
public 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()),
|
||||
),
|
||||
)([]);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import * as moment from 'moment';
|
||||
import { ITableColumn, ITableColumnAccessor } from '../../types/Table.types';
|
||||
import { FinancialDatePeriods } from '../../common/FinancialDatePeriods';
|
||||
import { IDateRange } from '../CashFlow/Cashflow.types';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
|
||||
export const BalanceSheetTableDatePeriods = <
|
||||
T extends GConstructor<FinancialSheet>,
|
||||
>(
|
||||
Base: T,
|
||||
) =>
|
||||
class extends R.pipe(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}
|
||||
*/
|
||||
public 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 -
|
||||
*/
|
||||
public 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[]}
|
||||
*/
|
||||
public datePeriodsColumnsAccessors = (): ITableColumnAccessor[] => {
|
||||
return R.compose(
|
||||
R.flatten,
|
||||
R.addIndex(R.map)(this.datePeriodColumnsAccessor),
|
||||
)(this.datePeriods);
|
||||
};
|
||||
|
||||
// -------------------------
|
||||
// # Columns.
|
||||
// -------------------------
|
||||
/**
|
||||
*
|
||||
* @param {number} index
|
||||
* @param {} dateRange
|
||||
* @returns {}
|
||||
*/
|
||||
public 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
|
||||
*/
|
||||
public 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[]}
|
||||
*/
|
||||
public datePeriodsColumns = (): ITableColumn[] => {
|
||||
return this.datePeriods.map(this.datePeriodColumn);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { I18nService } from 'nestjs-i18n';
|
||||
import { BalanceSheetInjectable } from './BalanceSheetInjectable';
|
||||
import { BalanceSheetTable } from './BalanceSheetTable';
|
||||
import { IBalanceSheetQuery, IBalanceSheetTable } from './BalanceSheet.types';
|
||||
|
||||
@Injectable()
|
||||
export class BalanceSheetTableInjectable {
|
||||
constructor(
|
||||
private readonly balanceSheetService: BalanceSheetInjectable,
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the balance sheet in table format.
|
||||
* @param {number} query -
|
||||
* @returns {Promise<IBalanceSheetTable>}
|
||||
*/
|
||||
public async table(filter: IBalanceSheetQuery): Promise<IBalanceSheetTable> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { I18nService } from 'nestjs-i18n';
|
||||
import { ITableColumn } from '../../types/Table.types';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { BalanceSheetQuery } from './BalanceSheetQuery';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
|
||||
export const BalanceSheetTablePercentage = <
|
||||
T extends GConstructor<FinancialSheet>,
|
||||
>(
|
||||
Base: T,
|
||||
) =>
|
||||
class BalanceSheetComparsionPreviousYear extends Base {
|
||||
public readonly query: BalanceSheetQuery;
|
||||
public readonly i18n: I18nService;
|
||||
|
||||
// --------------------
|
||||
// # Columns
|
||||
// --------------------
|
||||
/**
|
||||
* Retrieve percentage of column/row columns.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
public percentageColumns = (): ITableColumn[] => {
|
||||
return R.pipe(
|
||||
R.when(
|
||||
this.query.isColumnsPercentageActive,
|
||||
R.append({
|
||||
key: 'percentage_of_column',
|
||||
label: this.i18n.t('balance_sheet.percentage_of_column'),
|
||||
}),
|
||||
),
|
||||
R.when(
|
||||
this.query.isRowsPercentageActive,
|
||||
R.append({
|
||||
key: 'percentage_of_row',
|
||||
label: this.i18n.t('balance_sheet.percentage_of_row'),
|
||||
}),
|
||||
),
|
||||
)([]);
|
||||
};
|
||||
|
||||
// --------------------
|
||||
// # Accessors
|
||||
// --------------------
|
||||
/**
|
||||
* Retrieves percentage of column/row accessors.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
public 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[]}
|
||||
*/
|
||||
public 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`,
|
||||
}),
|
||||
),
|
||||
)([]);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { BalanceSheetQuery } from './BalanceSheetQuery';
|
||||
import { FinancialTablePreviousPeriod } from '../../common/FinancialTablePreviousPeriod';
|
||||
import { FinancialDateRanges } from '../../common/FinancialDateRanges';
|
||||
import { IDateRange } from '../../types/Report.types';
|
||||
import { ITableColumn } from '../../types/Table.types';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
|
||||
export const BalanceSheetTablePreviousPeriod = <
|
||||
T extends GConstructor<FinancialSheet>,
|
||||
>(
|
||||
Base: T,
|
||||
) =>
|
||||
class BalanceSheetTablePreviousPeriod extends R.pipe(
|
||||
FinancialTablePreviousPeriod,
|
||||
FinancialDateRanges,
|
||||
)(Base) {
|
||||
readonly query: BalanceSheetQuery;
|
||||
|
||||
// --------------------
|
||||
// # Columns
|
||||
// --------------------
|
||||
/**
|
||||
* Retrieves the previous period columns.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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[]}
|
||||
*/
|
||||
public 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
|
||||
*/
|
||||
public 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)),
|
||||
),
|
||||
)([]);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
// @ts-nocheck
|
||||
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 { GConstructor } from '@/common/types/Constructor';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
import { BalanceSheetQuery } from './BalanceSheetQuery';
|
||||
|
||||
export const BalanceSheetTablePreviousYear = <
|
||||
T extends GConstructor<FinancialSheet>,
|
||||
>(
|
||||
Base: T,
|
||||
) =>
|
||||
class extends R.pipe(FinancialTablePreviousYear, FinancialDateRanges)(Base) {
|
||||
query: BalanceSheetQuery;
|
||||
|
||||
// --------------------
|
||||
// # 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)),
|
||||
),
|
||||
)([]);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import * as R from 'ramda';
|
||||
|
||||
export const BalanceSheetTotal = (Base: any) => class extends Base {};
|
||||
@@ -0,0 +1,100 @@
|
||||
import { IBalanceSheetQuery } from "./BalanceSheet.types";
|
||||
|
||||
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;
|
||||
}
|
||||
`;
|
||||
|
||||
export const getBalanceSheetDefaultQuery = (): 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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,670 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { defaultTo, set, sumBy, isEmpty, mapValues, get } from 'lodash';
|
||||
import * as mathjs from 'mathjs';
|
||||
import * as moment from 'moment';
|
||||
import { I18nService } from 'nestjs-i18n';
|
||||
import {
|
||||
ICashFlowSchemaSection,
|
||||
ICashFlowStatementQuery,
|
||||
ICashFlowStatementNetIncomeSection,
|
||||
ICashFlowStatementAccountSection,
|
||||
ICashFlowSchemaSectionAccounts,
|
||||
ICashFlowStatementAccountMeta,
|
||||
ICashFlowSchemaAccountRelation,
|
||||
ICashFlowStatementSectionType,
|
||||
ICashFlowStatementData,
|
||||
ICashFlowSchemaTotalSection,
|
||||
ICashFlowStatementTotalSection,
|
||||
ICashFlowStatementSection,
|
||||
ICashFlowCashBeginningNode,
|
||||
ICashFlowStatementAggregateSection,
|
||||
} from './Cashflow.types';
|
||||
import { CASH_FLOW_SCHEMA } from './schema';
|
||||
import { ACCOUNT_ROOT_TYPE } from '@/constants/accounts';
|
||||
import { CashFlowStatementDatePeriods } from './CashFlowDatePeriods';
|
||||
import { DISPLAY_COLUMNS_BY } from './constants';
|
||||
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
|
||||
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||
import { ILedger } from '@/modules/Ledger/types/Ledger.types';
|
||||
import { INumberFormatQuery } from '../../types/Report.types';
|
||||
import { transformToMapBy } from '@/utils/transform-to-map-by';
|
||||
import { accumSum } from '@/utils/accum-sum';
|
||||
import { ModelObject } from 'objection';
|
||||
import { CashflowStatementBase } from './CashflowStatementBase';
|
||||
|
||||
export class CashFlowStatement extends R.pipe(
|
||||
CashFlowStatementDatePeriods,
|
||||
FinancialSheetStructure,
|
||||
)(CashflowStatementBase) {
|
||||
readonly baseCurrency: string;
|
||||
readonly i18n: I18nService;
|
||||
readonly sectionsByIds = {};
|
||||
readonly cashFlowSchemaMap: Map<string, ICashFlowSchemaSection>;
|
||||
readonly cashFlowSchemaSeq: Array<string>;
|
||||
readonly accountByTypeMap: Map<string, Account[]>;
|
||||
readonly accountsByRootType: Map<string, Account[]>;
|
||||
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: Account[],
|
||||
ledger: ILedger,
|
||||
cashLedger: ILedger,
|
||||
netIncomeLedger: ILedger,
|
||||
query: ICashFlowStatementQuery,
|
||||
baseCurrency: string,
|
||||
i18n: I18nService,
|
||||
) {
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
// --------------------------------------------
|
||||
// # 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.t(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: ModelObject<Account>,
|
||||
): 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.t(sectionSchema.label),
|
||||
footerLabel: this.i18n.t(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.t(schemaSection.label),
|
||||
footerLabel: this.i18n.t(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.t(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: ModelObject<Account>,
|
||||
): 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.t(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));
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { sumBy, mapValues, get } from 'lodash';
|
||||
import { ACCOUNT_ROOT_TYPE } from '@/constants/accounts';
|
||||
import {
|
||||
ICashFlowDatePeriod,
|
||||
ICashFlowStatementNetIncomeSection,
|
||||
ICashFlowStatementAccountSection,
|
||||
ICashFlowStatementSection,
|
||||
ICashFlowSchemaTotalSection,
|
||||
ICashFlowStatementTotalSection,
|
||||
ICashFlowStatementQuery,
|
||||
IDateRange,
|
||||
} from './Cashflow.types';
|
||||
import { IFormatNumberSettings } from '../../types/Report.types';
|
||||
import { dateRangeFromToCollection } from '@/utils/date-range-collection';
|
||||
import { accumSum } from '@/utils/accum-sum';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
import { GConstructor } from '@/common/types/Constructor';
|
||||
import { Ledger } from '@/modules/Ledger/Ledger';
|
||||
|
||||
export const CashFlowStatementDatePeriods = <
|
||||
T extends GConstructor<FinancialSheet>,
|
||||
>(
|
||||
Base: T,
|
||||
) =>
|
||||
class extends Base {
|
||||
dateRangeSet: IDateRange[];
|
||||
query: ICashFlowStatementQuery;
|
||||
netIncomeLedger: Ledger;
|
||||
|
||||
/**
|
||||
* Initialize date range set.
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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[]}
|
||||
*/
|
||||
public getNetIncomeDatePeriods = (
|
||||
section: ICashFlowStatementNetIncomeSection,
|
||||
): ICashFlowDatePeriod[] => {
|
||||
return this.dateRangeSet.map(this.getNetIncomeDatePeriod.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes periods property to net income section.
|
||||
* @param {ICashFlowStatementNetIncomeSection} section
|
||||
* @returns {ICashFlowStatementNetIncomeSection}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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[]}
|
||||
*/
|
||||
public getAccountDatePeriods = (
|
||||
node: ICashFlowStatementAccountSection,
|
||||
): ICashFlowDatePeriod[] => {
|
||||
return this.getNodeDatePeriods(
|
||||
node,
|
||||
this.getAccountTotalDatePeriod.bind(this),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes `periods` property to account node.
|
||||
* @param {ICashFlowStatementAccountSection} node -
|
||||
* @return {ICashFlowStatementAccountSection}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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.
|
||||
*/
|
||||
public 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
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public assocPeriodsToAggregateNode = (
|
||||
node: ICashFlowStatementSection,
|
||||
): ICashFlowStatementSection => {
|
||||
const datePeriods = this.getAggregateNodeDatePeriods(node);
|
||||
return R.assoc('periods', datePeriods, node);
|
||||
};
|
||||
|
||||
// Total equation node --------------------
|
||||
|
||||
public 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[]}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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 {}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public getBeginningCashAccountPeriods = (
|
||||
node: ICashFlowStatementSection,
|
||||
): ICashFlowDatePeriod => {
|
||||
return this.getNodeDatePeriods(node, this.getBeginningCashDatePeriod);
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes `periods` property to cash at beginning date periods.
|
||||
* @param {ICashFlowStatementSection} section -
|
||||
* @return {ICashFlowStatementSection}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public assocCashAtBeginningAccountDatePeriods = (
|
||||
node: ICashFlowStatementSection,
|
||||
): ICashFlowStatementSection => {
|
||||
const datePeriods = this.getBeginningCashAccountPeriods(node);
|
||||
return R.assoc('periods', datePeriods, node);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,177 @@
|
||||
// @ts-nocheck
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as moment from 'moment';
|
||||
import { Knex } from 'knex';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { ModelObject } from 'objection';
|
||||
import { ICashFlowStatementQuery } from './Cashflow.types';
|
||||
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class CashFlowRepository {
|
||||
/**
|
||||
* @param {TenantModelProxy<typeof Account>} accountModel - Account model.
|
||||
* @param {TenantModelProxy<typeof AccountTransaction>} accountTransactionModel - Account transaction model.
|
||||
*/
|
||||
constructor(
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: TenantModelProxy<typeof Account>,
|
||||
|
||||
@Inject(AccountTransaction.name)
|
||||
private readonly accountTransactionModel: TenantModelProxy<
|
||||
typeof AccountTransaction
|
||||
>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 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<IAccount[]>}
|
||||
*/
|
||||
public async cashFlowAccounts(): Promise<Account[]> {
|
||||
const accounts = await this.accountModel().query();
|
||||
return accounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve total of csah at beginning transactions.
|
||||
* @param {ICashFlowStatementQuery} filter -
|
||||
* @return {Promise<IAccountTransaction[]>}
|
||||
*/
|
||||
public async cashAtBeginningTotalTransactions(
|
||||
filter: ICashFlowStatementQuery,
|
||||
): Promise<ModelObject<AccountTransaction>[]> {
|
||||
const cashBeginningPeriod = moment(filter.fromDate)
|
||||
.subtract(1, 'day')
|
||||
.toDate();
|
||||
|
||||
const transactions = await this.accountTransactionModel()
|
||||
.query()
|
||||
.onBuild((query) => {
|
||||
query.modify('creditDebitSummation');
|
||||
|
||||
query.select('accountId');
|
||||
query.groupBy('accountId');
|
||||
|
||||
query.withGraphFetched('account');
|
||||
query.modify('filterDateRange', null, cashBeginningPeriod);
|
||||
|
||||
this.commonFilterBranchesQuery(filter, query);
|
||||
});
|
||||
return transactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve accounts transactions.
|
||||
* @param {ICashFlowStatementQuery} filter
|
||||
* @return {Promise<IAccountTransaction>}
|
||||
*/
|
||||
public async getAccountsTransactions(
|
||||
filter: ICashFlowStatementQuery,
|
||||
): Promise<ModelObject<AccountTransaction>[]> {
|
||||
const groupByDateType = this.getGroupTypeFromPeriodsType(
|
||||
filter.displayColumnsBy,
|
||||
);
|
||||
return await this.accountTransactionModel()
|
||||
.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<IAccountTransaction[]>}
|
||||
*/
|
||||
public async getNetIncomeTransactions(
|
||||
filter: ICashFlowStatementQuery,
|
||||
): Promise<AccountTransaction[]> {
|
||||
const groupByDateType = this.getGroupTypeFromPeriodsType(
|
||||
filter.displayColumnsBy,
|
||||
);
|
||||
return await this.accountTransactionModel()
|
||||
.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 {ICashFlowStatementQuery} filter -
|
||||
* @return {Promise<ModelObject<AccountTransaction>[]>}
|
||||
*/
|
||||
public async cashAtBeginningPeriodTransactions(
|
||||
filter: ICashFlowStatementQuery,
|
||||
): Promise<ModelObject<AccountTransaction>[]> {
|
||||
const groupByDateType = this.getGroupTypeFromPeriodsType(
|
||||
filter.displayColumnsBy,
|
||||
);
|
||||
|
||||
return await this.accountTransactionModel()
|
||||
.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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { ModelObject } from 'objection';
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
ICashFlowStatementQuery,
|
||||
ICashFlowStatementDOO,
|
||||
} from './Cashflow.types';
|
||||
import { CashFlowStatement } from './CashFlow';
|
||||
import { CashflowSheetMeta } from './CashflowSheetMeta';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CashFlowRepository } from './CashFlowRepository';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
import { Ledger } from '@/modules/Ledger/Ledger';
|
||||
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
|
||||
import { I18nService } from 'nestjs-i18n';
|
||||
import { getDefaultCashflowQuery } from './constants';
|
||||
|
||||
@Injectable()
|
||||
export class CashFlowStatementService {
|
||||
/**
|
||||
* @param {CashFlowRepository} cashFlowRepo - Cash flow repository.
|
||||
* @param {CashflowSheetMeta} cashflowSheetMeta - Cashflow sheet meta.
|
||||
* @param {TenancyContext} tenancyContext - Tenancy context.
|
||||
*/
|
||||
constructor(
|
||||
private readonly cashFlowRepo: CashFlowRepository,
|
||||
private readonly cashflowSheetMeta: CashflowSheetMeta,
|
||||
private readonly tenancyContext: TenancyContext,
|
||||
private readonly i18n: I18nService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves cash at beginning transactions.
|
||||
* @param {ICashFlowStatementQuery} filter - Cash flow statement query.
|
||||
* @returns {Promise<ModelObject<AccountTransaction>[]>}
|
||||
*/
|
||||
private async cashAtBeginningTransactions(
|
||||
filter: ICashFlowStatementQuery,
|
||||
): Promise<ModelObject<AccountTransaction>[]> {
|
||||
const appendPeriodsOperToChain = (trans) =>
|
||||
R.append(
|
||||
this.cashFlowRepo.cashAtBeginningPeriodTransactions(filter),
|
||||
trans,
|
||||
);
|
||||
|
||||
const promisesChain = R.pipe(
|
||||
R.append(this.cashFlowRepo.cashAtBeginningTotalTransactions(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 {ICashFlowStatementQuery} query - Cashflow query.
|
||||
* @returns {Promise<ICashFlowStatementDOO>}
|
||||
*/
|
||||
public async cashFlow(
|
||||
query: ICashFlowStatementQuery,
|
||||
): Promise<ICashFlowStatementDOO> {
|
||||
// Retrieve all accounts on the storage.
|
||||
const accounts = await this.cashFlowRepo.cashFlowAccounts();
|
||||
const tenant = await this.tenancyContext.getTenant(true);
|
||||
|
||||
const filter = {
|
||||
...getDefaultCashflowQuery(),
|
||||
...query,
|
||||
};
|
||||
// Retrieve the accounts transactions.
|
||||
const transactions =
|
||||
await this.cashFlowRepo.getAccountsTransactions(filter);
|
||||
// Retrieve the net income transactions.
|
||||
const netIncome = await this.cashFlowRepo.getNetIncomeTransactions(filter);
|
||||
// Retrieve the cash at beginning transactions.
|
||||
const cashAtBeginningTransactions =
|
||||
await this.cashAtBeginningTransactions(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,
|
||||
this.i18n,
|
||||
);
|
||||
// Retrieve the cashflow sheet meta.
|
||||
const meta = await this.cashflowSheetMeta.meta(filter);
|
||||
|
||||
return {
|
||||
data: cashFlowInstance.reportData(),
|
||||
query: filter,
|
||||
meta,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { isEmpty } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { I18nService } from 'nestjs-i18n';
|
||||
import {
|
||||
ICashFlowStatementSection,
|
||||
ICashFlowStatementSectionType,
|
||||
IDateRange,
|
||||
ICashFlowStatementDOO,
|
||||
} from './Cashflow.types';
|
||||
import { ITableRow, ITableColumn } from '../../types/Table.types';
|
||||
import { dateRangeFromToCollection } from '@/utils/date-range-collection';
|
||||
import { tableRowMapper } from '../../utils/Table.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 class CashFlowTable {
|
||||
private report: ICashFlowStatementDOO;
|
||||
private i18n: I18nService;
|
||||
private dateRangeSet: IDateRange[];
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {ICashFlowStatement} reportStatement - Statement.
|
||||
* @param {I18nService} i18n - I18n service.
|
||||
*/
|
||||
constructor(reportStatement: ICashFlowStatementDOO, i18n: I18nService) {
|
||||
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.t('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.t('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),
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines the given column type is the current.
|
||||
* @reutrns {boolean}
|
||||
*/
|
||||
private isDisplayColumnsBy = (displayColumnsType: string): Boolean => {
|
||||
return this.report.query.displayColumnsType === displayColumnsType;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines 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.t('Account name') }]),
|
||||
R.when(
|
||||
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
|
||||
R.concat(this.datePeriodsColumns()),
|
||||
),
|
||||
R.concat(this.totalColumns()),
|
||||
)([]);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Response } from 'express';
|
||||
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
|
||||
import { ICashFlowStatementQuery } from './Cashflow.types';
|
||||
import { AcceptType } from '@/constants/accept-type';
|
||||
import { CashflowSheetApplication } from './CashflowSheetApplication';
|
||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
@Controller('reports/cashflow-statement')
|
||||
@ApiTags('reports')
|
||||
export class CashflowController {
|
||||
constructor(private readonly cashflowSheetApp: CashflowSheetApplication) {}
|
||||
|
||||
@Get()
|
||||
@ApiResponse({ status: 200, description: 'Cashflow statement report' })
|
||||
@ApiOperation({ summary: 'Get cashflow statement report' })
|
||||
async getCashflow(
|
||||
@Query() query: ICashFlowStatementQuery,
|
||||
@Res() res: Response,
|
||||
@Headers('accept') acceptHeader: string,
|
||||
) {
|
||||
// Retrieves the json table format.
|
||||
if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) {
|
||||
const table = await this.cashflowSheetApp.table(query);
|
||||
|
||||
return res.status(200).send(table);
|
||||
// Retrieves the csv format.
|
||||
} else if (acceptHeader.includes(AcceptType.ApplicationCsv)) {
|
||||
const buffer = await this.cashflowSheetApp.csv(query);
|
||||
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
|
||||
return res.status(200).send(buffer);
|
||||
// Retrieves the pdf format.
|
||||
} else if (acceptHeader.includes(AcceptType.ApplicationXlsx)) {
|
||||
const buffer = await this.cashflowSheetApp.xlsx(query);
|
||||
|
||||
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.cashflowSheetApp.pdf(query);
|
||||
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
});
|
||||
return res.send(pdfContent);
|
||||
// Retrieves the json format.
|
||||
} else {
|
||||
const cashflow = await this.cashflowSheetApp.sheet(query);
|
||||
|
||||
return res.status(200).send(cashflow);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
import { Knex } from 'knex';
|
||||
import { IFinancialSheetCommonMeta, INumberFormatQuery } from '../../types/Report.types';
|
||||
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||
import { Ledger } from '@/modules/Ledger/Ledger';
|
||||
import { IFinancialTable, ITableRow } from '../../types/Table.types';
|
||||
|
||||
|
||||
export interface ICashFlowStatementQuery {
|
||||
fromDate: Date | string;
|
||||
toDate: Date | string;
|
||||
displayColumnsBy: string;
|
||||
displayColumnsType: string;
|
||||
noneZero: boolean;
|
||||
noneTransactions: boolean;
|
||||
numberFormat: INumberFormatQuery;
|
||||
basis: string;
|
||||
|
||||
branchesIds?: number[];
|
||||
}
|
||||
|
||||
export interface ICashFlowStatementTotal {
|
||||
amount: number;
|
||||
formattedAmount: string;
|
||||
currencyCode: string;
|
||||
}
|
||||
|
||||
export interface ICashFlowStatementTotalPeriod {
|
||||
fromDate: Date;
|
||||
toDate: Date;
|
||||
total: ICashFlowStatementTotal;
|
||||
}
|
||||
|
||||
export interface ICashFlowStatementCommonSection {
|
||||
id: string;
|
||||
label: string;
|
||||
total: ICashFlowStatementTotal;
|
||||
footerLabel?: string;
|
||||
}
|
||||
|
||||
export interface ICashFlowStatementAccountMeta {
|
||||
id: number;
|
||||
label: string;
|
||||
code: string;
|
||||
total: ICashFlowStatementTotal;
|
||||
accountType: string;
|
||||
adjustmentType: string;
|
||||
sectionType: ICashFlowStatementSectionType.ACCOUNT;
|
||||
}
|
||||
|
||||
export enum ICashFlowStatementSectionType {
|
||||
REGULAR = 'REGULAR',
|
||||
AGGREGATE = 'AGGREGATE',
|
||||
NET_INCOME = 'NET_INCOME',
|
||||
ACCOUNT = 'ACCOUNT',
|
||||
ACCOUNTS = 'ACCOUNTS',
|
||||
TOTAL = 'TOTAL',
|
||||
CASH_AT_BEGINNING = 'CASH_AT_BEGINNING',
|
||||
}
|
||||
|
||||
export interface ICashFlowStatementAccountSection
|
||||
extends ICashFlowStatementCommonSection {
|
||||
sectionType: ICashFlowStatementSectionType.ACCOUNTS;
|
||||
children: ICashFlowStatementAccountMeta[];
|
||||
total: ICashFlowStatementTotal;
|
||||
}
|
||||
|
||||
export interface ICashFlowStatementNetIncomeSection
|
||||
extends ICashFlowStatementCommonSection {
|
||||
sectionType: ICashFlowStatementSectionType.NET_INCOME;
|
||||
}
|
||||
|
||||
export interface ICashFlowStatementTotalSection
|
||||
extends ICashFlowStatementCommonSection {
|
||||
sectionType: ICashFlowStatementSectionType.TOTAL;
|
||||
}
|
||||
|
||||
export interface ICashFlowStatementAggregateSection
|
||||
extends ICashFlowStatementCommonSection {
|
||||
sectionType: ICashFlowStatementSectionType.AGGREGATE;
|
||||
}
|
||||
|
||||
export interface ICashFlowCashBeginningNode
|
||||
extends ICashFlowStatementCommonSection {
|
||||
sectionType: ICashFlowStatementSectionType.CASH_AT_BEGINNING;
|
||||
}
|
||||
|
||||
export type ICashFlowStatementSection =
|
||||
| ICashFlowStatementAccountSection
|
||||
| ICashFlowStatementNetIncomeSection
|
||||
| ICashFlowStatementTotalSection
|
||||
| ICashFlowStatementCommonSection;
|
||||
|
||||
export interface ICashFlowStatementColumn {}
|
||||
export interface ICashFlowStatementMeta extends IFinancialSheetCommonMeta {
|
||||
formattedToDate: string;
|
||||
formattedFromDate: string;
|
||||
formattedDateRange: string;
|
||||
}
|
||||
|
||||
export interface ICashFlowStatementDOO {
|
||||
data: ICashFlowStatementData;
|
||||
meta: ICashFlowStatementMeta;
|
||||
query: ICashFlowStatementQuery;
|
||||
}
|
||||
|
||||
export interface ICashFlowStatementTable extends IFinancialTable {
|
||||
meta: ICashFlowStatementMeta;
|
||||
query: ICashFlowStatementQuery;
|
||||
}
|
||||
|
||||
export interface ICashFlowStatementService {
|
||||
cashFlow(
|
||||
tenantId: number,
|
||||
query: ICashFlowStatementQuery
|
||||
): Promise<ICashFlowStatementDOO>;
|
||||
}
|
||||
|
||||
// CASH FLOW SCHEMA TYPES.
|
||||
// -----------------------------
|
||||
export interface ICashFlowSchemaCommonSection {
|
||||
id: string;
|
||||
label: string;
|
||||
children: ICashFlowSchemaSection[];
|
||||
footerLabel?: string;
|
||||
}
|
||||
|
||||
export enum CASH_FLOW_ACCOUNT_RELATION {
|
||||
MINES = 'mines',
|
||||
PLUS = 'plus',
|
||||
}
|
||||
|
||||
export enum CASH_FLOW_SECTION_ID {
|
||||
NET_INCOME = 'NET_INCOME',
|
||||
OPERATING = 'OPERATING',
|
||||
OPERATING_ACCOUNTS = 'OPERATING_ACCOUNTS',
|
||||
INVESTMENT = 'INVESTMENT',
|
||||
FINANCIAL = 'FINANCIAL',
|
||||
|
||||
NET_OPERATING = 'NET_OPERATING',
|
||||
NET_INVESTMENT = 'NET_INVESTMENT',
|
||||
NET_FINANCIAL = 'NET_FINANCIAL',
|
||||
|
||||
CASH_BEGINNING_PERIOD = 'CASH_BEGINNING_PERIOD',
|
||||
CASH_END_PERIOD = 'CASH_END_PERIOD',
|
||||
NET_CASH_INCREASE = 'NET_CASH_INCREASE',
|
||||
}
|
||||
|
||||
export interface ICashFlowSchemaAccountsSection
|
||||
extends ICashFlowSchemaCommonSection {
|
||||
sectionType: ICashFlowStatementSectionType.ACCOUNT;
|
||||
accountsRelations: ICashFlowSchemaAccountRelation[];
|
||||
}
|
||||
|
||||
export interface ICashFlowSchemaTotalSection
|
||||
extends ICashFlowStatementCommonSection {
|
||||
sectionType: ICashFlowStatementSectionType.TOTAL;
|
||||
equation: string;
|
||||
}
|
||||
|
||||
export type ICashFlowSchemaSection =
|
||||
| ICashFlowSchemaAccountsSection
|
||||
| ICashFlowSchemaTotalSection
|
||||
| ICashFlowSchemaCommonSection;
|
||||
|
||||
export type ICashFlowStatementData = ICashFlowSchemaSection[];
|
||||
|
||||
export interface ICashFlowSchemaAccountRelation {
|
||||
type: string;
|
||||
direction: CASH_FLOW_ACCOUNT_RELATION.PLUS;
|
||||
}
|
||||
|
||||
export interface ICashFlowSchemaSectionAccounts
|
||||
extends ICashFlowStatementCommonSection {
|
||||
type: ICashFlowStatementSectionType.ACCOUNT;
|
||||
accountsRelations: ICashFlowSchemaAccountRelation[];
|
||||
}
|
||||
|
||||
export interface ICashFlowSchemaSectionTotal {
|
||||
type: ICashFlowStatementSectionType.TOTAL;
|
||||
totalEquation: string;
|
||||
}
|
||||
|
||||
export interface ICashFlowDatePeriod {
|
||||
fromDate: ICashFlowDate;
|
||||
toDate: ICashFlowDate;
|
||||
total: ICashFlowStatementTotal;
|
||||
}
|
||||
|
||||
export interface ICashFlowDate {
|
||||
formattedDate: string;
|
||||
date: Date;
|
||||
}
|
||||
|
||||
export interface ICashFlowStatement {
|
||||
/**
|
||||
* Constructor method.
|
||||
* @constructor
|
||||
*/
|
||||
constructor(
|
||||
accounts: Account[],
|
||||
ledger: Ledger,
|
||||
cashLedger: Ledger,
|
||||
netIncomeLedger: Ledger,
|
||||
query: ICashFlowStatementQuery,
|
||||
baseCurrency: string
|
||||
): void;
|
||||
|
||||
reportData(): ICashFlowStatementData;
|
||||
}
|
||||
|
||||
export interface ICashFlowTable {
|
||||
constructor(reportStatement: ICashFlowStatement): void;
|
||||
tableRows(): ITableRow[];
|
||||
}
|
||||
|
||||
export interface IDateRange {
|
||||
fromDate: Date;
|
||||
toDate: Date;
|
||||
}
|
||||
|
||||
export interface ICashflowTransactionSchema {
|
||||
amount: number;
|
||||
date: Date;
|
||||
referenceNo?: string | null;
|
||||
transactionNumber: string;
|
||||
transactionType: string;
|
||||
creditAccountId: number;
|
||||
cashflowAccountId: number;
|
||||
userId: number;
|
||||
publishedAt?: Date | null;
|
||||
branchId?: number;
|
||||
}
|
||||
|
||||
export interface ICashflowTransactionInput extends ICashflowTransactionSchema {}
|
||||
|
||||
export interface ICategorizeCashflowTransactioDTO {
|
||||
date: Date;
|
||||
creditAccountId: number;
|
||||
referenceNo: string;
|
||||
transactionNumber: string;
|
||||
transactionType: string;
|
||||
exchangeRate: number;
|
||||
description: string;
|
||||
branchId: number;
|
||||
}
|
||||
|
||||
export interface IUncategorizedCashflowTransaction {
|
||||
id?: number;
|
||||
amount: number;
|
||||
date: Date;
|
||||
currencyCode: string;
|
||||
accountId: number;
|
||||
description: string;
|
||||
referenceNo: string;
|
||||
categorizeRefType: string;
|
||||
categorizeRefId: number;
|
||||
categorized: boolean;
|
||||
}
|
||||
|
||||
export interface CreateUncategorizedTransactionDTO {
|
||||
date: Date | string;
|
||||
accountId: number;
|
||||
amount: number;
|
||||
currencyCode: string;
|
||||
payee?: string;
|
||||
description?: string;
|
||||
referenceNo?: string | null;
|
||||
plaidTransactionId?: string | null;
|
||||
pending?: boolean;
|
||||
pendingPlaidTransactionId?: string | null;
|
||||
batch?: string;
|
||||
}
|
||||
|
||||
export interface IUncategorizedTransactionCreatingEventPayload {
|
||||
tenantId: number;
|
||||
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IUncategorizedTransactionCreatedEventPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransaction: any;
|
||||
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IPendingTransactionRemovingEventPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransactionId: number;
|
||||
pendingTransaction: IUncategorizedCashflowTransaction;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IPendingTransactionRemovedEventPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransactionId: number;
|
||||
pendingTransaction: IUncategorizedCashflowTransaction;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CashflowTableInjectable } from './CashflowTableInjectable';
|
||||
import { ICashFlowStatementQuery } from './Cashflow.types';
|
||||
import { TableSheet } from '../../common/TableSheet';
|
||||
|
||||
@Injectable()
|
||||
export class CashflowExportInjectable {
|
||||
constructor(private readonly cashflowSheetTable: CashflowTableInjectable) {}
|
||||
|
||||
/**
|
||||
* Retrieves the cashflow sheet in XLSX format.
|
||||
* @param {ICashFlowStatementQuery} query - Cashflow statement query.
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public async xlsx(query: ICashFlowStatementQuery): Promise<Buffer> {
|
||||
const table = await this.cashflowSheetTable.table(query);
|
||||
|
||||
const tableSheet = new TableSheet(table.table);
|
||||
const tableCsv = tableSheet.convertToXLSX();
|
||||
|
||||
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the cashflow sheet in CSV format.
|
||||
* @param {ICashFlowStatementQuery} query - Cashflow statement query.
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public async csv(query: ICashFlowStatementQuery): Promise<string> {
|
||||
const table = await this.cashflowSheetTable.table(query);
|
||||
|
||||
const tableSheet = new TableSheet(table.table);
|
||||
const tableCsv = tableSheet.convertToCSV();
|
||||
|
||||
return tableCsv;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { CashflowExportInjectable } from './CashflowExportInjectable';
|
||||
import { ICashFlowStatementQuery } from './Cashflow.types';
|
||||
import { CashFlowStatementService } from './CashFlowService';
|
||||
import { CashflowTableInjectable } from './CashflowTableInjectable';
|
||||
import { CashflowTablePdfInjectable } from './CashflowTablePdfInjectable';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class CashflowSheetApplication {
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {CashflowExportInjectable} cashflowExport
|
||||
* @param {} cashflowSheet
|
||||
* @param cashflowTable
|
||||
* @param cashflowPdf
|
||||
*/
|
||||
constructor(
|
||||
private readonly cashflowExport: CashflowExportInjectable,
|
||||
private readonly cashflowSheet: CashFlowStatementService,
|
||||
private readonly cashflowTable: CashflowTableInjectable,
|
||||
private readonly cashflowPdf: CashflowTablePdfInjectable,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the cashflow sheet
|
||||
* @param {ICashFlowStatementQuery} query - Cashflow statement query.
|
||||
*/
|
||||
public async sheet(query: ICashFlowStatementQuery) {
|
||||
return this.cashflowSheet.cashFlow(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the cashflow sheet in table format.
|
||||
* @param {ICashFlowStatementQuery} query - Cashflow statement query.
|
||||
*/
|
||||
public async table(query: ICashFlowStatementQuery) {
|
||||
return this.cashflowTable.table(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the cashflow sheet in XLSX format.
|
||||
* @param {ICashFlowStatementQuery} query - Cashflow statement query.
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public async xlsx(query: ICashFlowStatementQuery) {
|
||||
return this.cashflowExport.xlsx(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the cashflow sheet in CSV format.
|
||||
* @param {ICashFlowStatementQuery} query - Cashflow statement query.
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public async csv(query: ICashFlowStatementQuery): Promise<string> {
|
||||
return this.cashflowExport.csv(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the cashflow sheet in pdf format.
|
||||
* @param {ICashFlowStatementQuery} query - Cashflow statement query.
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public async pdf(query: ICashFlowStatementQuery): Promise<Buffer> {
|
||||
return this.cashflowPdf.pdf(query);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as moment from 'moment';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
|
||||
import {
|
||||
ICashFlowStatementMeta,
|
||||
ICashFlowStatementQuery,
|
||||
} from './Cashflow.types';
|
||||
|
||||
@Injectable()
|
||||
export class CashflowSheetMeta {
|
||||
constructor(private readonly financialSheetMeta: FinancialSheetMeta) {}
|
||||
|
||||
/**
|
||||
* Cashflow sheet meta.
|
||||
* @param {ICashFlowStatementQuery} query
|
||||
* @returns {Promise<ICashFlowStatementMeta>}
|
||||
*/
|
||||
public async meta(
|
||||
query: ICashFlowStatementQuery,
|
||||
): Promise<ICashFlowStatementMeta> {
|
||||
const meta = await this.financialSheetMeta.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}`;
|
||||
|
||||
const sheetName = 'Statement of Cash Flow';
|
||||
|
||||
return {
|
||||
...meta,
|
||||
sheetName,
|
||||
formattedToDate,
|
||||
formattedFromDate,
|
||||
formattedDateRange,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CashflowSheetMeta } from './CashflowSheetMeta';
|
||||
import { CashFlowRepository } from './CashFlowRepository';
|
||||
import { CashflowTablePdfInjectable } from './CashflowTablePdfInjectable';
|
||||
import { CashflowExportInjectable } from './CashflowExportInjectable';
|
||||
import { CashflowController } from './Cashflow.controller';
|
||||
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
|
||||
import { CashflowTableInjectable } from './CashflowTableInjectable';
|
||||
import { CashFlowStatementService } from './CashFlowService';
|
||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||
import { CashflowSheetApplication } from './CashflowSheetApplication';
|
||||
|
||||
@Module({
|
||||
imports: [FinancialSheetCommonModule],
|
||||
providers: [
|
||||
CashFlowRepository,
|
||||
CashflowSheetMeta,
|
||||
CashFlowStatementService,
|
||||
CashflowTablePdfInjectable,
|
||||
CashflowExportInjectable,
|
||||
CashflowTableInjectable,
|
||||
CashflowSheetApplication,
|
||||
TenancyContext,
|
||||
],
|
||||
controllers: [CashflowController],
|
||||
})
|
||||
export class CashflowStatementModule {}
|
||||
@@ -0,0 +1,47 @@
|
||||
// @ts-nocheck
|
||||
import * as R from 'ramda';
|
||||
import { map } from 'lodash';
|
||||
import { Account } from "@/modules/Accounts/models/Account.model";
|
||||
import { ICashFlowStatementQuery } from './Cashflow.types';
|
||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||
|
||||
export class CashflowStatementBase extends FinancialSheet {
|
||||
readonly accountsByRootType: Map<string, Account[]>;
|
||||
readonly query: ICashFlowStatementQuery;
|
||||
|
||||
// --------------------------------------------
|
||||
// # GENERAL UTILITIES
|
||||
// --------------------------------------------
|
||||
/**
|
||||
* Retrieve the expense accounts ids.
|
||||
* @return {number[]}
|
||||
*/
|
||||
public 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}
|
||||
*/
|
||||
public isDisplayColumnsBy = (displayColumnsBy: string): boolean => {
|
||||
return this.query.displayColumnsType === displayColumnsBy;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adjustments the given amount.
|
||||
* @param {string} direction
|
||||
* @param {number} amount -
|
||||
* @return {number}
|
||||
*/
|
||||
public amountAdjustment = (direction: 'mines' | 'plus', amount): number => {
|
||||
return R.when(
|
||||
R.always(R.equals(direction, 'mines')),
|
||||
R.multiply(-1),
|
||||
)(amount);
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user