refactor: financial reports to nestjs

This commit is contained in:
Ahmed Bouhuolia
2025-01-18 22:32:45 +02:00
parent 6dd854178d
commit dfc5674088
151 changed files with 5264 additions and 1296 deletions

View File

@@ -1,9 +1,16 @@
import { Module } from '@nestjs/common';
import { TableSheetPdf } from './TableSheetPdf';
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';
//
@Module({
providers: [TableSheetPdf],
imports: [PurchasesByItemsModule],
providers: [],
imports: [
PurchasesByItemsModule,
CustomerBalanceSummaryModule,
SalesByItemsModule,
GeneralLedgerModule
],
})
export class FinancialStatementsModule {}

View File

@@ -3,14 +3,16 @@ import { memoize } from 'lodash';
import {
IAccountTransactionsGroupBy,
IFinancialDatePeriodsUnit,
IFinancialSheetTotalPeriod,
IFormatNumberSettings,
} from '../types/Report.types';
import { dateRangeFromToCollection } from '@/utils/date-range-collection';
import { FinancialDateRanges } from './FinancialDateRanges';
import { Constructor } from '@/common/types/Constructor';
import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from './FinancialSheet';
export const FinancialDatePeriods = <T extends Constructor>(Base: T) =>
export const FinancialDatePeriods = <T extends GConstructor<FinancialSheet>>(
Base: T,
) =>
class extends R.compose(FinancialDateRanges)(Base) {
/**
* Retrieves the date ranges from the given from date to the given to date.
@@ -19,9 +21,9 @@ export const FinancialDatePeriods = <T extends Constructor>(Base: T) =>
* @param {string} unit
*/
public getDateRanges = memoize(
(fromDate: Date, toDate: Date, unit: string) => {
(fromDate: Date, toDate: Date, unit: moment.unitOfTime.StartOf) => {
return dateRangeFromToCollection(fromDate, toDate, unit);
}
},
);
/**
@@ -35,7 +37,7 @@ export const FinancialDatePeriods = <T extends Constructor>(Base: T) =>
total: number,
fromDate: Date,
toDate: Date,
overrideSettings?: IFormatNumberSettings
overrideSettings?: IFormatNumberSettings,
): IFinancialSheetTotalPeriod => {
return {
fromDate: this.getDateMeta(fromDate),
@@ -55,7 +57,7 @@ export const FinancialDatePeriods = <T extends Constructor>(Base: T) =>
total: number,
fromDate: Date,
toDate: Date,
overrideSettings: IFormatNumberSettings = {}
overrideSettings: IFormatNumberSettings = {},
) => {
return this.getDatePeriodMeta(total, fromDate, toDate, {
money: true,
@@ -79,8 +81,8 @@ export const FinancialDatePeriods = <T extends Constructor>(Base: T) =>
node: any,
fromDate: Date,
toDate: Date,
index: number
) => any
index: number,
) => any,
) => {
const curriedCallback = R.curry(callback)(node);
// Retrieves memorized date ranges.
@@ -88,7 +90,7 @@ export const FinancialDatePeriods = <T extends Constructor>(Base: T) =>
return dateRanges.map((dateRange, index) => {
return curriedCallback(dateRange.fromDate, dateRange.toDate, index);
});
}
},
);
/**
* Retrieve the accounts transactions group type from display columns by.
@@ -96,7 +98,7 @@ export const FinancialDatePeriods = <T extends Constructor>(Base: T) =>
* @returns {IAccountTransactionsGroupBy}
*/
public getGroupByFromDisplayColumnsBy = (
columnsBy: IFinancialDatePeriodsUnit
columnsBy: IFinancialDatePeriodsUnit,
): IAccountTransactionsGroupBy => {
const paris = {
week: IAccountTransactionsGroupBy.Day,

View File

@@ -1,14 +1,17 @@
import moment from 'moment';
import { IDateRange, IFinancialDatePeriodsUnit } from '../types/Report.types';
import { Constructor } from '@/common/types/Constructor';
import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from './FinancialSheet';
export const FinancialDateRanges = <T extends Constructor>(Base: T) =>
export const FinancialDateRanges = <T extends GConstructor<FinancialSheet>>(
Base: T,
) =>
class extends Base {
/**
* Retrieve previous period (PP) date of the given date.
* @param {Date} fromDate -
* @param {Date} toDate -
* @param {IFinancialDatePeriodsUnit} unit -
* @param {Date} date - Date.
* @param {number} value - Value.
* @param {IFinancialDatePeriodsUnit} unit - Unit of time.
* @returns {Date}
*/
public getPreviousPeriodDate = (
@@ -20,10 +23,10 @@ export const FinancialDateRanges = <T extends Constructor>(Base: T) =>
};
/**
* Retrieves the different
* Retrieves the different between two dates.
* @param {Date} fromDate
* @param {Date} toDate
* @returns
* @returns {number}
*/
public getPreviousPeriodDiff = (fromDate: Date, toDate: Date) => {
return moment(toDate).diff(fromDate, 'days') + 1;
@@ -31,8 +34,11 @@ export const FinancialDateRanges = <T extends Constructor>(Base: T) =>
/**
* Retrieves the periods period dates.
* @param {Date} fromDate -
* @param {Date} toDate -
* @param {Date} fromDate - From date.
* @param {Date} toDate - To date.
* @param {IFinancialDatePeriodsUnit} unit - Unit of time.
* @param {number} amount - Amount of time.
* @returns {IDateRange}
*/
public getPreviousPeriodDateRange = (
fromDate: Date,
@@ -65,9 +71,9 @@ export const FinancialDateRanges = <T extends Constructor>(Base: T) =>
/**
* Retrieves the previous period (PP) date range of date periods columns.
* @param {Date} fromDate -
* @param {Date} toDate -
* @param {IFinancialDatePeriodsUnit}
* @param {Date} fromDate - From date.
* @param {Date} toDate - To date.
* @param {IFinancialDatePeriodsUnit} unit - Unit of time.
* @returns {IDateRange}
*/
public getPPDatePeriodDateRange = (

View File

@@ -1,12 +1,16 @@
import * as mathjs from 'mathjs';
import * as R from 'ramda';
import { compose } from 'lodash/fp';
import { omit, get, mapValues } from 'lodash';
import { FinancialSheetStructure } from './FinancialSheetStructure';
import { Constructor } from '@/common/types/Constructor';
import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from './FinancialSheet';
export const FinancialEvaluateEquation = <T extends Constructor>(Base: T) =>
class extends compose(FinancialSheetStructure)(Base) {
export const FinancialEvaluateEquation = <
T extends GConstructor<FinancialSheet>,
>(
Base: T
) =>
class FinancialEvaluateEquation extends R.compose(FinancialSheetStructure)(Base) {
/**
* Evauluate equaation string with the given scope table.
* @param {string} equation -
@@ -34,7 +38,6 @@ export const FinancialEvaluateEquation = <T extends Constructor>(Base: T) =>
}
return acc;
},
{}
);
};

View File

@@ -1,9 +1,12 @@
import * as R from 'ramda';
import { get, isEmpty } from 'lodash';
import { Constructor } from '@/common/types/Constructor';
import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from './FinancialSheet';
export const FinancialHorizTotals = <T extends Constructor>(Base: T) =>
class extends Base {
export const FinancialHorizTotals = <T extends GConstructor<FinancialSheet>>(
Base: T,
) =>
class FinancialHorizTotals extends Base {
/**
*
*/

View File

@@ -4,9 +4,14 @@ import {
IFinancialNodeWithPreviousPeriod,
} from '../types/Report.types';
import * as R from 'ramda';
import { Constructor, GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from './FinancialSheet';
import { FinancialDatePeriods } from './FinancialDatePeriods';
export const FinancialPreviousPeriod = (Base) =>
class extends Base {
export const FinancialPreviousPeriod = <T extends GConstructor<FinancialSheet>>(
Base: T,
) =>
class extends R.compose(FinancialDatePeriods)(Base) {
// ---------------------------
// # Common Node.
// ---------------------------
@@ -16,16 +21,16 @@ export const FinancialPreviousPeriod = (Base) =>
* @returns {IFinancialNodeWithPreviousPeriod}
*/
public assocPreviousPeriodPercentageNode = (
accountNode: IProfitLossSheetAccountNode
accountNode: IProfitLossSheetAccountNode,
): IFinancialNodeWithPreviousPeriod => {
const percentage = this.getPercentageBasis(
accountNode.previousPeriod.amount,
accountNode.previousPeriodChange.amount
accountNode.previousPeriodChange.amount,
);
return R.assoc(
'previousPeriodPercentage',
this.getPercentageAmountMeta(percentage),
accountNode
accountNode,
);
};
@@ -35,16 +40,16 @@ export const FinancialPreviousPeriod = (Base) =>
* @returns {IFinancialNodeWithPreviousPeriod}
*/
public assocPreviousPeriodChangeNode = (
accountNode: IProfitLossSheetAccountNode
accountNode: IProfitLossSheetAccountNode,
): IFinancialNodeWithPreviousPeriod => {
const change = this.getAmountChange(
accountNode.total.amount,
accountNode.previousPeriod.amount
accountNode.previousPeriod.amount,
);
return R.assoc(
'previousPeriodChange',
this.getAmountMeta(change),
accountNode
accountNode,
);
};
@@ -57,16 +62,16 @@ export const FinancialPreviousPeriod = (Base) =>
* @returns {IFinancialNodeWithPreviousPeriod}
*/
public assocPreviousPeriodTotalPercentageNode = (
accountNode: IProfitLossSheetAccountNode
accountNode: IProfitLossSheetAccountNode,
): IFinancialNodeWithPreviousPeriod => {
const percentage = this.getPercentageBasis(
accountNode.previousPeriod.amount,
accountNode.previousPeriodChange.amount
accountNode.previousPeriodChange.amount,
);
return R.assoc(
'previousPeriodPercentage',
this.getPercentageTotalAmountMeta(percentage),
accountNode
accountNode,
);
};
@@ -76,16 +81,16 @@ export const FinancialPreviousPeriod = (Base) =>
* @returns {IFinancialNodeWithPreviousPeriod}
*/
public assocPreviousPeriodTotalChangeNode = (
accountNode: any
accountNode: any,
): IFinancialNodeWithPreviousPeriod => {
const change = this.getAmountChange(
accountNode.total.amount,
accountNode.previousPeriod.amount
accountNode.previousPeriod.amount,
);
return R.assoc(
'previousPeriodChange',
this.getTotalAmountMeta(change),
accountNode
accountNode,
);
};
@@ -97,19 +102,19 @@ export const FinancialPreviousPeriod = (Base) =>
public assocPreviousPeriodHorizNodeFromToDates = R.curry(
(
periodUnit: IFinancialDatePeriodsUnit,
horizNode: any
horizNode: any,
): IFinancialNodeWithPreviousPeriod => {
const { fromDate: PPFromDate, toDate: PPToDate } =
this.getPreviousPeriodDateRange(
horizNode.fromDate.date,
horizNode.toDate.date,
periodUnit
periodUnit,
);
return R.compose(
R.assoc('previousPeriodToDate', this.getDateMeta(PPToDate)),
R.assoc('previousPeriodFromDate', this.getDateMeta(PPFromDate))
R.assoc('previousPeriodFromDate', this.getDateMeta(PPFromDate)),
)(horizNode);
}
},
);
/**
@@ -121,7 +126,7 @@ export const FinancialPreviousPeriod = (Base) =>
public getPPHorizNodesTotalSumation = (index: number, node): number => {
return sumBy(
node.children,
`horizontalTotals[${index}].previousPeriod.amount`
`horizontalTotals[${index}].previousPeriod.amount`,
);
};
};

View File

@@ -1,50 +1,54 @@
import * as R from 'ramda';
import { sumBy } from 'lodash'
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 = (Base) =>
class extends Base {
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 {IProfitLossSheetAccountNode} accountNode
* @returns {IProfitLossSheetAccountNode}
* @param {IFinancialCommonNode & IFinancialNodeWithPreviousYear} accountNode
* @returns {IFinancialNodeWithPreviousYear}
*/
public assocPreviousYearChangetNode = (
node: IFinancialCommonNode & IFinancialNodeWithPreviousYear
node: IFinancialCommonNode & IFinancialNodeWithPreviousYear,
): IFinancialNodeWithPreviousYear => {
const change = this.getAmountChange(
node.total.amount,
node.previousYear.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
node: IFinancialCommonNode & IFinancialNodeWithPreviousYear,
): IFinancialNodeWithPreviousYear => {
const percentage = this.getPercentageBasis(
node.previousYear.amount,
node.previousYearChange.amount
node.previousYearChange.amount,
);
return R.assoc(
'previousYearPercentage',
this.getPercentageAmountMeta(percentage),
node
node,
);
};
@@ -54,16 +58,16 @@ export const FinancialPreviousYear = (Base) =>
* @returns {IProfitLossSheetAccountNode}
*/
public assocPreviousYearTotalChangeNode = (
node: IFinancialCommonNode & IFinancialNodeWithPreviousYear
node: IFinancialCommonNode & IFinancialNodeWithPreviousYear,
): IFinancialNodeWithPreviousYear => {
const change = this.getAmountChange(
node.total.amount,
node.previousYear.amount
node.previousYear.amount,
);
return R.assoc(
'previousYearChange',
this.getTotalAmountMeta(change),
node
node,
);
};
@@ -73,16 +77,16 @@ export const FinancialPreviousYear = (Base) =>
* @returns {IProfitLossSheetAccountNode}
*/
public assocPreviousYearTotalPercentageNode = (
node: IFinancialCommonNode & IFinancialNodeWithPreviousYear
node: IFinancialCommonNode & IFinancialNodeWithPreviousYear,
): IFinancialNodeWithPreviousYear => {
const percentage = this.getPercentageBasis(
node.previousYear.amount,
node.previousYearChange.amount
node.previousYearChange.amount,
);
return R.assoc(
'previousYearPercentage',
this.getPercentageTotalAmountMeta(percentage),
node
node,
);
};
@@ -92,27 +96,27 @@ export const FinancialPreviousYear = (Base) =>
* @returns
*/
public assocPreviousYearHorizNodeFromToDates = (
horizNode: IFinancialCommonHorizDatePeriodNode
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))
R.assoc('previousYearFromDate', this.getDateMeta(PYFromDate)),
)(horizNode);
};
/**
* Retrieves PP total sumation of the given horiz index node.
* @param {number} index
* @param {} node
* 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`
)
}
`horizontalTotals[${index}].previousYear.amount`,
);
};
};

View File

@@ -1,9 +1,12 @@
import * as R from 'ramda';
import { FinancialSheetStructure } from './FinancialSheetStructure';
import { Constructor } from '@/common/types/Constructor';
import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from './FinancialSheet';
export const FinancialSchema = <T extends Constructor>(Base: T) =>
class extends R.compose(FinancialSheetStructure)(Base) {
export const FinancialSchema = <T extends GConstructor<FinancialSheet>>(
Base: T
) =>
class FinancialSchema extends R.compose(FinancialSheetStructure)(Base) {
/**
*
* @returns
@@ -17,7 +20,7 @@ export const FinancialSchema = <T extends Constructor>(Base: T) =>
* @param {string|number} id
* @returns
*/
publicgetSchemaNodeById = (id: string | number) => {
public getSchemaNodeById = (id: string | number) => {
const schema = this.getSchema();
return this.findNodeDeep(schema, (node) => node.id === id);

View File

@@ -1,20 +1,21 @@
import moment from 'moment';
import {
ICashFlowStatementTotal,
IFormatNumberSettings,
INumberFormatQuery,
} from '../types/Report.types';
import { formatNumber } from '@/utils/format-number';
import { IFinancialTableTotal } from '../types/Table.types';
export default class FinancialSheet {
readonly numberFormat: INumberFormatQuery = {
export class FinancialSheet {
public numberFormat: INumberFormatQuery = {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'total',
negativeFormat: 'mines',
};
readonly baseCurrency: string;
public baseCurrency: string;
/**
* Transformes the number format query to settings
@@ -109,7 +110,7 @@ export default class FinancialSheet {
protected getAmountMeta(
amount: number,
overrideSettings?: IFormatNumberSettings
): ICashFlowStatementTotal {
): IFinancialTableTotal {
return {
amount,
formattedAmount: this.formatNumber(amount, overrideSettings),
@@ -125,7 +126,7 @@ export default class FinancialSheet {
protected getTotalAmountMeta(
amount: number,
title?: string
): ICashFlowStatementTotal {
): IFinancialTableTotal {
return {
...(title ? { title } : {}),
amount,

View File

@@ -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 {}

View File

@@ -4,10 +4,7 @@ import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable()
export class FinancialSheetMeta {
constructor(
private readonly inventoryService: InventoryService,
private readonly tenancyContext: TenancyContext,
) {}
constructor(private readonly tenancyContext: TenancyContext) {}
/**
* Retrieves the common meta data of the financial sheet.
@@ -20,8 +17,10 @@ export class FinancialSheetMeta {
const baseCurrency = tenantMetadata.baseCurrency;
const dateFormat = tenantMetadata.dateFormat;
const isCostComputeRunning =
this.inventoryService.isItemsCostComputeRunning(tenantId);
// const isCostComputeRunning =
// this.inventoryService.isItemsCostComputeRunning();
const isCostComputeRunning = false;
return {
organizationName,

View File

@@ -4,16 +4,20 @@ import {
mapValuesDeepReverse,
mapValuesDeep,
mapValues,
condense,
filterDeep,
reduceDeep,
findValueDeep,
filterNodesDeep,
} from 'utils/deepdash';
import { Constructor } from '@/common/types/Constructor';
} from '@/utils/deepdash';
import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from './FinancialSheet';
export const FinancialSheetStructure = <T extends Constructor>(Base: T) =>
class extends Base {
export const FinancialSheetStructure = <
T extends GConstructor<FinancialSheet>,
>(
Base: T
) =>
class FinancialSheetStructure extends Base {
/**
*
* @param nodes

View File

@@ -2,10 +2,21 @@ import * as R from 'ramda';
import { isEmpty, clone, cloneDeep, omit } from 'lodash';
import { increment } from '@/utils/increment';
import { ITableRow, ITableColumn } from '../types/Table.types';
import { IROW_TYPE } from './BalanceSheet/constants';
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;
export const FinancialTable = (Base) =>
class extends Base {
/**
* Table columns cell indexing.
* @param {ITableColumn[]} columns
@@ -23,13 +34,15 @@ export const FinancialTable = (Base) =>
});
};
addTotalRow = (node: ITableRow) => {
public addTotalRow = (node: ITableRow) => {
const clonedNode = clone(node);
if (clonedNode.children) {
const cells = cloneDeep(node.cells);
cells[0].value = this.i18n.__('financial_sheet.total_row', {
value: cells[0].value,
cells[0].value = this.i18n.t('financial_sheet.total_row', {
args: {
value: cells[0].value,
},
});
clonedNode.children.push({

View File

@@ -1,8 +1,18 @@
import moment from 'moment';
import { ITableColumn, IDateRange, ITableColumnAccessor } from '../types/Table.types';
import { ITableColumn, ITableColumnAccessor } from '../types/Table.types';
import { IDateRange } from '../types/Report.types';
import { Constructor, GConstructor } from '@/common/types/Constructor';
import { I18nService } from 'nestjs-i18n';
import { FinancialSheet } from './FinancialSheet';
export const FinancialTablePreviousPeriod = (Base) =>
export const FinancialTablePreviousPeriod = <
T extends GConstructor<FinancialSheet>,
>(
Base: T,
) =>
class extends Base {
public readonly i18n: I18nService;
getTotalPreviousPeriod = () => {
return this.query.PPToDate;
};
@@ -24,8 +34,8 @@ export const FinancialTablePreviousPeriod = (Base) =>
return {
key: 'previous_period',
label: this.i18n.__(`financial_sheet.previoud_period_date`, {
date: PPFormatted,
label: this.i18n.t(`financial_sheet.previoud_period_date`, {
args: { date: PPFormatted, }
}),
};
};
@@ -37,7 +47,7 @@ export const FinancialTablePreviousPeriod = (Base) =>
public getPreviousPeriodChangeColumn = (): ITableColumn => {
return {
key: 'previous_period_change',
label: this.i18n.__('fianncial_sheet.previous_period_change'),
label: this.i18n.t('fianncial_sheet.previous_period_change'),
};
};
@@ -48,7 +58,7 @@ export const FinancialTablePreviousPeriod = (Base) =>
public getPreviousPeriodPercentageColumn = (): ITableColumn => {
return {
key: 'previous_period_percentage',
label: this.i18n.__('financial_sheet.previous_period_percentage'),
label: this.i18n.t('financial_sheet.previous_period_percentage'),
};
};

View File

@@ -1,18 +1,32 @@
import 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 = (Base) =>
export const FinancialTablePreviousYear = <
T extends GConstructor<FinancialSheet>,
>(
Base: T,
) =>
class extends Base {
getTotalPreviousYear = () => {
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 -
* @param {DateRange} previousYear -
* @returns {ITableColumn}
*/
public getPreviousYearTotalColumn = (
@@ -23,8 +37,8 @@ export const FinancialTablePreviousYear = (Base) =>
return {
key: 'previous_year',
label: this.i18n.__('financial_sheet.previous_year_date', {
date: PYFormatted,
label: this.i18n.t('financial_sheet.previous_year_date', {
args: { date: PYFormatted },
}),
};
};
@@ -36,7 +50,7 @@ export const FinancialTablePreviousYear = (Base) =>
public getPreviousYearChangeColumn = (): ITableColumn => {
return {
key: 'previous_year_change',
label: this.i18n.__('financial_sheet.previous_year_change'),
label: this.i18n.t('financial_sheet.previous_year_change'),
};
};
@@ -47,7 +61,7 @@ export const FinancialTablePreviousYear = (Base) =>
public getPreviousYearPercentageColumn = (): ITableColumn => {
return {
key: 'previous_year_percentage',
label: this.i18n.__('financial_sheet.previous_year_percentage'),
label: this.i18n.t('financial_sheet.previous_year_percentage'),
};
};
@@ -89,7 +103,7 @@ export const FinancialTablePreviousYear = (Base) =>
/**
* Retrieves previous year total horizontal column accessor.
* @param {number} index
* @param {number} index - Index.
* @returns {ITableColumnAccessor}
*/
public getPreviousYearTotalHorizAccessor = (
@@ -103,7 +117,7 @@ export const FinancialTablePreviousYear = (Base) =>
/**
* Retrieves previous previous year change horizontal column accessor.
* @param {number} index
* @param {number} index
* @returns {ITableColumnAccessor}
*/
public getPreviousYearChangeHorizAccessor = (
@@ -117,7 +131,7 @@ export const FinancialTablePreviousYear = (Base) =>
/**
* Retrieves previous year percentage horizontal column accessor.
* @param {number} index
* @param {number} index
* @returns {ITableColumnAccessor}
*/
public getPreviousYearPercentageHorizAccessor = (

View File

@@ -1,13 +1,17 @@
import * as R from 'ramda';
import { ITableColumn, ITableData, ITableRow } from './types/Table.types';
import { FinancialTableStructure } from './common/FinancialTableStructure';
import { tableClassNames } from './utils';
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';
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,
@@ -60,8 +64,8 @@ export class TableSheetPdf {
/**
* Converts the table rows to pdf rows.
* @param {ITableRow[]} rows -
* @returns {ITableRow[]}
* @param {ITableRow[]} rows - The table rows to be converted.
* @returns {ITableRow[]} - The converted table rows.
*/
private tablePdfRows = (rows: ITableRow[]): ITableRow[] => {
const curriedFlatNestedTree = R.curry(
@@ -70,6 +74,8 @@ export class TableSheetPdf {
const flatNestedTree = curriedFlatNestedTree(R.__, {
nestedPrefix: '<span style="padding-left: 15px;"></span>',
});
// @ts-ignore
return R.compose(tableClassNames, flatNestedTree)(rows);
};
}

View File

@@ -1,9 +0,0 @@
import { Module } from '@nestjs/common';
import { BalanceSheetInjectable } from './BalanceSheetInjectable';
import { BalanceSheetApplication } from './BalanceSheetApplication';
@Module({
providers: [BalanceSheetInjectable, BalanceSheetApplication],
exports: [BalanceSheetInjectable],
})
export class BalanceSheetModule {}

View File

@@ -1,108 +0,0 @@
import * as R from 'ramda';
import {
IBalanceSheetQuery,
IBalanceSheetSchemaNode,
IBalanceSheetDataNode,
} from './BalanceSheet.types';
import { BalanceSheetSchema } from './BalanceSheetSchema';
import { BalanceSheetPercentage } from './BalanceSheetPercentage';
import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod';
import { BalanceSheetComparsionPreviousYear } from './BalanceSheetComparsionPreviousYear';
import { BalanceSheetDatePeriods } from './BalanceSheetDatePeriods';
import { BalanceSheetBase } from './BalanceSheetBase';
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
import BalanceSheetRepository from './BalanceSheetRepository';
import { BalanceSheetQuery } from './BalanceSheetQuery';
import { BalanceSheetFiltering } from './BalanceSheetFiltering';
import { BalanceSheetNetIncome } from './BalanceSheetNetIncome';
import { BalanceSheetAggregators } from './BalanceSheetAggregators';
import { BalanceSheetAccounts } from './BalanceSheetAccounts';
import FinancialSheet from '../../common/FinancialSheet';
import { INumberFormatQuery } from '../../types/Report.types';
export class BalanceSheet extends R.compose(
BalanceSheetAggregators,
BalanceSheetAccounts,
BalanceSheetNetIncome,
BalanceSheetFiltering,
BalanceSheetDatePeriods,
BalanceSheetComparsionPreviousPeriod,
BalanceSheetComparsionPreviousYear,
BalanceSheetPercentage,
BalanceSheetSchema,
BalanceSheetBase,
FinancialSheetStructure,
)(FinancialSheet) {
/**
* Balance sheet query.
* @param {BalanceSheetQuery}
*/
readonly query: BalanceSheetQuery;
/**
* Balance sheet number format query.
* @param {INumberFormatQuery}
*/
readonly numberFormat: INumberFormatQuery;
/**
* Base currency of the organization.
* @param {string}
*/
readonly baseCurrency: string;
/**
* Localization.
*/
readonly i18n: any;
/**
* Constructor method.
* @param {IBalanceSheetQuery} query -
* @param {IAccount[]} accounts -
* @param {string} baseCurrency -
*/
constructor(
query: IBalanceSheetQuery,
repository: BalanceSheetRepository,
baseCurrency: string,
i18n,
) {
super();
this.query = new BalanceSheetQuery(query);
this.repository = repository;
this.baseCurrency = baseCurrency;
this.numberFormat = this.query.query.numberFormat;
this.i18n = i18n;
}
/**
* Parses report schema nodes.
* @param {IBalanceSheetSchemaNode[]} schema
* @returns {IBalanceSheetDataNode[]}
*/
public parseSchemaNodes = (
schema: IBalanceSheetSchemaNode[],
): IBalanceSheetDataNode[] => {
return R.compose(
this.aggregatesSchemaParser,
this.netIncomeSchemaParser,
this.accountsSchemaParser,
)(schema) as IBalanceSheetDataNode[];
};
/**
* Retrieve the report statement data.
* @returns {IBalanceSheetDataNode[]}
*/
public reportData = () => {
const balanceSheetSchema = this.getSchema();
return R.compose(
this.reportFilterPlugin,
this.reportPercentageCompose,
this.parseSchemaNodes,
)(balanceSheetSchema);
};
}

View File

@@ -1,223 +0,0 @@
import {
IFinancialSheetBranchesQuery,
IFinancialSheetCommonMeta,
IFormatNumberSettings,
INumberFormatQuery,
} from '../../types/Report.types';
import { IFinancialTable } from '../../types/Table.types';
// Balance sheet schema nodes types.
export enum BALANCE_SHEET_SCHEMA_NODE_TYPE {
AGGREGATE = 'AGGREGATE',
ACCOUNTS = 'ACCOUNTS',
ACCOUNT = 'ACCOUNT',
NET_INCOME = 'NET_INCOME',
}
export enum BALANCE_SHEET_NODE_TYPE {
AGGREGATE = 'AGGREGATE',
ACCOUNTS = 'ACCOUNTS',
ACCOUNT = 'ACCOUNT',
}
// Balance sheet schema nodes ids.
export enum BALANCE_SHEET_SCHEMA_NODE_ID {
ASSETS = 'ASSETS',
CURRENT_ASSETS = 'CURRENT_ASSETS',
CASH_EQUIVALENTS = 'CASH_EQUIVALENTS',
ACCOUNTS_RECEIVABLE = 'ACCOUNTS_RECEIVABLE',
NON_CURRENT_ASSET = 'NON_CURRENT_ASSET',
FIXED_ASSET = 'FIXED_ASSET',
OTHER_CURRENT_ASSET = 'OTHER_CURRENT_ASSET',
INVENTORY = 'INVENTORY',
LIABILITY_EQUITY = 'LIABILITY_EQUITY',
LIABILITY = 'LIABILITY',
CURRENT_LIABILITY = 'CURRENT_LIABILITY',
LOGN_TERM_LIABILITY = 'LOGN_TERM_LIABILITY',
NON_CURRENT_LIABILITY = 'NON_CURRENT_LIABILITY',
EQUITY = 'EQUITY',
NET_INCOME = 'NET_INCOME',
}
// Balance sheet query.
export interface IBalanceSheetQuery extends IFinancialSheetBranchesQuery {
displayColumnsType: 'total' | 'date_periods';
displayColumnsBy: string;
fromDate: Date;
toDate: Date;
numberFormat: INumberFormatQuery;
noneTransactions: boolean;
noneZero: boolean;
basis: 'cash' | 'accrual';
accountIds: number[];
percentageOfColumn: boolean;
percentageOfRow: boolean;
previousPeriod: boolean;
previousPeriodAmountChange: boolean;
previousPeriodPercentageChange: boolean;
previousYear: boolean;
previousYearAmountChange: boolean;
previousYearPercentageChange: boolean;
}
// Balance sheet meta.
export interface IBalanceSheetMeta extends IFinancialSheetCommonMeta {
formattedAsDate: string;
formattedDateRange: string;
}
export interface IBalanceSheetFormatNumberSettings
extends IFormatNumberSettings {
type: string;
}
// Balance sheet service.
export interface IBalanceSheetStatementService {
balanceSheet(
tenantId: number,
query: IBalanceSheetQuery,
): Promise<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;
}

View File

@@ -1,205 +0,0 @@
import * as R from 'ramda';
import { defaultTo, toArray } from 'lodash';
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
import {
BALANCE_SHEET_SCHEMA_NODE_TYPE,
IBalanceSheetAccountNode,
IBalanceSheetAccountsNode,
IBalanceSheetDataNode,
IBalanceSheetSchemaAccountNode,
IBalanceSheetSchemaNode,
} from './BalanceSheet.types';
import { BalanceSheetNetIncome } from './BalanceSheetNetIncome';
import { BalanceSheetFiltering } from './BalanceSheetFiltering';
import { BalanceSheetDatePeriods } from './BalanceSheetDatePeriods';
import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod';
import { BalanceSheetComparsionPreviousYear } from './BalanceSheetComparsionPreviousYear';
import { BalanceSheetPercentage } from './BalanceSheetPercentage';
import { BalanceSheetSchema } from './BalanceSheetSchema';
import { BalanceSheetBase } from './BalanceSheetBase';
import { BalanceSheetQuery } from './BalanceSheetQuery';
import { flatToNestedArray } from '@/utils';
import BalanceSheetRepository from './BalanceSheetRepository';
import { Constructor } from '@/common/types/Constructor';
import { INumberFormatQuery } from '../../types/Report.types';
import { Account } from '@/modules/Accounts/models/Account.model';
export const BalanceSheetAccounts = <T extends Constructor>(Base: T) =>
class extends R.compose(
BalanceSheetNetIncome,
BalanceSheetFiltering,
BalanceSheetDatePeriods,
BalanceSheetComparsionPreviousPeriod,
BalanceSheetComparsionPreviousYear,
BalanceSheetPercentage,
BalanceSheetSchema,
BalanceSheetBase,
FinancialSheetStructure
)(Base) {
/**
* Balance sheet query.
* @param {BalanceSheetQuery}
*/
readonly query: BalanceSheetQuery;
/**
* Balance sheet number format query.
* @param {INumberFormatQuery}
*/
readonly numberFormat: INumberFormatQuery;
/**
* Base currency of the organization.
* @param {string}
*/
readonly baseCurrency: string;
/**
* Localization.
*/
readonly i18n: any;
/**
* Balance sheet repository.
*/
readonly repository: BalanceSheetRepository;
/**
* Retrieve the accounts node of accounts types.
* @param {string} accountsTypes
* @returns {IAccount[]}
*/
private getAccountsByAccountTypes = (
accountsTypes: string[]
): Account[] => {
const mapAccountsByTypes = R.map((accountType) =>
defaultTo(this.repository.accountsByType.get(accountType), [])
);
return R.compose(R.flatten, mapAccountsByTypes)(accountsTypes);
};
/**
* Mappes the account model to report account node.
* @param {Account} account
* @returns {IBalanceSheetAccountNode}
*/
private reportSchemaAccountNodeMapper = (
account: Account
): IBalanceSheetAccountNode => {
const childrenAccountsIds = this.repository.accountsGraph.dependenciesOf(
account.id
);
const accountIds = R.uniq(R.append(account.id, childrenAccountsIds));
const total = this.repository.totalAccountsLedger
.whereAccountsIds(accountIds)
.getClosingBalance();
return {
id: account.id,
index: account.index,
name: account.name,
code: account.code,
total: this.getAmountMeta(total),
nodeType: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNT,
};
};
/**
* Mappes the given account model to the balance sheet account node.
* @param {IAccount} account
* @returns {IBalanceSheetAccountNode}
*/
private reportSchemaAccountNodeComposer = (
account: Account
): IBalanceSheetAccountNode => {
return R.compose(
R.when(
this.query.isPreviousYearActive,
this.previousYearAccountNodeComposer
),
R.when(
this.query.isPreviousPeriodActive,
this.previousPeriodAccountNodeComposer
),
R.when(
this.query.isDatePeriodsColumnsType,
this.assocAccountNodeDatePeriods
),
this.reportSchemaAccountNodeMapper
)(account);
};
// -----------------------------
// - Accounts Node Praser
// -----------------------------
/**
* Retrieve the report accounts node by the given accounts types.
* @param {string[]} accountsTypes
* @returns {IBalanceSheetAccountNode[]}
*/
private getAccountsNodesByAccountTypes = (
accountsTypes: string[]
): IBalanceSheetAccountNode[] => {
// Retrieves accounts from the given defined node account types.
const accounts = this.getAccountsByAccountTypes(accountsTypes);
// Converts the flatten accounts to tree.
const accountsTree = flatToNestedArray(accounts, {
id: 'id',
parentId: 'parentAccountId',
});
// Maps over the accounts tree.
return this.mapNodesDeep(
accountsTree,
this.reportSchemaAccountNodeComposer
);
};
/**
* Mappes the accounts schema node type.
* @param {IBalanceSheetSchemaNode} node - Schema node.
* @returns {IBalanceSheetAccountNode}
*/
private reportSchemaAccountsNodeMapper = (
node: IBalanceSheetSchemaAccountNode
): IBalanceSheetAccountsNode => {
const accounts = this.getAccountsNodesByAccountTypes(node.accountsTypes);
const children = toArray(node?.children);
return {
id: node.id,
name: this.i18n.__(node.name),
nodeType: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS,
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS,
children: [...accounts, ...children],
total: this.getTotalAmountMeta(0),
};
};
/**
* Mappes the given report schema node.
* @param {IBalanceSheetSchemaNode | IBalanceSheetDataNode} node - Schema node.
* @return {IBalanceSheetSchemaNode | IBalanceSheetDataNode}
*/
private reportAccountSchemaParser = (
node: IBalanceSheetSchemaNode | IBalanceSheetDataNode
): IBalanceSheetSchemaNode | IBalanceSheetDataNode => {
return R.compose(
R.when(
this.isSchemaNodeType(BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS),
this.reportSchemaAccountsNodeMapper
)
)(node);
};
/**
* Parses the report accounts schema nodes.
* @param {IBalanceSheetSchemaNode[]} nodes -
* @return {IBalanceSheetStructureSection[]}
*/
public accountsSchemaParser = (
nodes: (IBalanceSheetSchemaNode | IBalanceSheetDataNode)[]
): (IBalanceSheetDataNode | IBalanceSheetSchemaNode)[] => {
return this.mapNodesDeepReverse(nodes, this.reportAccountSchemaParser);
};
};

View File

@@ -1,141 +0,0 @@
import * as R from 'ramda';
import {
BALANCE_SHEET_SCHEMA_NODE_TYPE,
IBalanceSheetAggregateNode,
IBalanceSheetDataNode,
IBalanceSheetSchemaAggregateNode,
IBalanceSheetSchemaNode,
INumberFormatQuery,
} from '@/interfaces';
import { BalanceSheetDatePeriods } from './BalanceSheetDatePeriods';
import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod';
import { BalanceSheetComparsionPreviousYear } from './BalanceSheetComparsionPreviousYear';
import { BalanceSheetPercentage } from './BalanceSheetPercentage';
import { BalanceSheetSchema } from './BalanceSheetSchema';
import { BalanceSheetBase } from './BalanceSheetBase';
import { BalanceSheetQuery } from './BalanceSheetQuery';
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
import { Constructor } from '@/common/types/Constructor';
export const BalanceSheetAggregators = <T extends Constructor>(Base: T) =>
class extends R.compose(
BalanceSheetDatePeriods,
BalanceSheetComparsionPreviousPeriod,
BalanceSheetComparsionPreviousYear,
BalanceSheetPercentage,
BalanceSheetSchema,
BalanceSheetBase,
FinancialSheetStructure
)(Base) {
/**
* Balance sheet query.
* @param {BalanceSheetQuery}
*/
readonly query: BalanceSheetQuery;
/**
* Balance sheet number format query.
* @param {INumberFormatQuery}
*/
readonly numberFormat: INumberFormatQuery;
/**
* Base currency of the organization.
* @param {string}
*/
readonly baseCurrency: string;
/**
* Localization.
*/
readonly i18n: any;
/**
* Sets total amount that calculated from node children.
* @param {IBalanceSheetSection} node
* @returns {IBalanceSheetDataNode}
*/
public aggregateNodeTotalMapper = (
node: IBalanceSheetDataNode
): IBalanceSheetDataNode => {
return R.compose(
R.when(
this.query.isPreviousYearActive,
this.previousYearAggregateNodeComposer
),
R.when(
this.query.isPreviousPeriodActive,
this.previousPeriodAggregateNodeComposer
),
R.when(
this.query.isDatePeriodsColumnsType,
this.assocAggregateNodeDatePeriods
)
)(node);
};
/**
* Mappes the aggregate schema node type.
* @param {IBalanceSheetSchemaAggregateNode} node - Schema node.
* @return {IBalanceSheetAggregateNode}
*/
public reportSchemaAggregateNodeMapper = (
node: IBalanceSheetSchemaAggregateNode
): IBalanceSheetAggregateNode => {
const total = this.getTotalOfNodes(node.children);
return {
name: this.i18n.__(node.name),
id: node.id,
nodeType: BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE,
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE,
total: this.getTotalAmountMeta(total),
children: node.children,
};
};
/**
* Compose shema aggregate node of balance sheet schema.
* @param {IBalanceSheetSchemaAggregateNode} node
* @returns {IBalanceSheetSchemaAggregateNode}
*/
public schemaAggregateNodeCompose = (
node: IBalanceSheetSchemaAggregateNode
) => {
return R.compose(
this.aggregateNodeTotalMapper,
this.reportSchemaAggregateNodeMapper
)(node);
};
/**
* Mappes the given report schema node.
* @param {IBalanceSheetSchemaNode} node - Schema node.
* @return {IBalanceSheetDataNode}
*/
public reportAggregateSchemaParser = (
node: IBalanceSheetSchemaNode
): IBalanceSheetDataNode => {
return R.compose(
R.when(
this.isSchemaNodeType(BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE),
this.schemaAggregateNodeCompose
),
R.when(
this.isSchemaNodeType(BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS),
this.schemaAggregateNodeCompose
)
)(node);
};
/**
* Mappes the report schema nodes.
* @param {IBalanceSheetSchemaNode[]} nodes -
* @return {IBalanceSheetStructureSection[]}
*/
public aggregatesSchemaParser = (
nodes: (IBalanceSheetSchemaNode | IBalanceSheetDataNode)[]
): (IBalanceSheetDataNode | IBalanceSheetSchemaNode)[] => {
return this.mapNodesDeepReverse(nodes, this.reportAggregateSchemaParser);
};
};

View File

@@ -1,60 +0,0 @@
import { Injectable } from '@nestjs/common';
import { IBalanceSheetQuery } from '@/interfaces';
import { BalanceSheetExportInjectable } from './BalanceSheetExportInjectable';
import { BalanceSheetTableInjectable } from './BalanceSheetTableInjectable';
import { BalanceSheetStatementService } from './BalanceSheetInjectable';
@Injectable()
export class BalanceSheetApplication {
constructor(
private readonly balanceSheetExportService: BalanceSheetExportInjectable,
private readonly balanceSheetTableService: BalanceSheetTableInjectable,
private readonly balanceSheetService: BalanceSheetStatementService,
) {}
/**
* Retrieves the balnace sheet in json format.
* @param {IBalanceSheetQuery} query
* @returns {Promise<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);
}
}

View File

@@ -1,33 +0,0 @@
import * as R from 'ramda';
import { IBalanceSheetDataNode, IBalanceSheetSchemaNode } from '@/interfaces';
import { Constructor } from '@/common/types/Constructor';
export const BalanceSheetBase = <T extends Constructor>(Base: T) =>
class extends Base {
/**
* Detarmines the node type of the given schema node.
* @param {IBalanceSheetStructureSection} node -
* @param {string} type -
* @return {boolean}
*/
public isSchemaNodeType = R.curry(
(type: string, node: IBalanceSheetSchemaNode): boolean => {
return node.type === type;
}
);
isNodeType = R.curry(
(type: string, node: IBalanceSheetDataNode): boolean => {
return node.nodeType === type;
}
);
/**
* Detarmines the given display columns by type.
* @param {string} displayColumnsBy
* @returns {boolean}
*/
public isDisplayColumnsBy = (displayColumnsBy: string): boolean => {
return this.query.displayColumnsType === displayColumnsBy;
};
};

View File

@@ -1,271 +0,0 @@
import * as R from 'ramda';
import { sumBy } from 'lodash';
import {
IBalanceSheetAccountNode,
IBalanceSheetDataNode,
IBalanceSheetAggregateNode,
IBalanceSheetTotal,
IBalanceSheetCommonNode,
IBalanceSheetComparsions,
} from '@/interfaces';
import { FinancialPreviousPeriod } from '../../common/FinancialPreviousPeriod';
import { FinancialHorizTotals } from '../../common/FinancialHorizTotals';
import { Constructor } from '@/common/types/Constructor';
export const BalanceSheetComparsionPreviousPeriod = <T extends Constructor>(
Base: T,
) =>
class
extends R.compose(FinancialPreviousPeriod, FinancialHorizTotals)(Base)
implements IBalanceSheetComparsions
{
// ------------------------------
// # Account
// ------------------------------
/**
* Associates the previous period to account node.
* @param {IBalanceSheetDataNode} node
* @returns {IBalanceSheetDataNode}
*/
public assocPreviousPeriodAccountNode = (
node: IBalanceSheetDataNode,
): IBalanceSheetDataNode => {
const total = this.repository.PPTotalAccountsLedger.whereAccountId(
node.id,
).getClosingBalance();
return R.assoc('previousPeriod', this.getAmountMeta(total), node);
};
/**
* Previous period account node composer.
* @param {IBalanceSheetAccountNode} node
* @returns {IBalanceSheetAccountNode}
*/
public previousPeriodAccountNodeComposer = (
node: IBalanceSheetAccountNode,
): IBalanceSheetAccountNode => {
return R.compose(
R.when(
this.isNodeHasHorizTotals,
this.assocPreivousPeriodAccountHorizNodeComposer,
),
R.when(
this.query.isPreviousPeriodPercentageActive,
this.assocPreviousPeriodPercentageNode,
),
R.when(
this.query.isPreviousPeriodChangeActive,
this.assocPreviousPeriodChangeNode,
),
R.when(
this.query.isPreviousPeriodActive,
this.assocPreviousPeriodAccountNode,
),
)(node);
};
// ------------------------------
// # Aggregate
// ------------------------------
/**
* Assoc previous period total to aggregate node.
* @param {IBalanceSheetAggregateNode} node
* @returns {IBalanceSheetAggregateNode}
*/
public assocPreviousPeriodAggregateNode = (
node: IBalanceSheetAggregateNode,
): IBalanceSheetAggregateNode => {
const total = sumBy(node.children, 'previousYear.amount');
return R.assoc('previousPeriod', this.getTotalAmountMeta(total), node);
};
/**
* Previous period aggregate node composer.
* @param {IBalanceSheetAccountNode} node
* @returns {IBalanceSheetAccountNode}
*/
public previousPeriodAggregateNodeComposer = (
node: IBalanceSheetAccountNode,
): IBalanceSheetAccountNode => {
return R.compose(
R.when(
this.isNodeHasHorizTotals,
this.assocPreviousPeriodAggregateHorizNode,
),
R.when(
this.query.isPreviousPeriodPercentageActive,
this.assocPreviousPeriodTotalPercentageNode,
),
R.when(
this.query.isPreviousPeriodChangeActive,
this.assocPreviousPeriodTotalChangeNode,
),
R.when(
this.query.isPreviousPeriodActive,
this.assocPreviousPeriodAggregateNode,
),
)(node);
};
// ------------------------------
// # Horizontal Nodes - Account.
// ------------------------------
/**
* Retrieve the given account total in the given period.
* @param {number} accountId - Account id.
* @param {Date} fromDate - From date.
* @param {Date} toDate - To date.
* @returns {number}
*/
private getAccountPPDatePeriodTotal = R.curry(
(accountId: number, fromDate: Date, toDate: Date): number => {
const PPPeriodsTotal =
this.repository.PPPeriodsAccountsLedger.whereAccountId(accountId)
.whereToDate(toDate)
.getClosingBalance();
const PPPeriodsOpeningTotal =
this.repository.PPPeriodsOpeningAccountLedger.whereAccountId(
accountId,
).getClosingBalance();
return PPPeriodsOpeningTotal + PPPeriodsTotal;
},
);
/**
* Assoc preivous period to account horizontal total node.
* @param {IBalanceSheetAccountNode} node
* @returns {}
*/
private assocPreviousPeriodAccountHorizTotal = R.curry(
(node: IBalanceSheetAccountNode, totalNode) => {
const total = this.getAccountPPDatePeriodTotal(
node.id,
totalNode.previousPeriodFromDate.date,
totalNode.previousPeriodToDate.date,
);
return R.assoc('previousPeriod', this.getAmountMeta(total), totalNode);
},
);
/**
* Previous year account horizontal node composer.
* @param {IBalanceSheetAccountNode} node -
* @param {IBalanceSheetTotal}
* @returns {IBalanceSheetTotal}
*/
private previousPeriodAccountHorizNodeCompose = R.curry(
(
node: IBalanceSheetAccountNode,
horizontalTotalNode: IBalanceSheetTotal,
): IBalanceSheetTotal => {
return R.compose(
R.when(
this.query.isPreviousPeriodPercentageActive,
this.assocPreviousPeriodPercentageNode,
),
R.when(
this.query.isPreviousPeriodChangeActive,
this.assocPreviousPeriodChangeNode,
),
R.when(
this.query.isPreviousPeriodActive,
this.assocPreviousPeriodAccountHorizTotal(node),
),
R.when(
this.query.isPreviousPeriodActive,
this.assocPreviousPeriodHorizNodeFromToDates(
this.query.displayColumnsBy,
),
),
)(horizontalTotalNode);
},
);
/**
*
* @param {IBalanceSheetAccountNode} node
* @returns
*/
private assocPreivousPeriodAccountHorizNodeComposer = (
node: IBalanceSheetAccountNode,
) => {
const horizontalTotals = R.map(
this.previousPeriodAccountHorizNodeCompose(node),
node.horizontalTotals,
);
return R.assoc('horizontalTotals', horizontalTotals, node);
};
// ------------------------------
// # Horizontal Nodes - Aggregate
// ------------------------------
/**
* Assoc previous year total to horizontal node.
* @param node
* @returns
*/
private assocPreviousPeriodAggregateHorizTotalNode = R.curry(
(node, index: number, totalNode) => {
const total = this.getPPHorizNodesTotalSumation(index, node);
return R.assoc(
'previousPeriod',
this.getTotalAmountMeta(total),
totalNode,
);
},
);
/**
* Compose previous period to aggregate horizontal nodes.
* @param {IBalanceSheetTotal} node
* @returns {IBalanceSheetTotal}
*/
private previousPeriodAggregateHorizNodeComposer = R.curry(
(
node: IBalanceSheetCommonNode,
horiontalTotalNode: IBalanceSheetTotal,
index: number,
): IBalanceSheetTotal => {
return R.compose(
R.when(
this.query.isPreviousPeriodPercentageActive,
this.assocPreviousPeriodTotalPercentageNode,
),
R.when(
this.query.isPreviousPeriodChangeActive,
this.assocPreviousPeriodTotalChangeNode,
),
R.when(
this.query.isPreviousPeriodActive,
this.assocPreviousPeriodAggregateHorizTotalNode(node, index),
),
R.when(
this.query.isPreviousPeriodActive,
this.assocPreviousPeriodHorizNodeFromToDates(
this.query.displayColumnsBy,
),
),
)(horiontalTotalNode);
},
);
/**
* Assoc
* @param {IBalanceSheetCommonNode} node
* @returns {IBalanceSheetCommonNode}
*/
private assocPreviousPeriodAggregateHorizNode = (
node: IBalanceSheetCommonNode,
) => {
const horizontalTotals = R.addIndex(R.map)(
this.previousPeriodAggregateHorizNodeComposer(node),
node.horizontalTotals,
);
return R.assoc('horizontalTotals', horizontalTotals, node);
};
};

View File

@@ -1,269 +0,0 @@
import * as R from 'ramda';
import { sumBy, isEmpty } from 'lodash';
import {
IBalanceSheetAccountNode,
IBalanceSheetCommonNode,
IBalanceSheetDataNode,
IBalanceSheetTotal,
ITableColumn,
} from '@/interfaces';
import { FinancialPreviousYear } from '../FinancialPreviousYear';
export const BalanceSheetComparsionPreviousYear = (Base: any) =>
class
extends R.compose(FinancialPreviousYear)(Base)
implements IBalanceSheetComparsions
{
// ------------------------------
// # Account
// ------------------------------
/**
* Associates the previous year to account node.
* @param {IBalanceSheetDataNode} node
* @returns {IBalanceSheetDataNode}
*/
protected assocPreviousYearAccountNode = (
node: IBalanceSheetDataNode
): IBalanceSheetDataNode => {
const closingBalance =
this.repository.PYTotalAccountsLedger.whereAccountId(
node.id
).getClosingBalance();
return R.assoc('previousYear', this.getAmountMeta(closingBalance), node);
};
/**
* Assoc previous year attributes to account node.
* @param {IBalanceSheetAccountNode} node
* @returns {IBalanceSheetAccountNode}
*/
protected previousYearAccountNodeComposer = (
node: IBalanceSheetAccountNode
): IBalanceSheetAccountNode => {
return R.compose(
R.when(
this.isNodeHasHorizontalTotals,
this.assocPreviousYearAccountHorizNodeComposer
),
R.when(
this.query.isPreviousYearPercentageActive,
this.assocPreviousYearPercentageNode
),
R.when(
this.query.isPreviousYearChangeActive,
this.assocPreviousYearChangetNode
),
this.assocPreviousYearAccountNode
)(node);
};
// ------------------------------
// # Aggregate
// ------------------------------
/**
* Assoc previous year on aggregate node.
* @param {IBalanceSheetAccountNode} node
* @returns {IBalanceSheetAccountNode}
*/
protected assocPreviousYearAggregateNode = (
node: IBalanceSheetAccountNode
): IBalanceSheetAccountNode => {
const total = sumBy(node.children, 'previousYear.amount');
return R.assoc('previousYear', this.getTotalAmountMeta(total), node);
};
/**
* Assoc previous year attributes to aggregate node.
* @param {IBalanceSheetAccountNode} node
* @returns {IBalanceSheetAccountNode}
*/
protected previousYearAggregateNodeComposer = (
node: IBalanceSheetAccountNode
): IBalanceSheetAccountNode => {
return R.compose(
R.when(
this.query.isPreviousYearPercentageActive,
this.assocPreviousYearTotalPercentageNode
),
R.when(
this.query.isPreviousYearChangeActive,
this.assocPreviousYearTotalChangeNode
),
R.when(
this.isNodeHasHorizontalTotals,
this.assocPreviousYearAggregateHorizNode
),
this.assocPreviousYearAggregateNode
)(node);
};
// ------------------------------
// # Horizontal Nodes - Aggregate
// ------------------------------
/**
* Assoc previous year total to horizontal node.
* @param node
* @returns
*/
private assocPreviousYearAggregateHorizTotalNode = R.curry(
(node, index, totalNode) => {
const total = this.getPYHorizNodesTotalSumation(index, node);
return R.assoc(
'previousYear',
this.getTotalAmountMeta(total),
totalNode
);
}
);
/**
* Compose previous year to aggregate horizontal nodes.
* @param {IBalanceSheetTotal} node
* @returns {IBalanceSheetTotal}
*/
private previousYearAggregateHorizNodeComposer = R.curry(
(
node: IBalanceSheetCommonNode,
horiontalTotalNode: IBalanceSheetTotal,
index: number
): IBalanceSheetTotal => {
return R.compose(
R.when(
this.query.isPreviousYearPercentageActive,
this.assocPreviousYearTotalPercentageNode
),
R.when(
this.query.isPreviousYearChangeActive,
this.assocPreviousYearTotalChangeNode
),
R.when(
this.query.isPreviousYearActive,
this.assocPreviousYearAggregateHorizTotalNode(node, index)
),
R.when(
this.query.isPreviousYearActive,
this.assocPreviousYearHorizNodeFromToDates
)
)(horiontalTotalNode);
}
);
/**
* Assoc
* @param {IBalanceSheetCommonNode} node
* @returns {IBalanceSheetCommonNode}
*/
public assocPreviousYearAggregateHorizNode = (
node: IBalanceSheetCommonNode
): IBalanceSheetCommonNode => {
const horizontalTotals = R.addIndex(R.map)(
this.previousYearAggregateHorizNodeComposer(node),
node.horizontalTotals
) as IBalanceSheetTotal[];
return R.assoc('horizontalTotals', horizontalTotals, node);
};
// ------------------------------
// # Horizontal Nodes - Account.
// ------------------------------
/**
* Retrieve the given account total in the given period.
* @param {number} accountId - Account id.
* @param {Date} fromDate - From date.
* @param {Date} toDate - To date.
* @returns {number}
*/
private getAccountPYDatePeriodTotal = R.curry(
(accountId: number, fromDate: Date, toDate: Date): number => {
const PYPeriodsTotal =
this.repository.PYPeriodsAccountsLedger.whereAccountId(accountId)
.whereToDate(toDate)
.getClosingBalance();
const PYPeriodsOpeningTotal =
this.repository.PYPeriodsOpeningAccountLedger.whereAccountId(
accountId
).getClosingBalance();
return PYPeriodsOpeningTotal + PYPeriodsTotal;
}
);
/**
* Assoc preivous year to account horizontal total node.
* @param {IBalanceSheetAccountNode} node
* @returns {}
*/
private assocPreviousYearAccountHorizTotal = R.curry(
(node: IBalanceSheetAccountNode, totalNode) => {
const total = this.getAccountPYDatePeriodTotal(
node.id,
totalNode.previousYearFromDate.date,
totalNode.previousYearToDate.date
);
return R.assoc('previousYear', this.getAmountMeta(total), totalNode);
}
);
/**
* Previous year account horizontal node composer.
* @param {IBalanceSheetAccountNode} node -
* @param {IBalanceSheetTotal}
* @returns {IBalanceSheetTotal}
*/
private previousYearAccountHorizNodeCompose = R.curry(
(
node: IBalanceSheetAccountNode,
horizontalTotalNode: IBalanceSheetTotal
): IBalanceSheetTotal => {
return R.compose(
R.when(
this.query.isPreviousYearPercentageActive,
this.assocPreviousYearPercentageNode
),
R.when(
this.query.isPreviousYearChangeActive,
this.assocPreviousYearChangetNode
),
R.when(
this.query.isPreviousYearActive,
this.assocPreviousYearAccountHorizTotal(node)
),
R.when(
this.query.isPreviousYearActive,
this.assocPreviousYearHorizNodeFromToDates
)
)(horizontalTotalNode);
}
);
/**
* Assoc previous year horizontal nodes to account node.
* @param {IBalanceSheetAccountNode} node
* @returns {IBalanceSheetAccountNode}
*/
private assocPreviousYearAccountHorizNodeComposer = (
node: IBalanceSheetAccountNode
) => {
const horizontalTotals = R.map(
this.previousYearAccountHorizNodeCompose(node),
node.horizontalTotals
);
return R.assoc('horizontalTotals', horizontalTotals, node);
};
// ------------------------------
// # Horizontal Nodes - Aggregate.
// ------------------------------
/**
* Detarmines whether the given node has horizontal totals.
* @param {IBalanceSheetCommonNode} node
* @returns {boolean}
*/
public isNodeHasHorizontalTotals = (node: IBalanceSheetCommonNode) =>
!isEmpty(node.horizontalTotals);
};

View File

@@ -1,211 +0,0 @@
import * as R from 'ramda';
import { sumBy } from 'lodash';
import {
IBalanceSheetQuery,
IFormatNumberSettings,
IBalanceSheetDatePeriods,
IBalanceSheetAccountNode,
IBalanceSheetTotalPeriod,
IDateRange,
IBalanceSheetCommonNode,
} from '@/interfaces';
import FinancialSheet from '../FinancialSheet';
import { FinancialDatePeriods } from '../FinancialDatePeriods';
/**
* Balance sheet date periods.
*/
export const BalanceSheetDatePeriods = (Base: FinancialSheet) =>
class
extends R.compose(FinancialDatePeriods)(Base)
implements IBalanceSheetDatePeriods
{
/**
* @param {IBalanceSheetQuery}
*/
readonly query: IBalanceSheetQuery;
/**
* Retrieves the date periods based on the report query.
* @returns {IDateRange[]}
*/
get datePeriods(): IDateRange[] {
return this.getDateRanges(
this.query.fromDate,
this.query.toDate,
this.query.displayColumnsBy
);
}
/**
* Retrieves the date periods of the given node based on the report query.
* @param {IBalanceSheetCommonNode} node
* @param {Function} callback
* @returns {}
*/
protected getReportNodeDatePeriods = (
node: IBalanceSheetCommonNode,
callback: (
node: IBalanceSheetCommonNode,
fromDate: Date,
toDate: Date,
index: number
) => any
) => {
return this.getNodeDatePeriods(
this.query.fromDate,
this.query.toDate,
this.query.displayColumnsBy,
node,
callback
);
};
/**
* Retrieve the date period meta.
* @param {number} total - Total amount.
* @param {Date} fromDate - From date.
* @param {Date} toDate - To date.
* @return {ICashFlowDatePeriod}
*/
private getDatePeriodTotalMeta = (
total: number,
fromDate: Date,
toDate: Date,
overrideSettings: IFormatNumberSettings = {}
): IBalanceSheetTotalPeriod => {
return this.getDatePeriodMeta(total, fromDate, toDate, {
money: true,
...overrideSettings,
});
};
// --------------------------------
// # Account
// --------------------------------
/**
* Retrieve the given account date period total.
* @param {number} accountId
* @param {Date} toDate
* @returns {number}
*/
private getAccountDatePeriodTotal = (
accountId: number,
toDate: Date
): number => {
const periodTotalBetween = this.repository.periodsAccountsLedger
.whereAccountId(accountId)
.whereToDate(toDate)
.getClosingBalance();
const periodOpening = this.repository.periodsOpeningAccountLedger
.whereAccountId(accountId)
.getClosingBalance();
return periodOpening + periodTotalBetween;
};
/**
*
* @param {IBalanceSheetAccountNode} node
* @param {Date} fromDate
* @param {Date} toDate
* @returns {IBalanceSheetAccountNode}
*/
private getAccountNodeDatePeriod = (
node: IBalanceSheetAccountNode,
fromDate: Date,
toDate: Date
): IBalanceSheetTotalPeriod => {
const periodTotal = this.getAccountDatePeriodTotal(node.id, toDate);
return this.getDatePeriodTotalMeta(periodTotal, fromDate, toDate);
};
/**
* Retrieve total date periods of the given account node.
* @param {IBalanceSheetAccountNode} node
* @returns {IBalanceSheetAccountNode}
*/
private getAccountsNodeDatePeriods = (
node: IBalanceSheetAccountNode
): IBalanceSheetTotalPeriod[] => {
return this.getReportNodeDatePeriods(node, this.getAccountNodeDatePeriod);
};
/**
* Assoc total date periods to account node.
* @param {IBalanceSheetAccountNode} node
* @returns {IBalanceSheetAccountNode}
*/
public assocAccountNodeDatePeriods = (
node: IBalanceSheetAccountNode
): IBalanceSheetAccountNode => {
const datePeriods = this.getAccountsNodeDatePeriods(node);
return R.assoc('horizontalTotals', datePeriods, node);
};
// --------------------------------
// # Aggregate
// --------------------------------
/**
*
* @param {} node
* @param {number} index
* @returns {number}
*/
private getAggregateDatePeriodIndexTotal = (node, index) => {
return sumBy(node.children, `horizontalTotals[${index}].total.amount`);
};
/**
*
* @param {IBalanceSheetAccountNode} node
* @param {Date} fromDate
* @param {Date} toDate
* @returns
*/
public getAggregateNodeDatePeriod = (
node: IBalanceSheetAccountNode,
fromDate: Date,
toDate: Date,
index: number
) => {
const periodTotal = this.getAggregateDatePeriodIndexTotal(node, index);
return this.getDatePeriodTotalMeta(periodTotal, fromDate, toDate);
};
/**
*
* @param node
* @returns
*/
public getAggregateNodeDatePeriods = (node) => {
return this.getReportNodeDatePeriods(
node,
this.getAggregateNodeDatePeriod
);
};
/**
* Assoc total date periods to aggregate node.
* @param node
* @returns {}
*/
public assocAggregateNodeDatePeriods = (node) => {
const datePeriods = this.getAggregateNodeDatePeriods(node);
return R.assoc('horizontalTotals', datePeriods, node);
};
/**
*
* @param node
* @returns
*/
public assocAccountsNodeDatePeriods = (node) => {
return this.assocAggregateNodeDatePeriods(node);
};
};

View File

@@ -1,57 +0,0 @@
import { Injectable } from '@nestjs/common';
import { BalanceSheetTableInjectable } from './BalanceSheetTableInjectable';
import { BalanceSheetPdfInjectable } from './BalanceSheetPdfInjectable';
import { IBalanceSheetQuery } from './BalanceSheet.types';
import { TableSheet } from '../../common/TableSheet';
@Injectable()
export class BalanceSheetExportInjectable {
constructor(
private readonly balanceSheetTable: BalanceSheetTableInjectable,
private readonly balanceSheetPdf: BalanceSheetPdfInjectable,
) {}
/**
* Retrieves the trial balance sheet in XLSX format.
* @param {number} tenantId
* @param {ITrialBalanceSheetQuery} query
* @returns {Promise<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 {number} tenantId
* @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 {number} tenantId
* @param {IBalanceSheetQuery} query
* @returns {Promise<Buffer>}
*/
public async pdf(
query: IBalanceSheetQuery,
): Promise<Buffer> {
return this.balanceSheetPdf.pdf(query);
}
}

View File

@@ -1,168 +0,0 @@
import * as R from 'ramda';
import { get } from 'lodash';
import {
IBalanceSheetDataNode,
BALANCE_SHEET_NODE_TYPE,
} from '../../../interfaces';
import { FinancialFilter } from '../FinancialFilter';
import { Constructor } from '@/common/types/Constructor';
export const BalanceSheetFiltering = <T extends Constructor>(Base: T) =>
class extends R.compose(FinancialFilter)(Base) {
// -----------------------
// # Account
// -----------------------
/**
* Filter report node detarmine.
* @param {IBalanceSheetDataNode} node - Balance sheet node.
* @return {boolean}
*/
private accountNoneZeroNodesFilterDetarminer = (
node: IBalanceSheetDataNode,
): boolean => {
return R.ifElse(
this.isNodeType(BALANCE_SHEET_NODE_TYPE.ACCOUNT),
this.isNodeNoneZero,
R.always(true),
)(node);
};
/**
* Detarmines account none-transactions node.
* @param {IBalanceSheetDataNode} node
* @returns {boolean}
*/
private accountNoneTransFilterDetarminer = (
node: IBalanceSheetDataNode,
): boolean => {
return R.ifElse(
this.isNodeType(BALANCE_SHEET_NODE_TYPE.ACCOUNT),
this.isNodeNoneZero,
R.always(true),
)(node);
};
/**
* Report nodes filter.
* @param {IBalanceSheetSection[]} nodes -
* @return {IBalanceSheetSection[]}
*/
private accountsNoneZeroNodesFilter = (
nodes: IBalanceSheetDataNode[],
): IBalanceSheetDataNode[] => {
return this.filterNodesDeep(
nodes,
this.accountNoneZeroNodesFilterDetarminer,
);
};
/**
* Filters the accounts none-transactions nodes.
* @param {IBalanceSheetDataNode[]} nodes
* @returns {IBalanceSheetDataNode[]}
*/
private accountsNoneTransactionsNodesFilter = (
nodes: IBalanceSheetDataNode[],
) => {
return this.filterNodesDeep(nodes, this.accountNoneTransFilterDetarminer);
};
// -----------------------
// # Aggregate/Accounts.
// -----------------------
/**
* Detearmines aggregate none-children filtering.
* @param {IBalanceSheetDataNode} node
* @returns {boolean}
*/
private aggregateNoneChildrenFilterDetarminer = (
node: IBalanceSheetDataNode,
): boolean => {
// Detarmines whether the given node is aggregate or accounts node.
const isAggregateOrAccounts =
this.isNodeType(BALANCE_SHEET_NODE_TYPE.AGGREGATE, node) ||
this.isNodeType(BALANCE_SHEET_NODE_TYPE.ACCOUNTS, node);
// Retrieve the schema node of the given id.
const schemaNode = this.getSchemaNodeById(node.id);
// Detarmines if the schema node is always should show.
const isSchemaAlwaysShow = get(schemaNode, 'alwaysShow', false);
return isAggregateOrAccounts && !isSchemaAlwaysShow
? this.isNodeHasChildren(node)
: true;
};
/**
* Filters aggregate none-children nodes.
* @param {IBalanceSheetDataNode[]} nodes
* @returns {IBalanceSheetDataNode[]}
*/
private aggregateNoneChildrenFilter = (
nodes: IBalanceSheetDataNode[],
): IBalanceSheetDataNode[] => {
return this.filterNodesDeep2(
this.aggregateNoneChildrenFilterDetarminer,
nodes,
);
};
// -----------------------
// # Composers.
// -----------------------
/**
* Filters none-zero nodes.
* @param {IBalanceSheetDataNode[]} nodes
* @returns {IBalanceSheetDataNode[]}
*/
private filterNoneZeroNodesCompose = (
nodes: IBalanceSheetDataNode[],
): IBalanceSheetDataNode[] => {
return R.compose(
this.aggregateNoneChildrenFilter,
this.accountsNoneZeroNodesFilter,
)(nodes);
};
/**
* Filters none-transactions nodes.
* @param {IBalanceSheetDataNode[]} nodes
* @returns {IBalanceSheetDataNode[]}
*/
private filterNoneTransNodesCompose = (
nodes: IBalanceSheetDataNode[],
): IBalanceSheetDataNode[] => {
return R.compose(
this.aggregateNoneChildrenFilter,
this.accountsNoneTransactionsNodesFilter,
)(nodes);
};
/**
* Supress nodes when accounts transactions ledger is empty.
* @param {IBalanceSheetDataNode[]} nodes
* @returns {IBalanceSheetDataNode[]}
*/
private supressNodesWhenAccountsTransactionsEmpty = (
nodes: IBalanceSheetDataNode[],
): IBalanceSheetDataNode[] => {
return this.repository.totalAccountsLedger.isEmpty() ? [] : nodes;
};
/**
* Compose report nodes filtering.
* @param {IBalanceSheetDataNode[]} nodes
* @returns {IBalanceSheetDataNode[]}
*/
protected reportFilterPlugin = (nodes: IBalanceSheetDataNode[]) => {
return R.compose(
this.supressNodesWhenAccountsTransactionsEmpty,
R.when(R.always(this.query.noneZero), this.filterNoneZeroNodesCompose),
R.when(
R.always(this.query.noneTransactions),
this.filterNoneTransNodesCompose,
),
)(nodes);
};
};

View File

@@ -1,101 +0,0 @@
import moment from 'moment';
import {
IBalanceSheetStatementService,
IBalanceSheetQuery,
IBalanceSheetStatement,
} from './BalanceSheet.types';
import BalanceSheetRepository from './BalanceSheetRepository';
import { BalanceSheetMetaInjectable } from './BalanceSheetMeta';
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
@Injectable()
export class BalanceSheetInjectable {
constructor(
private readonly balanceSheetMeta: BalanceSheetMetaInjectable,
private readonly eventPublisher: EventEmitter2,
) {}
/**
* Defaults balance sheet filter query.
* @return {IBalanceSheetQuery}
*/
get defaultQuery(): IBalanceSheetQuery {
return {
displayColumnsType: 'total',
displayColumnsBy: 'month',
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'),
numberFormat: {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'total',
negativeFormat: 'mines',
},
noneZero: false,
noneTransactions: false,
basis: 'cash',
accountIds: [],
percentageOfColumn: false,
percentageOfRow: false,
previousPeriod: false,
previousPeriodAmountChange: false,
previousPeriodPercentageChange: false,
previousYear: false,
previousYearAmountChange: false,
previousYearPercentageChange: false,
};
}
/**
* Retrieve balance sheet statement.
* @param {number} tenantId
* @param {IBalanceSheetQuery} query
* @return {IBalanceSheetStatement}
*/
public async balanceSheet(
tenantId: number,
query: IBalanceSheetQuery,
): Promise<IBalanceSheetStatement> {
const filter = {
...this.defaultQuery,
...query,
};
const balanceSheetRepo = new BalanceSheetRepository(models, filter);
// Loads all resources.
await balanceSheetRepo.asyncInitialize();
// Balance sheet report instance.
const balanceSheetInstanace = new BalanceSheetStatementService(
filter,
balanceSheetRepo,
tenant.metadata.baseCurrency,
i18n,
);
// Balance sheet data.
const data = balanceSheetInstanace.reportData();
// Balance sheet meta.
const meta = await this.balanceSheetMeta.meta(tenantId, filter);
// Triggers `onBalanceSheetViewed` event.
await this.eventPublisher.emitAsync(events.reports.onBalanceSheetViewed, {
query,
});
return {
query: filter,
data,
meta,
};
}
}

View File

@@ -1,32 +0,0 @@
import { Inject, Service } from 'typedi';
import { FinancialSheetMeta } from '../FinancialSheetMeta';
import { IBalanceSheetMeta, IBalanceSheetQuery } from '@/interfaces';
import moment from 'moment';
@Service()
export class BalanceSheetMetaInjectable {
@Inject()
private financialSheetMeta: FinancialSheetMeta;
/**
* Retrieve the balance sheet meta.
* @param {number} tenantId -
* @returns {IBalanceSheetMeta}
*/
public async meta(
tenantId: number,
query: IBalanceSheetQuery
): Promise<IBalanceSheetMeta> {
const commonMeta = await this.financialSheetMeta.meta(tenantId);
const formattedAsDate = moment(query.toDate).format('YYYY/MM/DD');
const formattedDateRange = `As ${formattedAsDate}`;
const sheetName = 'Balance Sheet Statement';
return {
...commonMeta,
sheetName,
formattedAsDate,
formattedDateRange,
};
}
}

View File

@@ -1,226 +0,0 @@
import * as R from 'ramda';
import {
BALANCE_SHEET_SCHEMA_NODE_TYPE,
IBalanceSheetDataNode,
IBalanceSheetNetIncomeNode,
IBalanceSheetSchemaNetIncomeNode,
IBalanceSheetSchemaNode,
IBalanceSheetTotalPeriod,
} from '@/interfaces';
import { BalanceSheetComparsionPreviousYear } from './BalanceSheetComparsionPreviousYear';
import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod';
import { FinancialPreviousPeriod } from '../FinancialPreviousPeriod';
import { FinancialHorizTotals } from '../FinancialHorizTotals';
import BalanceSheetRepository from './BalanceSheetRepository';
import { BalanceSheetQuery } from './BalanceSheetQuery';
import { BalanceSheetNetIncomePP } from './BalanceSheetNetIncomePP';
import { BalanceSheetNetIncomePY } from './BalanceSheetNetIncomePY';
export const BalanceSheetNetIncome = (Base: any) =>
class extends R.compose(
BalanceSheetNetIncomePP,
BalanceSheetNetIncomePY,
BalanceSheetComparsionPreviousYear,
BalanceSheetComparsionPreviousPeriod,
FinancialPreviousPeriod,
FinancialHorizTotals
)(Base) {
private repository: BalanceSheetRepository;
private query: BalanceSheetQuery;
/**
* Retrieves the closing balance of income accounts.
* @returns {number}
*/
private getIncomeTotal = () => {
const closeingBalance = this.repository.incomeLedger.getClosingBalance();
return closeingBalance;
};
/**
* Retrieves the closing balance of expenses accounts.
* @returns {number}
*/
private getExpensesTotal = () => {
const closingBalance = this.repository.expensesLedger.getClosingBalance();
return closingBalance;
};
/**
* Retrieves the total net income.
* @returns {number}
*/
protected getNetIncomeTotal = () => {
const income = this.getIncomeTotal();
const expenses = this.getExpensesTotal();
return income - expenses;
};
/**
* Mappes the aggregate schema node type.
* @param {IBalanceSheetSchemaNetIncomeNode} node - Schema node.
* @return {IBalanceSheetAggregateNode}
*/
protected schemaNetIncomeNodeMapper = (
node: IBalanceSheetSchemaNetIncomeNode
): IBalanceSheetNetIncomeNode => {
const total = this.getNetIncomeTotal();
return {
id: node.id,
name: this.i18n.__(node.name),
nodeType: BALANCE_SHEET_SCHEMA_NODE_TYPE.NET_INCOME,
total: this.getTotalAmountMeta(total),
};
};
/**
* Mapps the net income shcema node to report node.
* @param {IBalanceSheetSchemaNetIncomeNode} node
* @returns {IBalanceSheetNetIncomeNode}
*/
protected schemaNetIncomeNodeCompose = (
node: IBalanceSheetSchemaNetIncomeNode
): IBalanceSheetNetIncomeNode => {
return R.compose(
R.when(
this.query.isPreviousYearActive,
this.previousYearNetIncomeNodeCompose
),
R.when(
this.query.isPreviousPeriodActive,
this.previousPeriodNetIncomeNodeCompose
),
R.when(
this.query.isDatePeriodsColumnsType,
this.assocNetIncomeDatePeriodsNode
),
this.schemaNetIncomeNodeMapper
)(node);
};
// --------------------------------
// # Date Periods
// --------------------------------
/**
* Retreives total income of the given date period.
* @param {number} accountId -
* @param {Date} toDate -
* @returns {number}
*/
private getIncomeDatePeriodTotal = (toDate: Date): number => {
const periodTotalBetween = this.repository.incomePeriodsAccountsLedger
.whereToDate(toDate)
.getClosingBalance();
const periodOpening =
this.repository.incomePeriodsOpeningAccountsLedger.getClosingBalance();
return periodOpening + periodTotalBetween;
};
/**
* Retrieves total expense of the given date period.
* @param {number} accountId -
* @param {Date} toDate -
* @returns {number}
*/
private getExpensesDatePeriodTotal = (toDate: Date): number => {
const periodTotalBetween = this.repository.expensesPeriodsAccountsLedger
.whereToDate(toDate)
.getClosingBalance();
const periodOpening =
this.repository.expensesOpeningAccountLedger.getClosingBalance();
return periodOpening + periodTotalBetween;
};
/**
* Retrieve the given net income date period total.
* @param {number} accountId
* @param {Date} toDate
* @returns {number}
*/
private getNetIncomeDatePeriodTotal = (toDate: Date): number => {
const income = this.getIncomeDatePeriodTotal(toDate);
const expense = this.getExpensesDatePeriodTotal(toDate);
return income - expense;
};
/**
* Retrieves the net income date period node.
* @param {IBalanceSheetNetIncomeNode} node
* @param {Date} fromDate
* @param {Date} toDate
* @returns {IBalanceSheetNetIncomeNode}
*/
private getNetIncomeDatePeriodNode = (
node: IBalanceSheetNetIncomeNode,
fromDate: Date,
toDate: Date
): IBalanceSheetTotalPeriod => {
const periodTotal = this.getNetIncomeDatePeriodTotal(toDate);
return this.getDatePeriodTotalMeta(periodTotal, fromDate, toDate);
};
/**
* Retrieve total date periods of the given net income node.
* @param {IBalanceSheetNetIncomeNode} node
* @returns {IBalanceSheetNetIncomeNode}
*/
private getNetIncomeDatePeriodsNode = (
node: IBalanceSheetNetIncomeNode
): IBalanceSheetTotalPeriod[] => {
return this.getReportNodeDatePeriods(
node,
this.getNetIncomeDatePeriodNode
);
};
/**
* Assoc total date periods to net income node.
* @param {IBalanceSheetNetIncomeNode} node
* @returns {IBalanceSheetNetIncomeNode}
*/
public assocNetIncomeDatePeriodsNode = (
node: IBalanceSheetNetIncomeNode
): IBalanceSheetNetIncomeNode => {
const datePeriods = this.getNetIncomeDatePeriodsNode(node);
return R.assoc('horizontalTotals', datePeriods, node);
};
// -----------------------------
// - Net Income Nodes Praser
// -----------------------------
/**
* Mappes the given report schema node.
* @param {IBalanceSheetSchemaNode} node - Schema node.
* @return {IBalanceSheetDataNode}
*/
private reportNetIncomeNodeSchemaParser = (
schemaNode: IBalanceSheetSchemaNode
): IBalanceSheetDataNode => {
return R.compose(
R.when(
this.isSchemaNodeType(BALANCE_SHEET_SCHEMA_NODE_TYPE.NET_INCOME),
this.schemaNetIncomeNodeCompose
)
)(schemaNode);
};
/**
* Parses the report net income schema nodes.
* @param {(IBalanceSheetSchemaNode | IBalanceSheetDataNode)[]} nodes -
* @return {IBalanceSheetDataNode[]}
*/
public netIncomeSchemaParser = (
nodes: (IBalanceSheetSchemaNode | IBalanceSheetDataNode)[]
): IBalanceSheetDataNode[] => {
return this.mapNodesDeep(nodes, this.reportNetIncomeNodeSchemaParser);
};
};

View File

@@ -1,120 +0,0 @@
import * as R from 'ramda';
import {
IBalanceSheetNetIncomeNode,
IBalanceSheetTotalPeriod,
} from '@/interfaces';
import { BalanceSheetComparsionPreviousYear } from './BalanceSheetComparsionPreviousYear';
import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod';
import { FinancialPreviousPeriod } from '../FinancialPreviousPeriod';
import { FinancialHorizTotals } from '../FinancialHorizTotals';
import BalanceSheetRepository from './BalanceSheetRepository';
import { BalanceSheetQuery } from './BalanceSheetQuery';
import { BalanceSheetNetIncomePP } from './BalanceSheetNetIncomePP';
import { BalanceSheetNetIncomePY } from './BalanceSheetNetIncomePY';
export const BalanceSheetNetIncomeDatePeriods = (Base: any) =>
class extends R.compose(
BalanceSheetNetIncomePP,
BalanceSheetNetIncomePY,
BalanceSheetComparsionPreviousYear,
BalanceSheetComparsionPreviousPeriod,
FinancialPreviousPeriod,
FinancialHorizTotals
)(Base) {
private repository: BalanceSheetRepository;
private query: BalanceSheetQuery;
// --------------------------------
// # Date Periods
// --------------------------------
/**
* Retreives total income of the given date period.
* @param {number} accountId -
* @param {Date} toDate -
* @returns {number}
*/
private getIncomeDatePeriodTotal = (toDate: Date): number => {
const periodTotalBetween = this.repository.incomePeriodsAccountsLedger
.whereToDate(toDate)
.getClosingBalance();
const periodOpening =
this.repository.incomePeriodsOpeningAccountsLedger.getClosingBalance();
return periodOpening + periodTotalBetween;
};
/**
* Retrieves total expense of the given date period.
* @param {number} accountId -
* @param {Date} toDate -
* @returns {number}
*/
private getExpensesDatePeriodTotal = (toDate: Date): number => {
const periodTotalBetween = this.repository.expensesPeriodsAccountsLedger
.whereToDate(toDate)
.getClosingBalance();
const periodOpening =
this.repository.expensesOpeningAccountLedger.getClosingBalance();
return periodOpening + periodTotalBetween;
};
/**
* Retrieve the given net income date period total.
* @param {number} accountId
* @param {Date} toDate
* @returns {number}
*/
private getNetIncomeDatePeriodTotal = (toDate: Date): number => {
const income = this.getIncomeDatePeriodTotal(toDate);
const expense = this.getExpensesDatePeriodTotal(toDate);
return income - expense;
};
/**
* Retrieves the net income date period node.
* @param {IBalanceSheetNetIncomeNode} node
* @param {Date} fromDate
* @param {Date} toDate
* @returns {IBalanceSheetNetIncomeNode}
*/
private getNetIncomeDatePeriodNode = (
node: IBalanceSheetNetIncomeNode,
fromDate: Date,
toDate: Date
): IBalanceSheetTotalPeriod => {
const periodTotal = this.getNetIncomeDatePeriodTotal(toDate);
return this.getDatePeriodTotalMeta(periodTotal, fromDate, toDate);
};
/**
* Retrieve total date periods of the given net income node.
* @param {IBalanceSheetNetIncomeNode} node
* @returns {IBalanceSheetNetIncomeNode}
*/
private getNetIncomeDatePeriodsNode = (
node: IBalanceSheetNetIncomeNode
): IBalanceSheetTotalPeriod[] => {
return this.getReportNodeDatePeriods(
node,
this.getNetIncomeDatePeriodNode
);
};
/**
* Assoc total date periods to net income node.
* @param {IBalanceSheetNetIncomeNode} node
* @returns {IBalanceSheetNetIncomeNode}
*/
public assocNetIncomeDatePeriodsNode = (
node: IBalanceSheetNetIncomeNode
): IBalanceSheetNetIncomeNode => {
const datePeriods = this.getNetIncomeDatePeriodsNode(node);
return R.assoc('horizontalTotals', datePeriods, node);
};
};

View File

@@ -1,127 +0,0 @@
import * as R from 'ramda';
import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod';
import { FinancialPreviousPeriod } from '../FinancialPreviousPeriod';
import { FinancialHorizTotals } from '../FinancialHorizTotals';
import { IBalanceSheetNetIncomeNode, IBalanceSheetTotal } from '@/interfaces';
import { BalanceSheetQuery } from './BalanceSheetQuery';
import BalanceSheetRepository from './BalanceSheetRepository';
export const BalanceSheetNetIncomeDatePeriodsPP = (Base: any) =>
class extends R.compose(
BalanceSheetComparsionPreviousPeriod,
FinancialPreviousPeriod,
FinancialHorizTotals
)(Base) {
query: BalanceSheetQuery;
repository: BalanceSheetRepository;
/**
* Retrieves the PY total income of the given date period.
* @param {number} accountId -
* @param {Date} toDate -
* @return {number}
*/
private getPPIncomeDatePeriodTotal = R.curry((toDate: Date) => {
const PYPeriodsTotal = this.repository.incomePPPeriodsAccountsLedger
.whereToDate(toDate)
.getClosingBalance();
const PYPeriodsOpeningTotal =
this.repository.incomePPPeriodsOpeningAccountLedger.getClosingBalance();
return PYPeriodsOpeningTotal + PYPeriodsTotal;
});
/**
* Retrieves the PY total expense of the given date period.
* @param {number} accountId -
* @param {Date} toDate -
* @returns {number}
*/
private getPPExpenseDatePeriodTotal = R.curry((toDate: Date) => {
const PYPeriodsTotal = this.repository.expensePPPeriodsAccountsLedger
.whereToDate(toDate)
.getClosingBalance();
const PYPeriodsOpeningTotal =
this.repository.expensePPPeriodsOpeningAccountLedger.getClosingBalance();
return PYPeriodsOpeningTotal + PYPeriodsTotal;
});
/**
* Retrieve the given net income total of the given period.
* @param {number} accountId - Account id.
* @param {Date} toDate - To date.
* @returns {number}
*/
private getPPNetIncomeDatePeriodTotal = R.curry((toDate: Date) => {
const income = this.getPPIncomeDatePeriodTotal(toDate);
const expense = this.getPPExpenseDatePeriodTotal(toDate);
return income - expense;
});
/**
* Assoc preivous period to account horizontal total node.
* @param {IBalanceSheetAccountNode} node
* @returns {}
*/
private assocPreviousPeriodNetIncomeHorizTotal = R.curry(
(node: IBalanceSheetNetIncomeNode, totalNode) => {
const total = this.getPPNetIncomeDatePeriodTotal(
totalNode.previousPeriodToDate.date
);
return R.assoc('previousPeriod', this.getAmountMeta(total), totalNode);
}
);
/**
* Compose previous period to aggregate horizontal nodes.
* @param {IBalanceSheetTotal} node
* @returns {IBalanceSheetTotal}
*/
private previousPeriodNetIncomeHorizNodeComposer = R.curry(
(
node: IBalanceSheetNetIncomeNode,
horiontalTotalNode: IBalanceSheetTotal
): IBalanceSheetTotal => {
return R.compose(
R.when(
this.query.isPreviousPeriodPercentageActive,
this.assocPreviousPeriodTotalPercentageNode
),
R.when(
this.query.isPreviousPeriodChangeActive,
this.assocPreviousPeriodTotalChangeNode
),
R.when(
this.query.isPreviousPeriodActive,
this.assocPreviousPeriodNetIncomeHorizTotal(node)
),
R.when(
this.query.isPreviousPeriodActive,
this.assocPreviousPeriodHorizNodeFromToDates(
this.query.displayColumnsBy
)
)
)(horiontalTotalNode);
}
);
/**
* Associate the PP to net income horizontal nodes.
* @param {IBalanceSheetCommonNode} node
* @returns {IBalanceSheetCommonNode}
*/
public assocPreviousPeriodNetIncomeHorizNode = (
node: IBalanceSheetNetIncomeNode
): IBalanceSheetNetIncomeNode => {
const horizontalTotals = R.addIndex(R.map)(
this.previousPeriodNetIncomeHorizNodeComposer(node),
node.horizontalTotals
) as IBalanceSheetTotal[];
return R.assoc('horizontalTotals', horizontalTotals, node);
};
};

View File

@@ -1,122 +0,0 @@
import * as R from 'ramda';
import { BalanceSheetComparsionPreviousYear } from './BalanceSheetComparsionPreviousYear';
import { FinancialPreviousPeriod } from '../FinancialPreviousPeriod';
import { FinancialHorizTotals } from '../FinancialHorizTotals';
import { IBalanceSheetNetIncomeNode, IBalanceSheetTotal } from '@/interfaces';
import { BalanceSheetQuery } from './BalanceSheetQuery';
import BalanceSheetRepository from './BalanceSheetRepository';
export const BalanceSheetNetIncomeDatePeriodsPY = (Base: any) =>
class extends R.compose(
BalanceSheetComparsionPreviousYear,
FinancialPreviousPeriod,
FinancialHorizTotals
)(Base) {
query: BalanceSheetQuery;
repository: BalanceSheetRepository;
/**
* Retrieves the PY total income of the given date period.
* @param {Date} toDate -
* @return {number}
*/
private getPYIncomeDatePeriodTotal = R.curry((toDate: Date) => {
const PYPeriodsTotal = this.repository.incomePYPeriodsAccountsLedger
.whereToDate(toDate)
.getClosingBalance();
const PYPeriodsOpeningTotal =
this.repository.incomePYPeriodsOpeningAccountLedger.getClosingBalance();
return PYPeriodsOpeningTotal + PYPeriodsTotal;
});
/**
* Retrieves the PY total expense of the given date period.
* @param {Date} toDate -
* @returns {number}
*/
private getPYExpenseDatePeriodTotal = R.curry((toDate: Date) => {
const PYPeriodsTotal = this.repository.expensePYPeriodsAccountsLedger
.whereToDate(toDate)
.getClosingBalance();
const PYPeriodsOpeningTotal =
this.repository.expensePYPeriodsOpeningAccountLedger.getClosingBalance();
return PYPeriodsOpeningTotal + PYPeriodsTotal;
});
/**
* Retrieve the given net income total of the given period.
* @param {Date} toDate - To date.
* @returns {number}
*/
private getPYNetIncomeDatePeriodTotal = R.curry((toDate: Date) => {
const income = this.getPYIncomeDatePeriodTotal(toDate);
const expense = this.getPYExpenseDatePeriodTotal(toDate);
return income - expense;
});
/**
* Assoc preivous year to account horizontal total node.
* @param {IBalanceSheetAccountNode} node
* @returns {}
*/
private assocPreviousYearNetIncomeHorizTotal = R.curry(
(node: IBalanceSheetNetIncomeNode, totalNode) => {
const total = this.getPYNetIncomeDatePeriodTotal(
totalNode.previousYearToDate.date
);
return R.assoc('previousYear', this.getAmountMeta(total), totalNode);
}
);
/**
* Compose PY to net income horizontal nodes.
* @param {IBalanceSheetTotal} node
* @returns {IBalanceSheetTotal}
*/
private previousYearNetIncomeHorizNodeComposer = R.curry(
(
node: IBalanceSheetNetIncomeNode,
horiontalTotalNode: IBalanceSheetTotal
): IBalanceSheetTotal => {
return R.compose(
R.when(
this.query.isPreviousYearPercentageActive,
this.assocPreviousYearTotalPercentageNode
),
R.when(
this.query.isPreviousYearChangeActive,
this.assocPreviousYearTotalChangeNode
),
R.when(
this.query.isPreviousYearActive,
this.assocPreviousYearNetIncomeHorizTotal(node)
),
R.when(
this.query.isPreviousYearActive,
this.assocPreviousYearHorizNodeFromToDates
)
)(horiontalTotalNode);
}
);
/**
* Associate the PY to net income horizontal nodes.
* @param {IBalanceSheetCommonNode} node
* @returns {IBalanceSheetCommonNode}
*/
public assocPreviousYearNetIncomeHorizNode = (
node: IBalanceSheetNetIncomeNode
): IBalanceSheetNetIncomeNode => {
const horizontalTotals = R.addIndex(R.map)(
this.previousYearNetIncomeHorizNodeComposer(node),
node.horizontalTotals
) as IBalanceSheetTotal[];
return R.assoc('horizontalTotals', horizontalTotals, node);
};
};

View File

@@ -1,75 +0,0 @@
import * as R from 'ramda';
import {
IBalanceSheetDataNode,
IBalanceSheetNetIncomeNode,
} from '@/interfaces';
import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod';
import { FinancialPreviousPeriod } from '../FinancialPreviousPeriod';
import { FinancialHorizTotals } from '../FinancialHorizTotals';
import BalanceSheetRepository from './BalanceSheetRepository';
import { BalanceSheetQuery } from './BalanceSheetQuery';
import { BalanceSheetNetIncomeDatePeriodsPP } from './BalanceSheetNetIncomeDatePeriodsPP';
export const BalanceSheetNetIncomePP = (Base: any) =>
class extends R.compose(
BalanceSheetNetIncomeDatePeriodsPP,
BalanceSheetComparsionPreviousPeriod,
FinancialPreviousPeriod,
FinancialHorizTotals
)(Base) {
private repository: BalanceSheetRepository;
private query: BalanceSheetQuery;
// -------------------------------
// # Previous Period (PP)
// -------------------------------
/**
* Retrieves the PP net income.
* @returns {}
*/
protected getPreviousPeriodNetIncome = () => {
const income = this.repository.incomePPAccountsLedger.getClosingBalance();
const expense =
this.repository.expensePPAccountsLedger.getClosingBalance();
return income - expense;
};
/**
* Associates the previous period to account node.
* @param {IBalanceSheetDataNode} node
* @returns {IBalanceSheetDataNode}
*/
protected assocPreviousPeriodNetIncomeNode = (
node: IBalanceSheetDataNode
): IBalanceSheetDataNode => {
const total = this.getPreviousPeriodNetIncome();
return R.assoc('previousPeriod', this.getAmountMeta(total), node);
};
/**
* Previous period account node composer.
* @param {IBalanceSheetNetIncomeNode} node
* @returns {IBalanceSheetNetIncomeNode}
*/
protected previousPeriodNetIncomeNodeCompose = (
node: IBalanceSheetNetIncomeNode
): IBalanceSheetNetIncomeNode => {
return R.compose(
R.when(
this.isNodeHasHorizTotals,
this.assocPreviousPeriodNetIncomeHorizNode
),
R.when(
this.query.isPreviousPeriodPercentageActive,
this.assocPreviousPeriodPercentageNode
),
R.when(
this.query.isPreviousPeriodChangeActive,
this.assocPreviousPeriodChangeNode
),
this.assocPreviousPeriodNetIncomeNode
)(node);
};
};

View File

@@ -1,79 +0,0 @@
import * as R from 'ramda';
import {
IBalanceSheetDataNode,
IBalanceSheetNetIncomeNode,
} from '@/interfaces';
import { BalanceSheetComparsionPreviousYear } from './BalanceSheetComparsionPreviousYear';
import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod';
import { FinancialPreviousPeriod } from '../FinancialPreviousPeriod';
import { FinancialHorizTotals } from '../FinancialHorizTotals';
import BalanceSheetRepository from './BalanceSheetRepository';
import { BalanceSheetQuery } from './BalanceSheetQuery';
import { BalanceSheetNetIncomeDatePeriodsPY } from './BalanceSheetNetIncomeDatePeriodsPY';
export const BalanceSheetNetIncomePY = (Base: any) =>
class extends R.compose(
BalanceSheetNetIncomeDatePeriodsPY,
BalanceSheetComparsionPreviousYear,
BalanceSheetComparsionPreviousPeriod,
FinancialPreviousPeriod,
FinancialHorizTotals
)(Base) {
private repository: BalanceSheetRepository;
private query: BalanceSheetQuery;
// ------------------------------
// # Previous Year (PY)
// ------------------------------
/**
* Retrieves the previous year (PY) net income.
* @returns {number}
*/
protected getPreviousYearNetIncome = () => {
const income =
this.repository.incomePYTotalAccountsLedger.getClosingBalance();
const expense =
this.repository.expensePYTotalAccountsLedger.getClosingBalance();
return income - expense;
};
/**
* Assoc previous year on aggregate node.
* @param {IBalanceSheetAccountNode} node
* @returns {IBalanceSheetAccountNode}
*/
protected assocPreviousYearNetIncomeNode = (
node: IBalanceSheetNetIncomeNode
): IBalanceSheetNetIncomeNode => {
const total = this.getPreviousYearNetIncome();
return R.assoc('previousYear', this.getTotalAmountMeta(total), node);
};
/**
* Assoc previous year attributes to aggregate node.
* @param {IBalanceSheetAccountNode} node
* @returns {IBalanceSheetAccountNode}
*/
protected previousYearNetIncomeNodeCompose = (
node: IBalanceSheetNetIncomeNode
): IBalanceSheetNetIncomeNode => {
return R.compose(
R.when(
this.query.isPreviousYearPercentageActive,
this.assocPreviousYearTotalPercentageNode
),
R.when(
this.query.isPreviousYearChangeActive,
this.assocPreviousYearTotalChangeNode
),
// Associate the PY to date periods horizontal nodes.
R.when(
this.isNodeHasHorizontalTotals,
this.assocPreviousYearNetIncomeHorizNode
),
this.assocPreviousYearNetIncomeNode
)(node);
};
};

View File

@@ -1,32 +0,0 @@
import { TableSheetPdf } from '../../TableSheetPdf';
import { IBalanceSheetQuery } from './BalanceSheet.types';
import { BalanceSheetTableInjectable } from './BalanceSheetTableInjectable';
import { HtmlTableCustomCss } from './constants';
import { Injectable } from '@nestjs/common';
@Injectable()
export class BalanceSheetPdfInjectable {
constructor(
private readonly balanceSheetTable: BalanceSheetTableInjectable,
private readonly tableSheetPdf: TableSheetPdf,
) {}
/**
* Converts the given balance sheet table to pdf.
* @param {number} tenantId - Tenant ID.
* @param {IBalanceSheetQuery} query - Balance sheet query.
* @returns {Promise<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,
);
}
}

View File

@@ -1,226 +0,0 @@
import * as R from 'ramda';
import { get } from 'lodash';
import { BalanceSheetQuery } from './BalanceSheetQuery';
import { IBalanceSheetDataNode } from './BalanceSheet.types';
import { Constructor } from '@/common/types/Constructor';
export const BalanceSheetPercentage = (Base: any) =>
class extends Base {
readonly query: BalanceSheetQuery;
/**
* Assoc percentage of column to report node.
* @param {IBalanceSheetDataNode} node
* @returns {IBalanceSheetDataNode}
*/
public assocReportNodeColumnPercentage = R.curry(
(
parentTotal: number,
node: IBalanceSheetDataNode
): IBalanceSheetDataNode => {
const percentage = this.getPercentageBasis(
parentTotal,
node.total.amount
);
return R.assoc(
'percentageColumn',
this.getPercentageAmountMeta(percentage),
node
);
}
);
/**
* Assoc percentage of row to report node.
* @param {IBalanceSheetDataNode} node
* @returns {IBalanceSheetDataNode}
*/
public assocReportNodeRowPercentage = R.curry(
(
parentTotal: number,
node: IBalanceSheetDataNode
): IBalanceSheetDataNode => {
const percenatage = this.getPercentageBasis(
parentTotal,
node.total.amount
);
return R.assoc(
'percentageRow',
this.getPercentageAmountMeta(percenatage),
node
);
}
);
/**
* Assoc percentage of row to horizontal total.
* @param {number} parentTotal -
* @param {IBalanceSheetDataNode} node
* @returns {IBalanceSheetDataNode}
*/
public assocRowPercentageHorizTotals = R.curry(
(
parentTotal: number,
node: IBalanceSheetDataNode
): IBalanceSheetDataNode => {
const assocRowPercen = this.assocReportNodeRowPercentage(parentTotal);
const horTotals = R.map(assocRowPercen)(node.horizontalTotals);
return R.assoc('horizontalTotals', horTotals, node);
}
);
/**
*
* @param {} parentNode -
* @param {} horTotalNode -
* @param {number} index -
*/
private assocColumnPercentageHorizTotal = R.curry(
(parentNode, horTotalNode, index) => {
const parentTotal = get(
parentNode,
`horizontalTotals[${index}].total.amount`,
0
);
return this.assocReportNodeColumnPercentage(parentTotal, horTotalNode);
}
);
/**
* Assoc column percentage to horizontal totals nodes.
* @param {IBalanceSheetDataNode} node
* @returns {IBalanceSheetDataNode}
*/
public assocColumnPercentageHorizTotals = R.curry(
(
parentNode: IBalanceSheetDataNode,
node: IBalanceSheetDataNode
): IBalanceSheetDataNode => {
// Horizontal totals.
const assocColPerc = this.assocColumnPercentageHorizTotal(parentNode);
const horTotals = R.addIndex(R.map)(assocColPerc)(
node.horizontalTotals
);
return R.assoc('horizontalTotals', horTotals, node);
}
);
/**
*
* @param {number} parentTotal -
* @param {} node
* @returns
*/
public reportNodeColumnPercentageComposer = R.curry(
(parentNode, node) => {
const parentTotal = parentNode.total.amount;
return R.compose(
R.when(
this.isNodeHasHorizoTotals,
this.assocColumnPercentageHorizTotals(parentNode)
),
this.assocReportNodeColumnPercentage(parentTotal)
)(node);
}
);
/**
*
* @param node
* @returns
*/
private reportNodeRowPercentageComposer = (node) => {
const total = node.total.amount;
return R.compose(
R.when(
this.isNodeHasHorizoTotals,
this.assocRowPercentageHorizTotals(total)
),
this.assocReportNodeRowPercentage(total)
)(node);
};
/**
*
*/
private assocNodeColumnPercentageChildren = (node) => {
const children = this.mapNodesDeep(
node.children,
this.reportNodeColumnPercentageComposer(node)
);
return R.assoc('children', children, node);
};
/**
*
* @param node
* @returns
*/
private reportNodeColumnPercentageDeepMap = (node) => {
const parentTotal = node.total.amount;
const parentNode = node;
return R.compose(
R.when(
this.isNodeHasHorizoTotals,
this.assocColumnPercentageHorizTotals(parentNode)
),
this.assocReportNodeColumnPercentage(parentTotal),
this.assocNodeColumnPercentageChildren
)(node);
};
/**
*
* @param {IBalanceSheetDataNode[]} node
* @returns {IBalanceSheetDataNode[]}
*/
private reportColumnsPercentageMapper = (
nodes: IBalanceSheetDataNode[]
): IBalanceSheetDataNode[] => {
return R.map(this.reportNodeColumnPercentageDeepMap, nodes);
};
/**
*
* @param nodes
* @returns
*/
private reportRowsPercentageMapper = (nodes) => {
return this.mapNodesDeep(nodes, this.reportNodeRowPercentageComposer);
};
/**
*
* @param nodes
* @returns
*/
public reportPercentageCompose = (nodes) => {
return R.compose(
R.when(
this.query.isColumnsPercentageActive,
this.reportColumnsPercentageMapper
),
R.when(
this.query.isRowsPercentageActive,
this.reportRowsPercentageMapper
)
)(nodes);
};
/**
* Detarmines whether the given node has horizontal total.
* @param {IBalanceSheetDataNode} node
* @returns {boolean}
*/
public isNodeHasHorizoTotals = (
node: IBalanceSheetDataNode
): boolean => {
return (
!R.isEmpty(node.horizontalTotals) && !R.isNil(node.horizontalTotals)
);
};
};

View File

@@ -1,182 +0,0 @@
import { merge } from 'lodash';
import * as R from 'ramda';
import { IBalanceSheetQuery, IFinancialDatePeriodsUnit } from '@/interfaces';
import { FinancialDateRanges } from '../FinancialDateRanges';
import { DISPLAY_COLUMNS_BY } from './constants';
export class BalanceSheetQuery extends R.compose(FinancialDateRanges)(
class {}
) {
/**
* Balance sheet query.
* @param {IBalanceSheetQuery}
*/
public readonly query: IBalanceSheetQuery;
/**
* Previous year to date.
* @param {Date}
*/
public readonly PYToDate: Date;
/**
* Previous year from date.
* @param {Date}
*/
public readonly PYFromDate: Date;
/**
* Previous period to date.
* @param {Date}
*/
public readonly PPToDate: Date;
/**
* Previous period from date.
* @param {Date}
*/
public readonly PPFromDate: Date;
/**
* Constructor method
* @param {IBalanceSheetQuery} query
*/
constructor(query: IBalanceSheetQuery) {
super();
this.query = query;
// Pervious Year (PY) Dates.
this.PYToDate = this.getPreviousYearDate(this.query.toDate);
this.PYFromDate = this.getPreviousYearDate(this.query.fromDate);
// Previous Period (PP) Dates for Total column.
if (this.isTotalColumnType()) {
const { fromDate, toDate } = this.getPPTotalDateRange(
this.query.fromDate,
this.query.toDate
);
this.PPToDate = toDate;
this.PPFromDate = fromDate;
// Previous Period (PP) Dates for Date period columns type.
} else if (this.isDatePeriodsColumnsType()) {
const { fromDate, toDate } = this.getPPDatePeriodDateRange(
this.query.fromDate,
this.query.toDate,
this.query.displayColumnsBy as IFinancialDatePeriodsUnit
);
this.PPToDate = toDate;
this.PPFromDate = fromDate;
}
return merge(this, query);
}
// ---------------------------
// # Columns Type/By.
// ---------------------------
/**
* Detarmines the given display columns type.
* @param {string} displayColumnsBy
* @returns {boolean}
*/
public isDisplayColumnsBy = (displayColumnsBy: string): boolean => {
return this.query.displayColumnsBy === displayColumnsBy;
};
/**
* Detarmines the given display columns by type.
* @param {string} displayColumnsBy
* @returns {boolean}
*/
public isDisplayColumnsType = (displayColumnsType: string): boolean => {
return this.query.displayColumnsType === displayColumnsType;
};
/**
* Detarmines whether the columns type is date periods.
* @returns {boolean}
*/
public isDatePeriodsColumnsType = (): boolean => {
return this.isDisplayColumnsType(DISPLAY_COLUMNS_BY.DATE_PERIODS);
};
/**
* Detarmines whether the columns type is total.
* @returns {boolean}
*/
public isTotalColumnType = (): boolean => {
return this.isDisplayColumnsType(DISPLAY_COLUMNS_BY.TOTAL);
};
// ---------------------------
// # Percentage column/row.
// ---------------------------
/**
* Detarmines whether the percentage of column active.
* @returns {boolean}
*/
public isColumnsPercentageActive = (): boolean => {
return this.query.percentageOfColumn;
};
/**
* Detarmines whether the percentage of row active.
* @returns {boolean}
*/
public isRowsPercentageActive = (): boolean => {
return this.query.percentageOfRow;
};
// ---------------------------
// # Previous Year (PY)
// ---------------------------
/**
* Detarmines the report query has previous year enabled.
* @returns {boolean}
*/
public isPreviousYearActive = (): boolean => {
return this.query.previousYear;
};
/**
* Detarmines the report query has previous year percentage change active.
* @returns {boolean}
*/
public isPreviousYearPercentageActive = (): boolean => {
return this.query.previousYearPercentageChange;
};
/**
* Detarmines the report query has previous year change active.
* @returns {boolean}
*/
public isPreviousYearChangeActive = (): boolean => {
return this.query.previousYearAmountChange;
};
// ---------------------------
// # Previous Period (PP).
// ---------------------------
/**
* Detarmines the report query has previous period enabled.
* @returns {boolean}
*/
public isPreviousPeriodActive = (): boolean => {
return this.query.previousPeriod;
};
/**
* Detarmines wether the preivous period percentage is active.
* @returns {boolean}
*/
public isPreviousPeriodPercentageActive = (): boolean => {
return this.query.previousPeriodPercentageChange;
};
/**
* Detarmines wether the previous period change is active.
* @returns {boolean}
*/
public isPreviousPeriodChangeActive = (): boolean => {
return this.query.previousPeriodAmountChange;
};
}

View File

@@ -1,402 +0,0 @@
import { Service } from 'typedi';
import * as R from 'ramda';
import { Knex } from 'knex';
import { isEmpty } from 'lodash';
import {
IAccountTransactionsGroupBy,
IBalanceSheetQuery,
ILedger,
} from '@/interfaces';
import { transformToMapBy } from 'utils';
import Ledger from '@/services/Accounting/Ledger';
import { BalanceSheetQuery } from './BalanceSheetQuery';
import { FinancialDatePeriods } from '../FinancialDatePeriods';
import { BalanceSheetRepositoryNetIncome } from './BalanceSheetRepositoryNetIncome';
@Service()
export default class BalanceSheetRepository extends R.compose(
BalanceSheetRepositoryNetIncome,
FinancialDatePeriods
)(class {}) {
/**
*
*/
private readonly models;
/**
* @param {number}
*/
public readonly tenantId: number;
/**
* @param {BalanceSheetQuery}
*/
public readonly query: BalanceSheetQuery;
/**
* @param {}
*/
public accounts: any;
/**
* @param {}
*/
public accountsGraph: any;
/**
*
*/
public accountsByType: any;
/**
* PY from date.
* @param {Date}
*/
public readonly PYFromDate: Date;
/**
* PY to date.
* @param {Date}
*/
public readonly PYToDate: Date;
/**
* PP to date.
* @param {Date}
*/
public readonly PPToDate: Date;
/**
* PP from date.
* @param {Date}
*/
public readonly PPFromDate: Date;
/**
* Total closing accounts ledger.
* @param {Ledger}
*/
public totalAccountsLedger: Ledger;
/**
* Total income accounts ledger.
*/
public incomeLedger: Ledger;
/**
* Total expense accounts ledger.
*/
public expensesLedger: Ledger;
/**
* Transactions group type.
* @param {IAccountTransactionsGroupBy}
*/
public transactionsGroupType: IAccountTransactionsGroupBy =
IAccountTransactionsGroupBy.Month;
// -----------------------
// # Date Periods
// -----------------------
/**
* @param {Ledger}
*/
public periodsAccountsLedger: Ledger;
/**
* @param {Ledger}
*/
public periodsOpeningAccountLedger: Ledger;
// -----------------------
// # Previous Year (PY).
// -----------------------
/**
* @param {Ledger}
*/
public PYPeriodsOpeningAccountLedger: Ledger;
/**
* @param {Ledger}
*/
public PYPeriodsAccountsLedger: Ledger;
/**
* @param {Ledger}
*/
public PYTotalAccountsLedger: ILedger;
// -----------------------
// # Previous Period (PP).
// -----------------------
/**
* @param {Ledger}
*/
public PPTotalAccountsLedger: Ledger;
/**
* @param {Ledger}
*/
public PPPeriodsAccountsLedger: ILedger;
/**
* @param {Ledger}
*/
public PPPeriodsOpeningAccountLedger: ILedger;
/**
* Constructor method.
* @param {number} tenantId
* @param {IBalanceSheetQuery} query
*/
constructor(models: any, query: IBalanceSheetQuery) {
super();
this.query = new BalanceSheetQuery(query);
this.models = models;
this.transactionsGroupType = this.getGroupByFromDisplayColumnsBy(
this.query.displayColumnsBy
);
}
/**
* Async initialize.
* @returns {Promise<void>}
*/
public asyncInitialize = async () => {
await this.initAccounts();
await this.initAccountsGraph();
await this.initAccountsTotalLedger();
// Date periods.
if (this.query.isDatePeriodsColumnsType()) {
await this.initTotalDatePeriods();
}
// Previous Year (PY).
if (this.query.isPreviousYearActive()) {
await this.initTotalPreviousYear();
}
if (
this.query.isPreviousYearActive() &&
this.query.isDatePeriodsColumnsType()
) {
await this.initPeriodsPreviousYear();
}
// Previous Period (PP).
if (this.query.isPreviousPeriodActive()) {
await this.initTotalPreviousPeriod();
}
if (
this.query.isPreviousPeriodActive() &&
this.query.isDatePeriodsColumnsType()
) {
await this.initPeriodsPreviousPeriod();
}
//
await this.asyncInitializeNetIncome();
};
// ----------------------------
// # Accounts
// ----------------------------
public initAccounts = async () => {
const accounts = await this.getAccounts();
this.accounts = accounts;
this.accountsByType = transformToMapBy(accounts, 'accountType');
this.accountsByParentType = transformToMapBy(accounts, 'accountParentType');
};
/**
* Initialize accounts graph.
*/
public initAccountsGraph = async () => {
const { Account } = this.models;
this.accountsGraph = Account.toDependencyGraph(this.accounts);
};
// ----------------------------
// # Closing Total
// ----------------------------
/**
* Initialize accounts closing total based on the given query.
* @returns {Promise<void>}
*/
private 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>}
*/
private 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>}
*/
private 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>}
*/
private 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>}
*/
private 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[]>}
*/
private getAccounts = () => {
const { Account } = this.models;
return Account.query();
};
/**
* Closing accounts date periods.
* @param {Date} fromDate
* @param {Date} toDate
* @param {string} datePeriodsType
* @returns
*/
public accountsDatePeriods = async (
fromDate: Date,
toDate: Date,
datePeriodsType: string
) => {
const { AccountTransaction } = this.models;
return AccountTransaction.query().onBuild((query) => {
query.sum('credit as credit');
query.sum('debit as debit');
query.groupBy('accountId');
query.select(['accountId']);
query.modify('groupByDateFormat', datePeriodsType);
query.modify('filterDateRange', fromDate, toDate);
query.withGraphFetched('account');
this.commonFilterBranchesQuery(query);
});
};
/**
* Retrieve the opening balance transactions of the report.
* @param {Date|string} openingDate -
*/
public closingAccountsTotal = async (openingDate: Date | string) => {
const { AccountTransaction } = this.models;
return AccountTransaction.query().onBuild((query) => {
query.sum('credit as credit');
query.sum('debit as debit');
query.groupBy('accountId');
query.select(['accountId']);
query.modify('filterDateRange', null, openingDate);
query.withGraphFetched('account');
this.commonFilterBranchesQuery(query);
});
};
/**
* Common branches filter query.
* @param {Knex.QueryBuilder} query
*/
private commonFilterBranchesQuery = (query: Knex.QueryBuilder) => {
if (!isEmpty(this.query.branchesIds)) {
query.modify('filterByBranches', this.query.branchesIds);
}
};
}

View File

@@ -1,227 +0,0 @@
import * as R from 'ramda';
import { FinancialDatePeriods } from '../../common/FinancialDatePeriods';
import { ModelObject } from 'objection';
import { Account } from '@/modules/Accounts/models/Account.model';
import { ILedger } from '@/modules/Ledger/types/Ledger.types';
import { ACCOUNT_PARENT_TYPE } from '@/constants/accounts';
import { Constructor } from '@/common/types/Constructor';
export const BalanceSheetRepositoryNetIncome = <T extends Constructor>(
Base: T,
) =>
class extends R.compose(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.
*/
private initIncomeAccounts = () => {
const incomeAccounts = this.accountsByParentType.get(
ACCOUNT_PARENT_TYPE.INCOME,
);
const incomeAccountsIds = incomeAccounts.map((a) => a.id);
this.incomeAccounts = incomeAccounts;
this.incomeAccountsIds = incomeAccountsIds;
};
/**
* Initialize expense accounts.
*/
private initExpenseAccounts = () => {
const expensesAccounts = this.accountsByParentType.get(
ACCOUNT_PARENT_TYPE.EXPENSE,
);
const expensesAccountsIds = expensesAccounts.map((a) => a.id);
this.expenseAccounts = expensesAccounts;
this.expenseAccountsIds = expensesAccountsIds;
};
/**
* Initialize the income total ledger.
*/
private initIncomeTotalLedger = (): void => {
// Inject to the repository.
this.incomeLedger = this.totalAccountsLedger.whereAccountsIds(
this.incomeAccountsIds,
);
};
/**
* Initialize the expenses total ledger.
*/
private initExpensesTotalLedger = (): void => {
this.expensesLedger = this.totalAccountsLedger.whereAccountsIds(
this.expenseAccountsIds,
);
};
// ----------------------------
// # Net Income - Date Periods
// ----------------------------
/**
* Initialize the net income date periods.
*/
public initNetIncomeDatePeriods = () => {
this.incomePeriodsAccountsLedger =
this.periodsAccountsLedger.whereAccountsIds(this.incomeAccountsIds);
this.incomePeriodsOpeningAccountsLedger =
this.periodsOpeningAccountLedger.whereAccountsIds(
this.incomeAccountsIds,
);
this.expensesPeriodsAccountsLedger =
this.periodsAccountsLedger.whereAccountsIds(this.expenseAccountsIds);
this.expensesOpeningAccountLedger =
this.periodsOpeningAccountLedger.whereAccountsIds(
this.expenseAccountsIds,
);
};
// ----------------------------
// # Net Income - Previous Period
// ----------------------------
/**
* Initialize the total net income PP.
*/
public initNetIncomePreviousPeriod = () => {
this.incomePPAccountsLedger = this.PPTotalAccountsLedger.whereAccountsIds(
this.incomeAccountsIds,
);
this.expensePPAccountsLedger =
this.PPTotalAccountsLedger.whereAccountsIds(this.expenseAccountsIds);
};
/**
* Initialize the net income periods of previous period.
*/
public initNetIncomePeriodsPreviousPeriod = () => {
this.incomePPPeriodsAccountsLedger =
this.PPPeriodsAccountsLedger.whereAccountsIds(this.incomeAccountsIds);
this.incomePPPeriodsOpeningAccountLedger =
this.PPPeriodsOpeningAccountLedger.whereAccountsIds(
this.incomeAccountsIds,
);
this.expensePPPeriodsAccountsLedger =
this.PPPeriodsAccountsLedger.whereAccountsIds(this.expenseAccountsIds);
this.expensePPPeriodsOpeningAccountLedger =
this.PPPeriodsOpeningAccountLedger.whereAccountsIds(
this.expenseAccountsIds,
);
};
// ----------------------------
// # Net Income - Previous Year
// ----------------------------
/**
* Initialize the net income PY total.
*/
public initNetIncomePreviousYear = () => {
this.incomePYTotalAccountsLedger =
this.PYTotalAccountsLedger.whereAccountsIds(this.incomeAccountsIds);
this.expensePYTotalAccountsLedger =
this.PYTotalAccountsLedger.whereAccountsIds(this.expenseAccountsIds);
};
/**
* Initialize the net income PY periods.
*/
public initNetIncomePeriodsPreviewYear = () => {
this.incomePYPeriodsAccountsLedger =
this.PYPeriodsAccountsLedger.whereAccountsIds(this.incomeAccountsIds);
this.incomePYPeriodsOpeningAccountLedger =
this.PYPeriodsOpeningAccountLedger.whereAccountsIds(
this.incomeAccountsIds,
);
this.expensePYPeriodsAccountsLedger =
this.PYPeriodsAccountsLedger.whereAccountsIds(this.expenseAccountsIds);
this.expensePYPeriodsOpeningAccountLedger =
this.PYPeriodsOpeningAccountLedger.whereAccountsIds(
this.expenseAccountsIds,
);
};
};

View File

@@ -1,129 +0,0 @@
/* eslint-disable import/prefer-default-export */
import * as R from 'ramda';
import {
BALANCE_SHEET_SCHEMA_NODE_ID,
BALANCE_SHEET_SCHEMA_NODE_TYPE,
} from './BalanceSheet.types';
import { FinancialSchema } from '../../common/FinancialSchema';
import { Constructor } from '@/common/types/Constructor';
import { ACCOUNT_TYPE } from '@/constants/accounts';
export const BalanceSheetSchema = <T extends Constructor>(Base: T) =>
class extends R.compose(FinancialSchema)(Base) {
/**
* Retrieves the balance sheet schema.
* @returns
*/
getSchema = () => {
return getBalanceSheetSchema();
};
};
/**
* Retrieve the balance sheet report schema.
*/
export const getBalanceSheetSchema = () => [
{
name: 'balance_sheet.assets',
id: BALANCE_SHEET_SCHEMA_NODE_ID.ASSETS,
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE,
children: [
{
name: 'balance_sheet.current_asset',
id: BALANCE_SHEET_SCHEMA_NODE_ID.CURRENT_ASSETS,
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE,
children: [
{
name: 'balance_sheet.cash_and_cash_equivalents',
id: BALANCE_SHEET_SCHEMA_NODE_ID.CASH_EQUIVALENTS,
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS,
accountsTypes: [ACCOUNT_TYPE.CASH, ACCOUNT_TYPE.BANK],
},
{
name: 'balance_sheet.accounts_receivable',
id: BALANCE_SHEET_SCHEMA_NODE_ID.ACCOUNTS_RECEIVABLE,
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS,
accountsTypes: [ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE],
},
{
name: 'balance_sheet.inventory',
id: BALANCE_SHEET_SCHEMA_NODE_ID.INVENTORY,
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS,
accountsTypes: [ACCOUNT_TYPE.INVENTORY],
},
{
name: 'balance_sheet.other_current_assets',
id: BALANCE_SHEET_SCHEMA_NODE_ID.OTHER_CURRENT_ASSET,
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS,
accountsTypes: [ACCOUNT_TYPE.OTHER_CURRENT_ASSET],
},
],
alwaysShow: true,
},
{
name: 'balance_sheet.fixed_asset',
id: BALANCE_SHEET_SCHEMA_NODE_ID.FIXED_ASSET,
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS,
accountsTypes: [ACCOUNT_TYPE.FIXED_ASSET],
},
{
name: 'balance_sheet.non_current_assets',
id: BALANCE_SHEET_SCHEMA_NODE_ID.NON_CURRENT_ASSET,
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS,
accountsTypes: [ACCOUNT_TYPE.NON_CURRENT_ASSET],
},
],
alwaysShow: true,
},
{
name: 'balance_sheet.liabilities_and_equity',
id: BALANCE_SHEET_SCHEMA_NODE_ID.LIABILITY_EQUITY,
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE,
children: [
{
name: 'balance_sheet.liabilities',
id: BALANCE_SHEET_SCHEMA_NODE_ID.LIABILITY,
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE,
children: [
{
name: 'balance_sheet.current_liabilties',
id: BALANCE_SHEET_SCHEMA_NODE_ID.CURRENT_LIABILITY,
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS,
accountsTypes: [
ACCOUNT_TYPE.ACCOUNTS_PAYABLE,
ACCOUNT_TYPE.TAX_PAYABLE,
ACCOUNT_TYPE.CREDIT_CARD,
ACCOUNT_TYPE.OTHER_CURRENT_LIABILITY,
],
},
{
name: 'balance_sheet.long_term_liabilities',
id: BALANCE_SHEET_SCHEMA_NODE_ID.LOGN_TERM_LIABILITY,
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS,
accountsTypes: [ACCOUNT_TYPE.LOGN_TERM_LIABILITY],
},
{
name: 'balance_sheet.non_current_liabilities',
id: BALANCE_SHEET_SCHEMA_NODE_ID.NON_CURRENT_LIABILITY,
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS,
accountsTypes: [ACCOUNT_TYPE.NON_CURRENT_LIABILITY],
},
],
},
{
name: 'balance_sheet.equity',
id: BALANCE_SHEET_SCHEMA_NODE_ID.EQUITY,
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS,
accountsTypes: [ACCOUNT_TYPE.EQUITY],
children: [
{
name: 'balance_sheet.net_income',
id: BALANCE_SHEET_SCHEMA_NODE_ID.NET_INCOME,
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.NET_INCOME,
},
],
},
],
alwaysShow: true,
},
];

View File

@@ -1,278 +0,0 @@
import * as R from 'ramda';
import {
IBalanceSheetStatementData,
ITableColumnAccessor,
IBalanceSheetQuery,
ITableColumn,
ITableRow,
BALANCE_SHEET_SCHEMA_NODE_TYPE,
IBalanceSheetDataNode,
IBalanceSheetSchemaNode,
IBalanceSheetNetIncomeNode,
IBalanceSheetAccountNode,
IBalanceSheetAccountsNode,
IBalanceSheetAggregateNode,
} from '@/interfaces';
import { tableRowMapper } from 'utils';
import FinancialSheet from '../FinancialSheet';
import { BalanceSheetComparsionPreviousYear } from './BalanceSheetComparsionPreviousYear';
import { IROW_TYPE, DISPLAY_COLUMNS_BY } from './constants';
import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod';
import { BalanceSheetPercentage } from './BalanceSheetPercentage';
import { FinancialSheetStructure } from '../FinancialSheetStructure';
import { BalanceSheetBase } from './BalanceSheetBase';
import { BalanceSheetTablePercentage } from './BalanceSheetTablePercentage';
import { BalanceSheetTablePreviousYear } from './BalanceSheetTablePreviousYear';
import { BalanceSheetTablePreviousPeriod } from './BalanceSheetTablePreviousPeriod';
import { FinancialTable } from '../FinancialTable';
import { BalanceSheetQuery } from './BalanceSheetQuery';
import { BalanceSheetTableDatePeriods } from './BalanceSheetTableDatePeriods';
export class BalanceSheetTable extends R.compose(
BalanceSheetTablePreviousPeriod,
BalanceSheetTablePreviousYear,
BalanceSheetTableDatePeriods,
BalanceSheetTablePercentage,
BalanceSheetComparsionPreviousYear,
BalanceSheetComparsionPreviousPeriod,
BalanceSheetPercentage,
FinancialSheetStructure,
FinancialTable,
BalanceSheetBase
)(FinancialSheet) {
/**
* Balance sheet data.
* @param {IBalanceSheetStatementData}
*/
private reportData: IBalanceSheetStatementData;
/**
* Balance sheet query.
* @parma {BalanceSheetQuery}
*/
private query: BalanceSheetQuery;
/**
* Constructor method.
* @param {IBalanceSheetStatementData} reportData -
* @param {IBalanceSheetQuery} query -
*/
constructor(
reportData: IBalanceSheetStatementData,
query: IBalanceSheetQuery,
i18n: any
) {
super();
this.reportData = reportData;
this.query = new BalanceSheetQuery(query);
this.i18n = i18n;
}
/**
* Detarmines the node type of the given schema node.
* @param {IBalanceSheetStructureSection} node -
* @param {string} type -
* @return {boolean}
*/
protected isNodeType = R.curry(
(type: string, node: IBalanceSheetSchemaNode): boolean => {
return node.nodeType === type;
}
);
// -------------------------
// # Accessors.
// -------------------------
/**
* Retrieve the common columns for all report nodes.
* @param {ITableColumnAccessor[]}
*/
private commonColumnsAccessors = (): ITableColumnAccessor[] => {
return R.compose(
R.concat([{ key: 'name', accessor: 'name' }]),
R.ifElse(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
R.concat(this.datePeriodsColumnsAccessors()),
R.concat(this.totalColumnAccessor())
)
)([]);
};
/**
* Retrieve the total column accessor.
* @return {ITableColumnAccessor[]}
*/
private totalColumnAccessor = (): ITableColumnAccessor[] => {
return R.pipe(
R.concat(this.previousPeriodColumnAccessor()),
R.concat(this.previousYearColumnAccessor()),
R.concat(this.percentageColumnsAccessor()),
R.concat([{ key: 'total', accessor: 'total.formattedAmount' }])
)([]);
};
/**
* Retrieves the table row from the given report aggregate node.
* @param {IBalanceSheetAggregateNode} node
* @returns {ITableRow}
*/
private aggregateNodeTableRowsMapper = (
node: IBalanceSheetAggregateNode
): ITableRow => {
const columns = this.commonColumnsAccessors();
const meta = {
rowTypes: [IROW_TYPE.AGGREGATE],
id: node.id,
};
return tableRowMapper(node, columns, meta);
};
/**
* Retrieves the table row from the given report accounts node.
* @param {IBalanceSheetAccountsNode} node
* @returns {ITableRow}
*/
private accountsNodeTableRowsMapper = (
node: IBalanceSheetAccountsNode
): ITableRow => {
const columns = this.commonColumnsAccessors();
const meta = {
rowTypes: [IROW_TYPE.ACCOUNTS],
id: node.id,
};
return tableRowMapper(node, columns, meta);
};
/**
* Retrieves the table row from the given report account node.
* @param {IBalanceSheetAccountNode} node
* @returns {ITableRow}
*/
private accountNodeTableRowsMapper = (
node: IBalanceSheetAccountNode
): ITableRow => {
const columns = this.commonColumnsAccessors();
const meta = {
rowTypes: [IROW_TYPE.ACCOUNT],
id: node.id,
};
return tableRowMapper(node, columns, meta);
};
/**
* Retrieves the table row from the given report net income node.
* @param {IBalanceSheetNetIncomeNode} node
* @returns {ITableRow}
*/
private netIncomeNodeTableRowsMapper = (
node: IBalanceSheetNetIncomeNode
): ITableRow => {
const columns = this.commonColumnsAccessors();
const meta = {
rowTypes: [IROW_TYPE.NET_INCOME],
id: node.id,
};
return tableRowMapper(node, columns, meta);
};
/**
* Mappes the given report node to table rows.
* @param {IBalanceSheetDataNode} node -
* @returns {ITableRow}
*/
private nodeToTableRowsMapper = (node: IBalanceSheetDataNode): ITableRow => {
return R.cond([
[
this.isNodeType(BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE),
this.aggregateNodeTableRowsMapper,
],
[
this.isNodeType(BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS),
this.accountsNodeTableRowsMapper,
],
[
this.isNodeType(BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNT),
this.accountNodeTableRowsMapper,
],
[
this.isNodeType(BALANCE_SHEET_SCHEMA_NODE_TYPE.NET_INCOME),
this.netIncomeNodeTableRowsMapper,
],
])(node);
};
/**
* Mappes the given report sections to table rows.
* @param {IBalanceSheetDataNode[]} nodes -
* @return {ITableRow}
*/
private nodesToTableRowsMapper = (
nodes: IBalanceSheetDataNode[]
): ITableRow[] => {
return this.mapNodesDeep(nodes, this.nodeToTableRowsMapper);
};
/**
* Retrieves the total children columns.
* @returns {ITableColumn[]}
*/
private totalColumnChildren = (): ITableColumn[] => {
return R.compose(
R.unless(
R.isEmpty,
R.concat([{ key: 'total', label: this.i18n.__('balance_sheet.total') }])
),
R.concat(this.percentageColumns()),
R.concat(this.getPreviousYearColumns()),
R.concat(this.previousPeriodColumns())
)([]);
};
/**
* Retrieve the total column.
* @returns {ITableColumn[]}
*/
private totalColumn = (): ITableColumn[] => {
return [
{
key: 'total',
label: this.i18n.__('balance_sheet.total'),
children: this.totalColumnChildren(),
},
];
};
/**
* Retrieve the report table rows.
* @returns {ITableRow[]}
*/
public tableRows = (): ITableRow[] => {
return R.compose(
this.addTotalRows,
this.nodesToTableRowsMapper
)(this.reportData);
};
// -------------------------
// # Columns.
// -------------------------
/**
* Retrieve the report table columns.
* @returns {ITableColumn[]}
*/
public tableColumns = (): ITableColumn[] => {
return R.compose(
this.tableColumnsCellIndexing,
R.concat([
{ key: 'name', label: this.i18n.__('balance_sheet.account_name') },
]),
R.ifElse(
this.query.isDatePeriodsColumnsType,
R.concat(this.datePeriodsColumns()),
R.concat(this.totalColumn())
)
)([]);
};
}

View File

@@ -1,137 +0,0 @@
import * as R from 'ramda';
import moment from 'moment';
import {
ITableColumn,
IDateRange,
ICashFlowDateRange,
ITableColumnAccessor,
} from '@/interfaces';
import { FinancialDatePeriods } from '../FinancialDatePeriods';
export const BalanceSheetTableDatePeriods = (Base) =>
class extends R.compose(FinancialDatePeriods)(Base) {
/**
* Retrieves the date periods based on the report query.
* @returns {IDateRange[]}
*/
get datePeriods() {
return this.getDateRanges(
this.query.fromDate,
this.query.toDate,
this.query.displayColumnsBy
);
}
/**
* Retrieve the formatted column label from the given date range.
* @param {ICashFlowDateRange} dateRange -
* @return {string}
*/
private formatColumnLabel = (dateRange: ICashFlowDateRange) => {
const monthFormat = (range) => moment(range.toDate).format('YYYY-MM');
const yearFormat = (range) => moment(range.toDate).format('YYYY');
const dayFormat = (range) => moment(range.toDate).format('YYYY-MM-DD');
const conditions = [
['month', monthFormat],
['year', yearFormat],
['day', dayFormat],
['quarter', monthFormat],
['week', dayFormat],
];
const conditionsPairs = R.map(
([type, formatFn]) => [
R.always(this.query.isDisplayColumnsBy(type)),
formatFn,
],
conditions
);
return R.compose(R.cond(conditionsPairs))(dateRange);
};
// -------------------------
// # Accessors.
// -------------------------
/**
* Date period columns accessor.
* @param {IDateRange} dateRange -
* @param {number} index -
*/
private datePeriodColumnsAccessor = R.curry(
(dateRange: IDateRange, index: number) => {
return R.pipe(
R.concat(this.previousPeriodHorizColumnAccessors(index)),
R.concat(this.previousYearHorizontalColumnAccessors(index)),
R.concat(this.percetangeDatePeriodColumnsAccessor(index)),
R.concat([
{
key: `date-range-${index}`,
accessor: `horizontalTotals[${index}].total.formattedAmount`,
},
])
)([]);
}
);
/**
* Retrieve the date periods columns accessors.
* @returns {ITableColumnAccessor[]}
*/
protected datePeriodsColumnsAccessors = (): ITableColumnAccessor[] => {
return R.compose(
R.flatten,
R.addIndex(R.map)(this.datePeriodColumnsAccessor)
)(this.datePeriods);
};
// -------------------------
// # Columns.
// -------------------------
/**
*
* @param {number} index
* @param {} dateRange
* @returns {}
*/
private datePeriodChildrenColumns = (
index: number,
dateRange: IDateRange
) => {
return R.compose(
R.unless(
R.isEmpty,
R.concat([
{ key: `total`, label: this.i18n.__('balance_sheet.total') },
])
),
R.concat(this.percentageColumns()),
R.concat(this.getPreviousYearHorizontalColumns(dateRange)),
R.concat(this.previousPeriodHorizontalColumns(dateRange))
)([]);
};
/**
*
* @param dateRange
* @param index
* @returns
*/
private datePeriodColumn = (
dateRange: IDateRange,
index: number
): ITableColumn => {
return {
key: `date-range-${index}`,
label: this.formatColumnLabel(dateRange),
children: this.datePeriodChildrenColumns(index, dateRange),
};
};
/**
* Date periods columns.
* @returns {ITableColumn[]}
*/
protected datePeriodsColumns = (): ITableColumn[] => {
return this.datePeriods.map(this.datePeriodColumn);
};
};

View File

@@ -1,35 +0,0 @@
import { BalanceSheetInjectable } from './BalanceSheetInjectable';
import { BalanceSheetTable } from './BalanceSheetTable';
import { IBalanceSheetQuery, IBalanceSheetTable } from './BalanceSheet.types';
import { Injectable } from '@nestjs/common';
import { I18nService } from 'nestjs-i18n';
@Injectable()
export class BalanceSheetTableInjectable {
constructor(
private readonly balanceSheetService: BalanceSheetInjectable,
private readonly i18nService: I18nService,
) {}
/**
* Retrieves the balance sheet in table format.
* @param {number} tenantId
* @param {number} query
* @returns {Promise<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,
};
}
}

View File

@@ -1,83 +0,0 @@
import * as R from 'ramda';
import { ITableColumn } from '@/interfaces';
export const BalanceSheetTablePercentage = (Base) =>
class extends Base {
// --------------------
// # Columns
// --------------------
/**
* Retrieve percentage of column/row columns.
* @returns {ITableColumn[]}
*/
protected percentageColumns = (): ITableColumn[] => {
return R.pipe(
R.when(
this.query.isColumnsPercentageActive,
R.append({
key: 'percentage_of_column',
label: this.i18n.__('balance_sheet.percentage_of_column'),
})
),
R.when(
this.query.isRowsPercentageActive,
R.append({
key: 'percentage_of_row',
label: this.i18n.__('balance_sheet.percentage_of_row'),
})
)
)([]);
};
// --------------------
// # Accessors
// --------------------
/**
* Retrieves percentage of column/row accessors.
* @returns {ITableColumn[]}
*/
protected percentageColumnsAccessor = (): ITableColumn[] => {
return R.pipe(
R.when(
this.query.isColumnsPercentageActive,
R.append({
key: 'percentage_of_column',
accessor: 'percentageColumn.formattedAmount',
})
),
R.when(
this.query.isRowsPercentageActive,
R.append({
key: 'percentage_of_row',
accessor: 'percentageRow.formattedAmount',
})
)
)([]);
};
/**
* Percentage columns accessors for date period columns.
* @param {number} index
* @returns {ITableColumn[]}
*/
protected percetangeDatePeriodColumnsAccessor = (
index: number
): ITableColumn[] => {
return R.pipe(
R.when(
this.query.isColumnsPercentageActive,
R.append({
key: `percentage_of_column-${index}`,
accessor: `horizontalTotals[${index}].percentageColumn.formattedAmount`,
})
),
R.when(
this.query.isRowsPercentageActive,
R.append({
key: `percentage_of_row-${index}`,
accessor: `horizontalTotals[${index}].percentageRow.formattedAmount`,
})
)
)([]);
};
};

View File

@@ -1,109 +0,0 @@
import * as R from 'ramda';
import { IDateRange, ITableColumn } from '@/interfaces';
import { BalanceSheetQuery } from './BalanceSheetQuery';
import { FinancialTablePreviousPeriod } from '../FinancialTablePreviousPeriod';
import { FinancialDateRanges } from '../FinancialDateRanges';
export const BalanceSheetTablePreviousPeriod = (Base) =>
class extends R.compose(
FinancialTablePreviousPeriod,
FinancialDateRanges
)(Base) {
readonly query: BalanceSheetQuery;
// --------------------
// # Columns
// --------------------
/**
* Retrieves the previous period columns.
* @returns {ITableColumn[]}
*/
protected previousPeriodColumns = (
dateRange?: IDateRange
): ITableColumn[] => {
return R.pipe(
// Previous period columns.
R.when(
this.query.isPreviousPeriodActive,
R.append(this.getPreviousPeriodTotalColumn(dateRange))
),
R.when(
this.query.isPreviousPeriodChangeActive,
R.append(this.getPreviousPeriodChangeColumn())
),
R.when(
this.query.isPreviousPeriodPercentageActive,
R.append(this.getPreviousPeriodPercentageColumn())
)
)([]);
};
/**
* Previous period for date periods
* @param {IDateRange} dateRange
* @returns {ITableColumn}
*/
protected previousPeriodHorizontalColumns = (
dateRange: IDateRange
): ITableColumn[] => {
const PPDateRange = this.getPPDatePeriodDateRange(
dateRange.fromDate,
dateRange.toDate,
this.query.displayColumnsBy
);
return this.previousPeriodColumns({
fromDate: PPDateRange.fromDate,
toDate: PPDateRange.toDate,
});
};
// --------------------
// # Accessors
// --------------------
/**
* Retrieves previous period columns accessors.
* @returns {ITableColumn[]}
*/
protected previousPeriodColumnAccessor = (): ITableColumn[] => {
return R.pipe(
// Previous period columns.
R.when(
this.query.isPreviousPeriodActive,
R.append(this.getPreviousPeriodTotalAccessor())
),
R.when(
this.query.isPreviousPeriodChangeActive,
R.append(this.getPreviousPeriodChangeAccessor())
),
R.when(
this.query.isPreviousPeriodPercentageActive,
R.append(this.getPreviousPeriodPercentageAccessor())
)
)([]);
};
/**
*
* @param {number} index
* @returns
*/
protected previousPeriodHorizColumnAccessors = (
index: number
): ITableColumn[] => {
return R.pipe(
// Previous period columns.
R.when(
this.query.isPreviousPeriodActive,
R.append(this.getPreviousPeriodTotalHorizAccessor(index))
),
R.when(
this.query.isPreviousPeriodChangeActive,
R.append(this.getPreviousPeriodChangeHorizAccessor(index))
),
R.when(
this.query.isPreviousPeriodPercentageActive,
R.append(this.getPreviousPeriodPercentageHorizAccessor(index))
)
)([]);
};
};

View File

@@ -1,99 +0,0 @@
import * as R from 'ramda';
import { IDateRange, } from '../../types/Report.types';
import { ITableColumn } from '../../types/Table.types';
import { FinancialTablePreviousYear } from '../../common/FinancialTablePreviousYear';
import { FinancialDateRanges } from '../../common/FinancialDateRanges';
import { Constructor } from '@/common/types/Constructor';
export const BalanceSheetTablePreviousYear = <T extends Constructor>(Base: T) =>
class extends R.compose(FinancialTablePreviousYear, FinancialDateRanges)(Base) {
// --------------------
// # Columns.
// --------------------
/**
* Retrieves pervious year comparison columns.
* @returns {ITableColumn[]}
*/
public getPreviousYearColumns = (
dateRange?: IDateRange
): ITableColumn[] => {
return R.pipe(
// Previous year columns.
R.when(
this.query.isPreviousYearActive,
R.append(this.getPreviousYearTotalColumn(dateRange))
),
R.when(
this.query.isPreviousYearChangeActive,
R.append(this.getPreviousYearChangeColumn())
),
R.when(
this.query.isPreviousYearPercentageActive,
R.append(this.getPreviousYearPercentageColumn())
)
)([]);
};
/**
*
* @param {IDateRange} dateRange
* @returns
*/
public getPreviousYearHorizontalColumns = (dateRange: IDateRange) => {
const PYDateRange = this.getPreviousYearDateRange(
dateRange.fromDate,
dateRange.toDate
);
return this.getPreviousYearColumns(PYDateRange);
};
// --------------------
// # Accessors.
// --------------------
/**
* Retrieves previous year columns accessors.
* @returns {ITableColumn[]}
*/
public previousYearColumnAccessor = (): ITableColumn[] => {
return R.pipe(
// Previous year columns.
R.when(
this.query.isPreviousYearActive,
R.append(this.getPreviousYearTotalAccessor())
),
R.when(
this.query.isPreviousYearChangeActive,
R.append(this.getPreviousYearChangeAccessor())
),
R.when(
this.query.isPreviousYearPercentageActive,
R.append(this.getPreviousYearPercentageAccessor())
)
)([]);
};
/**
* Previous year period column accessor.
* @param {number} index
* @returns {ITableColumn[]}
*/
public previousYearHorizontalColumnAccessors = (
index: number
): ITableColumn[] => {
return R.pipe(
// Previous year columns.
R.when(
this.query.isPreviousYearActive,
R.append(this.getPreviousYearTotalHorizAccessor(index))
),
R.when(
this.query.isPreviousYearChangeActive,
R.append(this.getPreviousYearChangeHorizAccessor(index))
),
R.when(
this.query.isPreviousYearPercentageActive,
R.append(this.getPreviousYearPercentageHorizAccessor(index))
)
)([]);
};
};

View File

@@ -1,3 +0,0 @@
import * as R from 'ramda';
export const BalanceSheetTotal = (Base: any) => class extends Base {};

View File

@@ -1,64 +0,0 @@
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;
}
`;

View File

@@ -1,703 +0,0 @@
import * as R from 'ramda';
import { defaultTo, map, set, sumBy, isEmpty, mapValues, get } from 'lodash';
import * as mathjs from 'mathjs';
import moment from 'moment';
import { compose } from 'lodash/fp';
import {
IAccount,
ILedger,
INumberFormatQuery,
ICashFlowSchemaSection,
ICashFlowStatementQuery,
ICashFlowStatementNetIncomeSection,
ICashFlowStatementAccountSection,
ICashFlowSchemaSectionAccounts,
ICashFlowStatementAccountMeta,
ICashFlowSchemaAccountRelation,
ICashFlowStatementSectionType,
ICashFlowStatementData,
ICashFlowSchemaTotalSection,
ICashFlowStatementTotalSection,
ICashFlowStatementSection,
ICashFlowCashBeginningNode,
ICashFlowStatementAggregateSection,
} from '@/interfaces';
import CASH_FLOW_SCHEMA from './schema';
import FinancialSheet from '../FinancialSheet';
import { transformToMapBy, accumSum } from 'utils';
import { ACCOUNT_ROOT_TYPE } from '@/data/AccountTypes';
import { CashFlowStatementDatePeriods } from './CashFlowDatePeriods';
import I18nService from '@/services/I18n/I18nService';
import { DISPLAY_COLUMNS_BY } from './constants';
import { FinancialSheetStructure } from '../FinancialSheetStructure';
export default class CashFlowStatement extends compose(
CashFlowStatementDatePeriods,
FinancialSheetStructure
)(FinancialSheet) {
readonly baseCurrency: string;
readonly i18n: I18nService;
readonly sectionsByIds = {};
readonly cashFlowSchemaMap: Map<string, ICashFlowSchemaSection>;
readonly cashFlowSchemaSeq: Array<string>;
readonly accountByTypeMap: Map<string, IAccount[]>;
readonly accountsByRootType: Map<string, IAccount[]>;
readonly ledger: ILedger;
readonly cashLedger: ILedger;
readonly netIncomeLedger: ILedger;
readonly query: ICashFlowStatementQuery;
readonly numberFormat: INumberFormatQuery;
readonly comparatorDateType: string;
readonly dateRangeSet: { fromDate: Date; toDate: Date }[];
/**
* Constructor method.
* @constructor
*/
constructor(
accounts: IAccount[],
ledger: ILedger,
cashLedger: ILedger,
netIncomeLedger: ILedger,
query: ICashFlowStatementQuery,
baseCurrency: string,
i18n
) {
super();
this.baseCurrency = baseCurrency;
this.i18n = i18n;
this.ledger = ledger;
this.cashLedger = cashLedger;
this.netIncomeLedger = netIncomeLedger;
this.accountByTypeMap = transformToMapBy(accounts, 'accountType');
this.accountsByRootType = transformToMapBy(accounts, 'accountRootType');
this.query = query;
this.numberFormat = this.query.numberFormat;
this.dateRangeSet = [];
this.comparatorDateType =
query.displayColumnsType === 'total' ? 'day' : query.displayColumnsBy;
this.initDateRangeCollection();
}
// --------------------------------------------
// # GENERAL UTILITIES
// --------------------------------------------
/**
* Retrieve the expense accounts ids.
* @return {number[]}
*/
private getAccountsIdsByType = (accountType: string): number[] => {
const expenseAccounts = this.accountsByRootType.get(accountType);
const expenseAccountsIds = map(expenseAccounts, 'id');
return expenseAccountsIds;
};
/**
* Detarmines the given display columns by type.
* @param {string} displayColumnsBy
* @returns {boolean}
*/
private isDisplayColumnsBy = (displayColumnsBy: string): boolean => {
return this.query.displayColumnsType === displayColumnsBy;
};
/**
* Adjustments the given amount.
* @param {string} direction
* @param {number} amount -
* @return {number}
*/
private amountAdjustment = (direction: 'mines' | 'plus', amount): number => {
return R.when(
R.always(R.equals(direction, 'mines')),
R.multiply(-1)
)(amount);
};
// --------------------------------------------
// # NET INCOME NODE
// --------------------------------------------
/**
* Retrieve the accounts net income.
* @returns {number} - Amount of net income.
*/
private getAccountsNetIncome(): number {
// Mapping income/expense accounts ids.
const incomeAccountsIds = this.getAccountsIdsByType(
ACCOUNT_ROOT_TYPE.INCOME
);
const expenseAccountsIds = this.getAccountsIdsByType(
ACCOUNT_ROOT_TYPE.EXPENSE
);
// Income closing balance.
const incomeClosingBalance = accumSum(incomeAccountsIds, (id) =>
this.netIncomeLedger.whereAccountId(id).getClosingBalance()
);
// Expense closing balance.
const expenseClosingBalance = accumSum(expenseAccountsIds, (id) =>
this.netIncomeLedger.whereAccountId(id).getClosingBalance()
);
// Net income = income - expenses.
const netIncome = incomeClosingBalance - expenseClosingBalance;
return netIncome;
}
/**
* Parses the net income section from the given section schema.
* @param {ICashFlowSchemaSection} nodeSchema - Report section schema.
* @returns {ICashFlowStatementNetIncomeSection}
*/
private netIncomeSectionMapper = (
nodeSchema: ICashFlowSchemaSection
): ICashFlowStatementNetIncomeSection => {
const netIncome = this.getAccountsNetIncome();
const node = {
id: nodeSchema.id,
label: this.i18n.__(nodeSchema.label),
total: this.getAmountMeta(netIncome),
sectionType: ICashFlowStatementSectionType.NET_INCOME,
};
return R.compose(
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocPeriodsToNetIncomeNode
)
)(node);
};
// --------------------------------------------
// # ACCOUNT NODE
// --------------------------------------------
/**
* Retrieve account meta.
* @param {ICashFlowSchemaAccountRelation} relation - Account relation.
* @param {IAccount} account -
* @returns {ICashFlowStatementAccountMeta}
*/
private accountMetaMapper = (
relation: ICashFlowSchemaAccountRelation,
account: IAccount
): ICashFlowStatementAccountMeta => {
// Retrieve the closing balance of the given account.
const getClosingBalance = (id) =>
this.ledger.whereAccountId(id).getClosingBalance();
const closingBalance = R.compose(
// Multiplies the amount by -1 in case the relation in mines.
R.curry(this.amountAdjustment)(relation.direction)
)(getClosingBalance(account.id));
const node = {
id: account.id,
code: account.code,
label: account.name,
accountType: account.accountType,
adjustmentType: relation.direction,
total: this.getAmountMeta(closingBalance),
sectionType: ICashFlowStatementSectionType.ACCOUNT,
};
return R.compose(
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocPeriodsToAccountNode
)
)(node);
};
/**
* Retrieve accounts sections by the given schema relation.
* @param {ICashFlowSchemaAccountRelation} relation
* @returns {ICashFlowStatementAccountMeta[]}
*/
private getAccountsBySchemaRelation = (
relation: ICashFlowSchemaAccountRelation
): ICashFlowStatementAccountMeta[] => {
const accounts = defaultTo(this.accountByTypeMap.get(relation.type), []);
const accountMetaMapper = R.curry(this.accountMetaMapper)(relation);
return R.map(accountMetaMapper)(accounts);
};
/**
* Retrieve the accounts meta.
* @param {string[]} types
* @returns {ICashFlowStatementAccountMeta[]}
*/
private getAccountsBySchemaRelations = (
relations: ICashFlowSchemaAccountRelation[]
): ICashFlowStatementAccountMeta[] => {
return R.pipe(
R.append(R.map(this.getAccountsBySchemaRelation)(relations)),
R.flatten
)([]);
};
/**
* Calculates the accounts total
* @param {ICashFlowStatementAccountMeta[]} accounts
* @returns {number}
*/
private getAccountsMetaTotal = (
accounts: ICashFlowStatementAccountMeta[]
): number => {
return sumBy(accounts, 'total.amount');
};
/**
* Retrieve the accounts section from the section schema.
* @param {ICashFlowSchemaSectionAccounts} sectionSchema
* @returns {ICashFlowStatementAccountSection}
*/
private accountsSectionParser = (
sectionSchema: ICashFlowSchemaSectionAccounts
): ICashFlowStatementAccountSection => {
const { accountsRelations } = sectionSchema;
const accounts = this.getAccountsBySchemaRelations(accountsRelations);
const accountsTotal = this.getAccountsMetaTotal(accounts);
const total = this.getTotalAmountMeta(accountsTotal);
const node = {
sectionType: ICashFlowStatementSectionType.ACCOUNTS,
id: sectionSchema.id,
label: this.i18n.__(sectionSchema.label),
footerLabel: this.i18n.__(sectionSchema.footerLabel),
children: accounts,
total,
};
return R.compose(
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocPeriodsToAggregateNode
)
)(node);
};
/**
* Detarmines the schema section type.
* @param {string} type
* @param {ICashFlowSchemaSection} section
* @returns {boolean}
*/
private isSchemaSectionType = R.curry(
(type: string, section: ICashFlowSchemaSection): boolean => {
return type === section.sectionType;
}
);
// --------------------------------------------
// # AGGREGATE NODE
// --------------------------------------------
/**
* Aggregate schema node parser to aggregate report node.
* @param {ICashFlowSchemaSection} schemaSection
* @returns {ICashFlowStatementAggregateSection}
*/
private regularSectionParser = R.curry(
(
children,
schemaSection: ICashFlowSchemaSection
): ICashFlowStatementAggregateSection => {
const node = {
id: schemaSection.id,
label: this.i18n.__(schemaSection.label),
footerLabel: this.i18n.__(schemaSection.footerLabel),
sectionType: ICashFlowStatementSectionType.AGGREGATE,
children,
};
return R.compose(
R.when(
this.isSchemaSectionType(ICashFlowStatementSectionType.AGGREGATE),
this.assocRegularSectionTotal
),
R.when(
this.isSchemaSectionType(ICashFlowStatementSectionType.AGGREGATE),
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocPeriodsToAggregateNode
)
)
)(node);
}
);
private transformSectionsToMap = (sections: ICashFlowSchemaSection[]) => {
return this.reduceNodesDeep(
sections,
(acc, section) => {
if (section.id) {
acc[`${section.id}`] = section;
}
return acc;
},
{}
);
};
// --------------------------------------------
// # TOTAL EQUATION NODE
// --------------------------------------------
private sectionsMapToTotal = (mappedSections: { [key: number]: any }) => {
return mapValues(mappedSections, (node) => get(node, 'total.amount') || 0);
};
/**
* Evauluate equaation string with the given scope table.
* @param {string} equation -
* @param {{ [key: string]: number }} scope -
* @return {number}
*/
private evaluateEquation = (
equation: string,
scope: { [key: string | number]: number }
): number => {
return mathjs.evaluate(equation, scope);
};
/**
* Retrieve the total section from the eqauation parser.
* @param {ICashFlowSchemaTotalSection} sectionSchema
* @param {ICashFlowSchemaSection[]} accumulatedSections
* @returns {ICashFlowStatementTotalSection}
*/
private totalEquationSectionParser = (
accumulatedSections: ICashFlowSchemaSection[],
sectionSchema: ICashFlowSchemaTotalSection
): ICashFlowStatementTotalSection => {
const mappedSectionsById = this.transformSectionsToMap(accumulatedSections);
const nodesTotalById = this.sectionsMapToTotal(mappedSectionsById);
const total = this.evaluateEquation(sectionSchema.equation, nodesTotalById);
return R.compose(
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
R.curry(this.assocTotalEquationDatePeriods)(
mappedSectionsById,
sectionSchema.equation
)
)
)({
sectionType: ICashFlowStatementSectionType.TOTAL,
id: sectionSchema.id,
label: this.i18n.__(sectionSchema.label),
total: this.getTotalAmountMeta(total),
});
};
/**
* Retrieve the beginning cash from date.
* @param {Date|string} fromDate -
* @return {Date}
*/
private beginningCashFrom = (fromDate: string | Date): Date => {
return moment(fromDate).subtract(1, 'days').toDate();
};
/**
* Retrieve account meta.
* @param {ICashFlowSchemaAccountRelation} relation
* @param {IAccount} account
* @returns {ICashFlowStatementAccountMeta}
*/
private cashAccountMetaMapper = (
relation: ICashFlowSchemaAccountRelation,
account: IAccount
): ICashFlowStatementAccountMeta => {
const cashToDate = this.beginningCashFrom(this.query.fromDate);
const closingBalance = this.cashLedger
.whereToDate(cashToDate)
.whereAccountId(account.id)
.getClosingBalance();
const node = {
id: account.id,
code: account.code,
label: account.name,
accountType: account.accountType,
adjustmentType: relation.direction,
total: this.getAmountMeta(closingBalance),
sectionType: ICashFlowStatementSectionType.ACCOUNT,
};
return R.compose(
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocCashAtBeginningAccountDatePeriods
)
)(node);
};
/**
* Retrieve accounts sections by the given schema relation.
* @param {ICashFlowSchemaAccountRelation} relation
* @returns {ICashFlowStatementAccountMeta[]}
*/
private getCashAccountsBySchemaRelation = (
relation: ICashFlowSchemaAccountRelation
): ICashFlowStatementAccountMeta[] => {
const accounts = this.accountByTypeMap.get(relation.type) || [];
const accountMetaMapper = R.curry(this.cashAccountMetaMapper)(relation);
return accounts.map(accountMetaMapper);
};
/**
* Retrieve the accounts meta.
* @param {string[]} types
* @returns {ICashFlowStatementAccountMeta[]}
*/
private getCashAccountsBySchemaRelations = (
relations: ICashFlowSchemaAccountRelation[]
): ICashFlowStatementAccountMeta[] => {
return R.concat(...R.map(this.getCashAccountsBySchemaRelation)(relations));
};
/**
* Parses the cash at beginning section.
* @param {ICashFlowSchemaTotalSection} sectionSchema -
* @return {ICashFlowCashBeginningNode}
*/
private cashAtBeginningSectionParser = (
nodeSchema: ICashFlowSchemaSection
): ICashFlowCashBeginningNode => {
const { accountsRelations } = nodeSchema;
const children = this.getCashAccountsBySchemaRelations(accountsRelations);
const total = this.getAccountsMetaTotal(children);
const node = {
sectionType: ICashFlowStatementSectionType.CASH_AT_BEGINNING,
id: nodeSchema.id,
label: this.i18n.__(nodeSchema.label),
children,
total: this.getTotalAmountMeta(total),
};
return R.compose(
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocCashAtBeginningDatePeriods
)
)(node);
};
/**
* Parses the schema section.
* @param {ICashFlowSchemaSection} schemaNode
* @returns {ICashFlowSchemaSection}
*/
private schemaSectionParser = (
schemaNode: ICashFlowSchemaSection,
children
): ICashFlowSchemaSection | ICashFlowStatementSection => {
return R.compose(
// Accounts node.
R.when(
this.isSchemaSectionType(ICashFlowStatementSectionType.ACCOUNTS),
this.accountsSectionParser
),
// Net income node.
R.when(
this.isSchemaSectionType(ICashFlowStatementSectionType.NET_INCOME),
this.netIncomeSectionMapper
),
// Cash at beginning node.
R.when(
this.isSchemaSectionType(
ICashFlowStatementSectionType.CASH_AT_BEGINNING
),
this.cashAtBeginningSectionParser
),
// Aggregate node. (that has no section type).
R.when(
this.isSchemaSectionType(ICashFlowStatementSectionType.AGGREGATE),
this.regularSectionParser(children)
)
)(schemaNode);
};
/**
* Parses the schema section.
* @param {ICashFlowSchemaSection | ICashFlowStatementSection} section
* @param {number} key
* @param {ICashFlowSchemaSection[]} parentValue
* @param {(ICashFlowSchemaSection | ICashFlowStatementSection)[]} accumulatedSections
* @returns {ICashFlowSchemaSection}
*/
private schemaSectionTotalParser = (
section: ICashFlowSchemaSection | ICashFlowStatementSection,
key: number,
parentValue: ICashFlowSchemaSection[],
context,
accumulatedSections: (ICashFlowSchemaSection | ICashFlowStatementSection)[]
): ICashFlowSchemaSection | ICashFlowStatementSection => {
return R.compose(
// Total equation section.
R.when(
this.isSchemaSectionType(ICashFlowStatementSectionType.TOTAL),
R.curry(this.totalEquationSectionParser)(accumulatedSections)
)
)(section);
};
/**
* Schema sections parser.
* @param {ICashFlowSchemaSection[]}schema
* @returns {ICashFlowStatementSection[]}
*/
private schemaSectionsParser = (
schema: ICashFlowSchemaSection[]
): ICashFlowStatementSection[] => {
return this.mapNodesDeepReverse(schema, this.schemaSectionParser);
};
/**
* Writes the `total` property to the aggregate node.
* @param {ICashFlowStatementSection} section
* @return {ICashFlowStatementSection}
*/
private assocRegularSectionTotal = (section: ICashFlowStatementSection) => {
const total = this.getAccountsMetaTotal(section.children);
return R.assoc('total', this.getTotalAmountMeta(total), section);
};
/**
* Parses total schema nodes.
* @param {(ICashFlowSchemaSection | ICashFlowStatementSection)[]} sections
* @returns {(ICashFlowSchemaSection | ICashFlowStatementSection)[]}
*/
private totalSectionsParser = (
sections: (ICashFlowSchemaSection | ICashFlowStatementSection)[]
): (ICashFlowSchemaSection | ICashFlowStatementSection)[] => {
return this.reduceNodesDeep(
sections,
(acc, value, key, parentValue, context) => {
set(
acc,
context.path,
this.schemaSectionTotalParser(value, key, parentValue, context, acc)
);
return acc;
},
[]
);
};
// --------------------------------------------
// REPORT FILTERING
// --------------------------------------------
/**
* Detarmines the given section has children and not empty.
* @param {ICashFlowStatementSection} section
* @returns {boolean}
*/
private isSectionHasChildren = (
section: ICashFlowStatementSection
): boolean => {
return !isEmpty(section.children);
};
/**
* Detarmines whether the section has no zero amount.
* @param {ICashFlowStatementSection} section
* @returns {boolean}
*/
private isSectionNoneZero = (section: ICashFlowStatementSection): boolean => {
return section.total.amount !== 0;
};
/**
* Detarmines whether the parent accounts sections has children.
* @param {ICashFlowStatementSection} section
* @returns {boolean}
*/
private isAccountsSectionHasChildren = (
section: ICashFlowStatementSection[]
): boolean => {
return R.ifElse(
this.isSchemaSectionType(ICashFlowStatementSectionType.ACCOUNTS),
this.isSectionHasChildren,
R.always(true)
)(section);
};
/**
* Detarmines the account section has no zero otherwise returns true.
* @param {ICashFlowStatementSection} section
* @returns {boolean}
*/
private isAccountLeafNoneZero = (
section: ICashFlowStatementSection[]
): boolean => {
return R.ifElse(
this.isSchemaSectionType(ICashFlowStatementSectionType.ACCOUNT),
this.isSectionNoneZero,
R.always(true)
)(section);
};
/**
* Deep filters the non-zero accounts leafs of the report sections.
* @param {ICashFlowStatementSection[]} sections
* @returns {ICashFlowStatementSection[]}
*/
private filterNoneZeroAccountsLeafs = (
sections: ICashFlowStatementSection[]
): ICashFlowStatementSection[] => {
return this.filterNodesDeep(sections, this.isAccountLeafNoneZero);
};
/**
* Deep filter the non-children sections of the report sections.
* @param {ICashFlowStatementSection[]} sections
* @returns {ICashFlowStatementSection[]}
*/
private filterNoneChildrenSections = (
sections: ICashFlowStatementSection[]
): ICashFlowStatementSection[] => {
return this.filterNodesDeep(sections, this.isAccountsSectionHasChildren);
};
/**
* Filters the report data.
* @param {ICashFlowStatementSection[]} sections
* @returns {ICashFlowStatementSection[]}
*/
private filterReportData = (
sections: ICashFlowStatementSection[]
): ICashFlowStatementSection[] => {
return R.compose(
this.filterNoneChildrenSections,
this.filterNoneZeroAccountsLeafs
)(sections);
};
/**
* Schema parser.
* @param {ICashFlowSchemaSection[]} schema
* @returns {ICashFlowSchemaSection[]}
*/
private schemaParser = (
schema: ICashFlowSchemaSection[]
): ICashFlowSchemaSection[] => {
return R.compose(
R.when(
R.always(this.query.noneTransactions || this.query.noneZero),
this.filterReportData
),
this.totalSectionsParser,
this.schemaSectionsParser
)(schema);
};
/**
* Retrieve the cashflow statement data.
* @return {ICashFlowStatementData}
*/
public reportData = (): ICashFlowStatementData => {
return this.schemaParser(R.clone(CASH_FLOW_SCHEMA));
};
}

View File

@@ -1,411 +0,0 @@
import * as R from 'ramda';
import { sumBy, mapValues, get } from 'lodash';
import { ACCOUNT_ROOT_TYPE } from '@/data/AccountTypes';
import { accumSum, dateRangeFromToCollection } from 'utils';
import {
ICashFlowDatePeriod,
ICashFlowStatementNetIncomeSection,
ICashFlowStatementAccountSection,
ICashFlowStatementSection,
ICashFlowSchemaTotalSection,
IFormatNumberSettings,
ICashFlowStatementTotalSection,
IDateRange,
ICashFlowStatementQuery,
} from '@/interfaces';
export const CashFlowStatementDatePeriods = (Base) =>
class extends Base {
dateRangeSet: IDateRange[];
query: ICashFlowStatementQuery;
/**
* Initialize date range set.
*/
private initDateRangeCollection() {
this.dateRangeSet = dateRangeFromToCollection(
this.query.fromDate,
this.query.toDate,
this.comparatorDateType
);
}
/**
* Retrieve the date period meta.
* @param {number} total - Total amount.
* @param {Date} fromDate - From date.
* @param {Date} toDate - To date.
* @return {ICashFlowDatePeriod}
*/
private getDatePeriodTotalMeta = (
total: number,
fromDate: Date,
toDate: Date,
overrideSettings: IFormatNumberSettings = {}
): ICashFlowDatePeriod => {
return this.getDatePeriodMeta(total, fromDate, toDate, {
money: true,
...overrideSettings,
});
};
/**
* Retrieve the date period meta.
* @param {number} total - Total amount.
* @param {Date} fromDate - From date.
* @param {Date} toDate - To date.
* @return {ICashFlowDatePeriod}
*/
private getDatePeriodMeta = (
total: number,
fromDate: Date,
toDate: Date,
overrideSettings?: IFormatNumberSettings
): ICashFlowDatePeriod => {
return {
fromDate: this.getDateMeta(fromDate),
toDate: this.getDateMeta(toDate),
total: this.getAmountMeta(total, overrideSettings),
};
};
// Net income --------------------
/**
* Retrieve the net income between the given date range.
* @param {Date} fromDate
* @param {Date} toDate
* @returns {number}
*/
private getNetIncomeDateRange = (fromDate: Date, toDate: Date) => {
// Mapping income/expense accounts ids.
const incomeAccountsIds = this.getAccountsIdsByType(
ACCOUNT_ROOT_TYPE.INCOME
);
const expenseAccountsIds = this.getAccountsIdsByType(
ACCOUNT_ROOT_TYPE.EXPENSE
);
// Income closing balance.
const incomeClosingBalance = accumSum(incomeAccountsIds, (id) =>
this.netIncomeLedger
.whereFromDate(fromDate)
.whereToDate(toDate)
.whereAccountId(id)
.getClosingBalance()
);
// Expense closing balance.
const expenseClosingBalance = accumSum(expenseAccountsIds, (id) =>
this.netIncomeLedger
.whereToDate(toDate)
.whereFromDate(fromDate)
.whereAccountId(id)
.getClosingBalance()
);
// Net income = income - expenses.
const netIncome = incomeClosingBalance - expenseClosingBalance;
return netIncome;
};
/**
* Retrieve the net income of date period.
* @param {IDateRange} dateRange -
* @retrun {ICashFlowDatePeriod}
*/
private getNetIncomeDatePeriod = (dateRange): ICashFlowDatePeriod => {
const total = this.getNetIncomeDateRange(
dateRange.fromDate,
dateRange.toDate
);
return this.getDatePeriodMeta(
total,
dateRange.fromDate,
dateRange.toDate
);
};
/**
* Retrieve the net income node between the given date ranges.
* @param {Date} fromDate
* @param {Date} toDate
* @returns {ICashFlowDatePeriod[]}
*/
private getNetIncomeDatePeriods = (
section: ICashFlowStatementNetIncomeSection
): ICashFlowDatePeriod[] => {
return this.dateRangeSet.map(this.getNetIncomeDatePeriod.bind(this));
};
/**
* Writes periods property to net income section.
* @param {ICashFlowStatementNetIncomeSection} section
* @returns {ICashFlowStatementNetIncomeSection}
*/
protected assocPeriodsToNetIncomeNode = (
section: ICashFlowStatementNetIncomeSection
): ICashFlowStatementNetIncomeSection => {
const incomeDatePeriods = this.getNetIncomeDatePeriods(section);
return R.assoc('periods', incomeDatePeriods, section);
};
// Account nodes --------------------
/**
* Retrieve the account total between date range.
* @param {Date} fromDate - From date.
* @param {Date} toDate - To date.
* @return {number}
*/
private getAccountTotalDateRange = (
node: ICashFlowStatementAccountSection,
fromDate: Date,
toDate: Date
): number => {
const closingBalance = this.ledger
.whereFromDate(fromDate)
.whereToDate(toDate)
.whereAccountId(node.id)
.getClosingBalance();
return this.amountAdjustment(node.adjustmentType, closingBalance);
};
/**
* Retrieve the given account node total date period.
* @param {ICashFlowStatementAccountSection} node -
* @param {Date} fromDate - From date.
* @param {Date} toDate - To date.
* @return {ICashFlowDatePeriod}
*/
private getAccountTotalDatePeriod = (
node: ICashFlowStatementAccountSection,
fromDate: Date,
toDate: Date
): ICashFlowDatePeriod => {
const total = this.getAccountTotalDateRange(node, fromDate, toDate);
return this.getDatePeriodMeta(total, fromDate, toDate);
};
/**
* Retrieve the accounts date periods nodes of the give account node.
* @param {ICashFlowStatementAccountSection} node -
* @return {ICashFlowDatePeriod[]}
*/
private getAccountDatePeriods = (
node: ICashFlowStatementAccountSection
): ICashFlowDatePeriod[] => {
return this.getNodeDatePeriods(
node,
this.getAccountTotalDatePeriod.bind(this)
);
}
/**
* Writes `periods` property to account node.
* @param {ICashFlowStatementAccountSection} node -
* @return {ICashFlowStatementAccountSection}
*/
protected assocPeriodsToAccountNode = (
node: ICashFlowStatementAccountSection
): ICashFlowStatementAccountSection => {
const datePeriods = this.getAccountDatePeriods(node);
return R.assoc('periods', datePeriods, node);
}
// Aggregate node -------------------------
/**
* Retrieve total of the given period index for node that has children nodes.
* @return {number}
*/
private getChildrenTotalPeriodByIndex = (
node: ICashFlowStatementSection,
index: number
): number => {
return sumBy(node.children, `periods[${index}].total.amount`);
}
/**
* Retrieve date period meta of the given node index.
* @param {ICashFlowStatementSection} node -
* @param {number} index - Loop index.
* @param {Date} fromDate - From date.
* @param {Date} toDate - To date.
*/
private getChildrenTotalPeriodMetaByIndex(
node: ICashFlowStatementSection,
index: number,
fromDate: Date,
toDate: Date
) {
const total = this.getChildrenTotalPeriodByIndex(node, index);
return this.getDatePeriodTotalMeta(total, fromDate, toDate);
}
/**
* Retrieve the date periods of aggregate node.
* @param {ICashFlowStatementSection} node
*/
private getAggregateNodeDatePeriods(node: ICashFlowStatementSection) {
const getChildrenTotalPeriodMetaByIndex = R.curry(
this.getChildrenTotalPeriodMetaByIndex.bind(this)
)(node);
return this.dateRangeSet.map((dateRange, index) =>
getChildrenTotalPeriodMetaByIndex(
index,
dateRange.fromDate,
dateRange.toDate
)
);
}
/**
* Writes `periods` property to aggregate section node.
* @param {ICashFlowStatementSection} node -
* @return {ICashFlowStatementSection}
*/
protected assocPeriodsToAggregateNode = (
node: ICashFlowStatementSection
): ICashFlowStatementSection => {
const datePeriods = this.getAggregateNodeDatePeriods(node);
return R.assoc('periods', datePeriods, node);
};
// Total equation node --------------------
private sectionsMapToTotalPeriod = (
mappedSections: { [key: number]: any },
index
) => {
return mapValues(
mappedSections,
(node) => get(node, `periods[${index}].total.amount`) || 0
);
};
/**
* Retrieve the date periods of the given total equation.
* @param {ICashFlowSchemaTotalSection}
* @param {string} equation -
* @return {ICashFlowDatePeriod[]}
*/
private getTotalEquationDatePeriods = (
node: ICashFlowSchemaTotalSection,
equation: string,
nodesTable
): ICashFlowDatePeriod[] => {
return this.getNodeDatePeriods(node, (node, fromDate, toDate, index) => {
const periodScope = this.sectionsMapToTotalPeriod(nodesTable, index);
const total = this.evaluateEquation(equation, periodScope);
return this.getDatePeriodTotalMeta(total, fromDate, toDate);
});
};
/**
* Associates the total periods of total equation to the ginve total node..
* @param {ICashFlowSchemaTotalSection} totalSection -
* @return {ICashFlowStatementTotalSection}
*/
protected assocTotalEquationDatePeriods = (
nodesTable: any,
equation: string,
node: ICashFlowSchemaTotalSection
): ICashFlowStatementTotalSection => {
const datePeriods = this.getTotalEquationDatePeriods(
node,
equation,
nodesTable
);
return R.assoc('periods', datePeriods, node);
};
// Cash at beginning ----------------------
/**
* Retrieve the date preioods of the given node and accumulated function.
* @param {} node
* @param {}
* @return {}
*/
private getNodeDatePeriods = (node, callback) => {
const curriedCallback = R.curry(callback)(node);
return this.dateRangeSet.map((dateRange, index) => {
return curriedCallback(dateRange.fromDate, dateRange.toDate, index);
});
};
/**
* Retrieve the account total between date range.
* @param {Date} fromDate - From date.
* @param {Date} toDate - To date.
* @return {number}
*/
private getBeginningCashAccountDateRange = (
node: ICashFlowStatementSection,
fromDate: Date,
toDate: Date
) => {
const cashToDate = this.beginningCashFrom(fromDate);
return this.cashLedger
.whereToDate(cashToDate)
.whereAccountId(node.id)
.getClosingBalance();
};
/**
* Retrieve the beginning cash date period.
* @param {ICashFlowStatementSection} node -
* @param {Date} fromDate - From date.
* @param {Date} toDate - To date.
* @return {ICashFlowDatePeriod}
*/
private getBeginningCashDatePeriod = (
node: ICashFlowStatementSection,
fromDate: Date,
toDate: Date
) => {
const total = this.getBeginningCashAccountDateRange(
node,
fromDate,
toDate
);
return this.getDatePeriodTotalMeta(total, fromDate, toDate);
};
/**
* Retrieve the beginning cash account periods.
* @param {ICashFlowStatementSection} node
* @return {ICashFlowDatePeriod}
*/
private getBeginningCashAccountPeriods = (
node: ICashFlowStatementSection
): ICashFlowDatePeriod => {
return this.getNodeDatePeriods(node, this.getBeginningCashDatePeriod);
};
/**
* Writes `periods` property to cash at beginning date periods.
* @param {ICashFlowStatementSection} section -
* @return {ICashFlowStatementSection}
*/
protected assocCashAtBeginningDatePeriods = (
node: ICashFlowStatementSection
): ICashFlowStatementSection => {
const datePeriods = this.getAggregateNodeDatePeriods(node);
return R.assoc('periods', datePeriods, node);
};
/**
* Associates `periods` propery to cash at beginning account node.
* @param {ICashFlowStatementSection} node -
* @return {ICashFlowStatementSection}
*/
protected assocCashAtBeginningAccountDatePeriods = (
node: ICashFlowStatementSection
): ICashFlowStatementSection => {
const datePeriods = this.getBeginningCashAccountPeriods(node);
return R.assoc('periods', datePeriods, node);
};
};

View File

@@ -1,173 +0,0 @@
import { Inject, Service } from 'typedi';
import moment from 'moment';
import { Knex } from 'knex';
import { isEmpty } from 'lodash';
import {
ICashFlowStatementQuery,
IAccountTransaction,
IAccount,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export default class CashFlowRepository {
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve the group type from periods type.
* @param {string} displayType
* @returns {string}
*/
protected getGroupTypeFromPeriodsType(displayType: string) {
const displayTypes = {
year: 'year',
day: 'day',
month: 'month',
quarter: 'month',
week: 'day',
};
return displayTypes[displayType] || 'month';
}
/**
* Retrieve the cashflow accounts.
* @returns {Promise<IAccount[]>}
*/
public async cashFlowAccounts(tenantId: number): Promise<IAccount[]> {
const { Account } = this.tenancy.models(tenantId);
const accounts = await Account.query();
return accounts;
}
/**
* Retrieve total of csah at beginning transactions.
* @param {number} tenantId -
* @param {ICashFlowStatementQuery} filter -
* @return {Promise<IAccountTransaction[]>}
*/
public cashAtBeginningTotalTransactions(
tenantId: number,
filter: ICashFlowStatementQuery
): Promise<IAccountTransaction[]> {
const { AccountTransaction } = this.tenancy.models(tenantId);
const cashBeginningPeriod = moment(filter.fromDate)
.subtract(1, 'day')
.toDate();
return AccountTransaction.query().onBuild((query) => {
query.modify('creditDebitSummation');
query.select('accountId');
query.groupBy('accountId');
query.withGraphFetched('account');
query.modify('filterDateRange', null, cashBeginningPeriod);
this.commonFilterBranchesQuery(filter, query);
});
}
/**
* Retrieve accounts transactions.
* @param {number} tenantId -
* @param {ICashFlowStatementQuery} filter
* @return {Promise<IAccountTransaction>}
*/
public getAccountsTransactions(
tenantId: number,
filter: ICashFlowStatementQuery
): Promise<IAccountTransaction[]> {
const { AccountTransaction } = this.tenancy.models(tenantId);
const groupByDateType = this.getGroupTypeFromPeriodsType(
filter.displayColumnsBy
);
return AccountTransaction.query().onBuild((query) => {
query.modify('creditDebitSummation');
query.modify('groupByDateFormat', groupByDateType);
query.select('accountId');
query.groupBy('accountId');
query.withGraphFetched('account');
query.modify('filterDateRange', filter.fromDate, filter.toDate);
this.commonFilterBranchesQuery(filter, query);
});
}
/**
* Retrieve the net income tranasctions.
* @param {number} tenantId -
* @param {ICashFlowStatementQuery} query -
* @return {Promise<IAccountTransaction[]>}
*/
public getNetIncomeTransactions(
tenantId: number,
filter: ICashFlowStatementQuery
): Promise<IAccountTransaction[]> {
const { AccountTransaction } = this.tenancy.models(tenantId);
const groupByDateType = this.getGroupTypeFromPeriodsType(
filter.displayColumnsBy
);
return AccountTransaction.query().onBuild((query) => {
query.modify('creditDebitSummation');
query.modify('groupByDateFormat', groupByDateType);
query.select('accountId');
query.groupBy('accountId');
query.withGraphFetched('account');
query.modify('filterDateRange', filter.fromDate, filter.toDate);
this.commonFilterBranchesQuery(filter, query);
});
}
/**
* Retrieve peridos of cash at beginning transactions.
* @param {number} tenantId -
* @param {ICashFlowStatementQuery} filter -
* @return {Promise<IAccountTransaction[]>}
*/
public cashAtBeginningPeriodTransactions(
tenantId: number,
filter: ICashFlowStatementQuery
): Promise<IAccountTransaction[]> {
const { AccountTransaction } = this.tenancy.models(tenantId);
const groupByDateType = this.getGroupTypeFromPeriodsType(
filter.displayColumnsBy
);
return AccountTransaction.query().onBuild((query) => {
query.modify('creditDebitSummation');
query.modify('groupByDateFormat', groupByDateType);
query.select('accountId');
query.groupBy('accountId');
query.withGraphFetched('account');
query.modify('filterDateRange', filter.fromDate, filter.toDate);
this.commonFilterBranchesQuery(filter, query);
});
}
/**
* Common branches filter query.
* @param {Knex.QueryBuilder} query
*/
private commonFilterBranchesQuery = (
query: ICashFlowStatementQuery,
knexQuery: Knex.QueryBuilder
) => {
if (!isEmpty(query.branchesIds)) {
knexQuery.modify('filterByBranches', query.branchesIds);
}
};
}

View File

@@ -1,154 +0,0 @@
import moment from 'moment';
import { Service, Inject } from 'typedi';
import * as R from 'ramda';
import TenancyService from '@/services/Tenancy/TenancyService';
import FinancialSheet from '../FinancialSheet';
import {
ICashFlowStatementService,
ICashFlowStatementQuery,
ICashFlowStatementDOO,
IAccountTransaction,
ICashFlowStatementMeta,
} from '@/interfaces';
import CashFlowStatement from './CashFlow';
import Ledger from '@/services/Accounting/Ledger';
import CashFlowRepository from './CashFlowRepository';
import InventoryService from '@/services/Inventory/Inventory';
import { parseBoolean } from 'utils';
import { Tenant } from '@/system/models';
import { CashflowSheetMeta } from './CashflowSheetMeta';
@Service()
export default class CashFlowStatementService
extends FinancialSheet
implements ICashFlowStatementService
{
@Inject()
tenancy: TenancyService;
@Inject()
cashFlowRepo: CashFlowRepository;
@Inject()
inventoryService: InventoryService;
@Inject()
private cashflowSheetMeta: CashflowSheetMeta;
/**
* Defaults balance sheet filter query.
* @return {IBalanceSheetQuery}
*/
get defaultQuery(): ICashFlowStatementQuery {
return {
displayColumnsType: 'total',
displayColumnsBy: 'day',
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'),
numberFormat: {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'total',
negativeFormat: 'mines',
},
noneZero: false,
noneTransactions: false,
basis: 'cash',
};
}
/**
* Retrieve cash at beginning transactions.
* @param {number} tenantId -
* @param {ICashFlowStatementQuery} filter -
* @retrun {Promise<IAccountTransaction[]>}
*/
private async cashAtBeginningTransactions(
tenantId: number,
filter: ICashFlowStatementQuery
): Promise<IAccountTransaction[]> {
const appendPeriodsOperToChain = (trans) =>
R.append(
this.cashFlowRepo.cashAtBeginningPeriodTransactions(tenantId, filter),
trans
);
const promisesChain = R.pipe(
R.append(
this.cashFlowRepo.cashAtBeginningTotalTransactions(tenantId, filter)
),
R.when(
R.always(R.equals(filter.displayColumnsType, 'date_periods')),
appendPeriodsOperToChain
)
)([]);
const promisesResults = await Promise.all(promisesChain);
const transactions = R.flatten(promisesResults);
return transactions;
}
/**
* Retrieve the cash flow sheet statement.
* @param {number} tenantId
* @param {ICashFlowStatementQuery} query
* @returns {Promise<ICashFlowStatementDOO>}
*/
public async cashFlow(
tenantId: number,
query: ICashFlowStatementQuery
): Promise<ICashFlowStatementDOO> {
const i18n = this.tenancy.i18n(tenantId);
// Retrieve all accounts on the storage.
const accounts = await this.cashFlowRepo.cashFlowAccounts(tenantId);
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
const filter = {
...this.defaultQuery,
...query,
};
// Retrieve the accounts transactions.
const transactions = await this.cashFlowRepo.getAccountsTransactions(
tenantId,
filter
);
// Retrieve the net income transactions.
const netIncome = await this.cashFlowRepo.getNetIncomeTransactions(
tenantId,
filter
);
// Retrieve the cash at beginning transactions.
const cashAtBeginningTransactions = await this.cashAtBeginningTransactions(
tenantId,
filter
);
// Transformes the transactions to ledgers.
const ledger = Ledger.fromTransactions(transactions);
const cashLedger = Ledger.fromTransactions(cashAtBeginningTransactions);
const netIncomeLedger = Ledger.fromTransactions(netIncome);
// Cash flow statement.
const cashFlowInstance = new CashFlowStatement(
accounts,
ledger,
cashLedger,
netIncomeLedger,
filter,
tenant.metadata.baseCurrency,
i18n
);
// Retrieve the cashflow sheet meta.
const meta = await this.cashflowSheetMeta.meta(tenantId, filter);
return {
data: cashFlowInstance.reportData(),
query: filter,
meta,
};
}
}

View File

@@ -1,373 +0,0 @@
import * as R from 'ramda';
import { isEmpty, times } from 'lodash';
import moment from 'moment';
import {
ICashFlowStatementSection,
ICashFlowStatementSectionType,
ICashFlowStatement,
ITableRow,
ITableColumn,
ICashFlowStatementQuery,
IDateRange,
ICashFlowStatementDOO,
} from '@/interfaces';
import { dateRangeFromToCollection, tableRowMapper } from 'utils';
import { mapValuesDeep } from 'utils/deepdash';
enum IROW_TYPE {
AGGREGATE = 'AGGREGATE',
NET_INCOME = 'NET_INCOME',
ACCOUNTS = 'ACCOUNTS',
ACCOUNT = 'ACCOUNT',
TOTAL = 'TOTAL',
}
const DEEP_CONFIG = { childrenPath: 'children', pathFormat: 'array' };
const DISPLAY_COLUMNS_BY = {
DATE_PERIODS: 'date_periods',
TOTAL: 'total',
};
export default class CashFlowTable implements ICashFlowTable {
private report: ICashFlowStatementDOO;
private i18n;
private dateRangeSet: IDateRange[];
/**
* Constructor method.
* @param {ICashFlowStatement} reportStatement
*/
constructor(reportStatement: ICashFlowStatementDOO, i18n) {
this.report = reportStatement;
this.i18n = i18n;
this.dateRangeSet = [];
this.initDateRangeCollection();
}
/**
* Initialize date range set.
*/
private initDateRangeCollection() {
this.dateRangeSet = dateRangeFromToCollection(
this.report.query.fromDate,
this.report.query.toDate,
this.report.query.displayColumnsBy
);
}
/**
* Retrieve the date periods columns accessors.
*/
private datePeriodsColumnsAccessors = () => {
return this.dateRangeSet.map((dateRange: IDateRange, index) => ({
key: `date-range-${index}`,
accessor: `periods[${index}].total.formattedAmount`,
}));
};
/**
* Retrieve the total column accessor.
*/
private totalColumnAccessor = () => {
return [{ key: 'total', accessor: 'total.formattedAmount' }];
};
/**
* Retrieve the common columns for all report nodes.
*/
private commonColumns = () => {
return R.compose(
R.concat([{ key: 'name', accessor: 'label' }]),
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
R.concat(this.datePeriodsColumnsAccessors())
),
R.concat(this.totalColumnAccessor())
)([]);
};
/**
* Retrieve the table rows of regular section.
* @param {ICashFlowStatementSection} section
* @returns {ITableRow[]}
*/
private regularSectionMapper = (
section: ICashFlowStatementSection
): ITableRow => {
const columns = this.commonColumns();
return tableRowMapper(section, columns, {
rowTypes: [IROW_TYPE.AGGREGATE],
id: section.id,
});
};
/**
* Retrieve the net income table rows of the section.
* @param {ICashFlowStatementSection} section
* @returns {ITableRow}
*/
private netIncomeSectionMapper = (
section: ICashFlowStatementSection
): ITableRow => {
const columns = this.commonColumns();
return tableRowMapper(section, columns, {
rowTypes: [IROW_TYPE.NET_INCOME, IROW_TYPE.TOTAL],
id: section.id,
});
};
/**
* Retrieve the accounts table rows of the section.
* @param {ICashFlowStatementSection} section
* @returns {ITableRow}
*/
private accountsSectionMapper = (
section: ICashFlowStatementSection
): ITableRow => {
const columns = this.commonColumns();
return tableRowMapper(section, columns, {
rowTypes: [IROW_TYPE.ACCOUNTS],
id: section.id,
});
};
/**
* Retrieve the account table row of account section.
* @param {ICashFlowStatementSection} section
* @returns {ITableRow}
*/
private accountSectionMapper = (
section: ICashFlowStatementSection
): ITableRow => {
const columns = this.commonColumns();
return tableRowMapper(section, columns, {
rowTypes: [IROW_TYPE.ACCOUNT],
id: `account-${section.id}`,
});
};
/**
* Retrieve the total table rows from the given total section.
* @param {ICashFlowStatementSection} section
* @returns {ITableRow}
*/
private totalSectionMapper = (
section: ICashFlowStatementSection
): ITableRow => {
const columns = this.commonColumns();
return tableRowMapper(section, columns, {
rowTypes: [IROW_TYPE.TOTAL],
id: section.id,
});
};
/**
* Detarmines the schema section type.
* @param {string} type
* @param {ICashFlowSchemaSection} section
* @returns {boolean}
*/
private isSectionHasType = (
type: string,
section: ICashFlowStatementSection
): boolean => {
return type === section.sectionType;
};
/**
* The report section mapper.
* @param {ICashFlowStatementSection} section
* @returns {ITableRow}
*/
private sectionMapper = (
section: ICashFlowStatementSection,
key: string,
parentSection: ICashFlowStatementSection
): ITableRow => {
const isSectionHasType = R.curry(this.isSectionHasType);
return R.pipe(
R.when(
isSectionHasType(ICashFlowStatementSectionType.AGGREGATE),
this.regularSectionMapper
),
R.when(
isSectionHasType(ICashFlowStatementSectionType.CASH_AT_BEGINNING),
this.regularSectionMapper
),
R.when(
isSectionHasType(ICashFlowStatementSectionType.NET_INCOME),
this.netIncomeSectionMapper
),
R.when(
isSectionHasType(ICashFlowStatementSectionType.ACCOUNTS),
this.accountsSectionMapper
),
R.when(
isSectionHasType(ICashFlowStatementSectionType.ACCOUNT),
this.accountSectionMapper
),
R.when(
isSectionHasType(ICashFlowStatementSectionType.TOTAL),
this.totalSectionMapper
)
)(section);
};
/**
* Mappes the sections to the table rows.
* @param {ICashFlowStatementSection[]} sections
* @returns {ITableRow[]}
*/
private mapSectionsToTableRows = (
sections: ICashFlowStatementSection[]
): ITableRow[] => {
return mapValuesDeep(sections, this.sectionMapper.bind(this), DEEP_CONFIG);
};
/**
* Appends the total to section's children.
* @param {ICashFlowStatementSection} section
* @returns {ICashFlowStatementSection}
*/
private appendTotalToSectionChildren = (
section: ICashFlowStatementSection
): ICashFlowStatementSection => {
const label = section.footerLabel
? section.footerLabel
: this.i18n.__('Total {{accountName}}', { accountName: section.label });
section.children.push({
sectionType: ICashFlowStatementSectionType.TOTAL,
label,
periods: section.periods,
total: section.total,
});
return section;
};
/**
*
* @param {ICashFlowStatementSection} section
* @returns {ICashFlowStatementSection}
*/
private mapSectionsToAppendTotalChildren = (
section: ICashFlowStatementSection
): ICashFlowStatementSection => {
const isSectionHasChildren = (section) => !isEmpty(section.children);
return R.compose(
R.when(isSectionHasChildren, this.appendTotalToSectionChildren.bind(this))
)(section);
};
/**
* Appends total node to children section.
* @param {ICashFlowStatementSection[]} sections
* @returns {ICashFlowStatementSection[]}
*/
private appendTotalToChildren = (sections: ICashFlowStatementSection[]) => {
return mapValuesDeep(
sections,
this.mapSectionsToAppendTotalChildren.bind(this),
DEEP_CONFIG
);
};
/**
* Retrieve the table rows of cash flow statement.
* @param {ICashFlowStatementSection[]} sections
* @returns {ITableRow[]}
*/
public tableRows = (): ITableRow[] => {
const sections = this.report.data;
return R.pipe(
this.appendTotalToChildren,
this.mapSectionsToTableRows
)(sections);
};
/**
* Retrieve the total columns.
* @returns {ITableColumn}
*/
private totalColumns = (): ITableColumn[] => {
return [{ key: 'total', label: this.i18n.__('Total') }];
};
/**
* Retrieve the formatted column label from the given date range.
* @param {ICashFlowDateRange} dateRange -
* @return {string}
*/
private formatColumnLabel = (dateRange: ICashFlowDateRange) => {
const monthFormat = (range) => moment(range.toDate).format('YYYY-MM');
const yearFormat = (range) => moment(range.toDate).format('YYYY');
const dayFormat = (range) => moment(range.toDate).format('YYYY-MM-DD');
const conditions = [
['month', monthFormat],
['year', yearFormat],
['day', dayFormat],
['quarter', monthFormat],
['week', dayFormat],
];
const conditionsPairs = R.map(
([type, formatFn]) => [
R.always(this.isDisplayColumnsType(type)),
formatFn,
],
conditions
);
return R.compose(R.cond(conditionsPairs))(dateRange);
};
/**
* Date periods columns.
* @returns {ITableColumn[]}
*/
private datePeriodsColumns = (): ITableColumn[] => {
return this.dateRangeSet.map((dateRange, index) => ({
key: `date-range-${index}`,
label: this.formatColumnLabel(dateRange),
}));
};
/**
* Detarmines the given column type is the current.
* @reutrns {boolean}
*/
private isDisplayColumnsBy = (displayColumnsType: string): Boolean => {
return this.report.query.displayColumnsType === displayColumnsType;
};
/**
* Detarmines whether the given display columns type is the current.
* @param {string} displayColumnsBy
* @returns {boolean}
*/
private isDisplayColumnsType = (displayColumnsBy: string): Boolean => {
return this.report.query.displayColumnsBy === displayColumnsBy;
};
/**
* Retrieve the table columns.
* @return {ITableColumn[]}
*/
public tableColumns = (): ITableColumn[] => {
return R.compose(
R.concat([{ key: 'name', label: this.i18n.__('Account name') }]),
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
R.concat(this.datePeriodsColumns())
),
R.concat(this.totalColumns())
)([]);
};
}

View File

@@ -1,46 +0,0 @@
import { Inject, Service } from 'typedi';
import { ICashFlowStatementQuery } from '@/interfaces';
import { TableSheet } from '@/lib/Xlsx/TableSheet';
import { CashflowTableInjectable } from './CashflowTableInjectable';
@Service()
export class CashflowExportInjectable {
@Inject()
private cashflowSheetTable: CashflowTableInjectable;
/**
* Retrieves the cashflow sheet in XLSX format.
* @param {number} tenantId
* @param {ICashFlowStatementQuery} query
* @returns {Promise<Buffer>}
*/
public async xlsx(
tenantId: number,
query: ICashFlowStatementQuery
): Promise<Buffer> {
const table = await this.cashflowSheetTable.table(tenantId, query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToXLSX();
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
}
/**
* Retrieves the cashflow sheet in CSV format.
* @param {number} tenantId
* @param {ICashFlowStatementQuery} query
* @returns {Promise<Buffer>}
*/
public async csv(
tenantId: number,
query: ICashFlowStatementQuery
): Promise<string> {
const table = await this.cashflowSheetTable.table(tenantId, query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
}

View File

@@ -1,75 +0,0 @@
import { Inject, Service } from 'typedi';
import { CashflowExportInjectable } from './CashflowExportInjectable';
import { ICashFlowStatementQuery } from '@/interfaces';
import CashFlowStatementService from './CashFlowService';
import { CashflowTableInjectable } from './CashflowTableInjectable';
import { CashflowTablePdfInjectable } from './CashflowTablePdfInjectable';
@Service()
export class CashflowSheetApplication {
@Inject()
private cashflowExport: CashflowExportInjectable;
@Inject()
private cashflowSheet: CashFlowStatementService;
@Inject()
private cashflowTable: CashflowTableInjectable;
@Inject()
private cashflowPdf: CashflowTablePdfInjectable;
/**
* Retrieves the cashflow sheet
* @param {number} tenantId
* @param {ICashFlowStatementQuery} query
*/
public async sheet(tenantId: number, query: ICashFlowStatementQuery) {
return this.cashflowSheet.cashFlow(tenantId, query);
}
/**
* Retrieves the cashflow sheet in table format.
* @param {number} tenantId
* @param {ICashFlowStatementQuery} query
*/
public async table(tenantId: number, query: ICashFlowStatementQuery) {
return this.cashflowTable.table(tenantId, query);
}
/**
* Retrieves the cashflow sheet in XLSX format.
* @param {number} tenantId
* @param {ICashFlowStatementQuery} query
* @returns {Promise<Buffer>}
*/
public async xlsx(tenantId: number, query: ICashFlowStatementQuery) {
return this.cashflowExport.xlsx(tenantId, query);
}
/**
* Retrieves the cashflow sheet in CSV format.
* @param {number} tenantId
* @param {ICashFlowStatementQuery} query
* @returns {Promise<Buffer>}
*/
public async csv(
tenantId: number,
query: ICashFlowStatementQuery
): Promise<string> {
return this.cashflowExport.csv(tenantId, query);
}
/**
* Retrieves the cashflow sheet in pdf format.
* @param {number} tenantId
* @param {ICashFlowStatementQuery} query
* @returns {Promise<Buffer>}
*/
public async pdf(
tenantId: number,
query: ICashFlowStatementQuery
): Promise<Buffer> {
return this.cashflowPdf.pdf(tenantId, query);
}
}

View File

@@ -1,36 +0,0 @@
import { Inject, Service } from 'typedi';
import moment from 'moment';
import { ICashFlowStatementMeta, ICashFlowStatementQuery } from '@/interfaces';
import { FinancialSheetMeta } from '../FinancialSheetMeta';
@Service()
export class CashflowSheetMeta {
@Inject()
private financialSheetMeta: FinancialSheetMeta;
/**
* CAshflow sheet meta.
* @param {number} tenantId
* @param {ICashFlowStatementQuery} query
* @returns {Promise<ICashFlowStatementMeta>}
*/
public async meta(
tenantId: number,
query: ICashFlowStatementQuery
): Promise<ICashFlowStatementMeta> {
const meta = await this.financialSheetMeta.meta(tenantId);
const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`;
const sheetName = 'Statement of Cash Flow';
return {
...meta,
sheetName,
formattedToDate,
formattedFromDate,
formattedDateRange,
};
}
}

View File

@@ -1,37 +0,0 @@
import { Inject, Service } from "typedi";
import { ICashFlowStatementQuery, ICashFlowStatementTable } from "@/interfaces";
import HasTenancyService from "@/services/Tenancy/TenancyService";
import CashFlowTable from "./CashFlowTable";
import CashFlowStatementService from "./CashFlowService";
@Service()
export class CashflowTableInjectable {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private cashflowSheet: CashFlowStatementService;
/**
* Retrieves the cash flow table.
* @returns {Promise<ICashFlowStatementTable>}
*/
public async table(
tenantId: number,
query: ICashFlowStatementQuery
): Promise<ICashFlowStatementTable> {
const i18n = this.tenancy.i18n(tenantId);
const cashflowDOO = await this.cashflowSheet.cashFlow(tenantId, query);
const cashflowTable = new CashFlowTable(cashflowDOO, i18n);
return {
table: {
columns: cashflowTable.tableColumns(),
rows: cashflowTable.tableRows(),
},
query: cashflowDOO.query,
meta: cashflowDOO.meta,
};
}
}

View File

@@ -1,34 +0,0 @@
import { Inject } from 'typedi';
import { CashflowTableInjectable } from './CashflowTableInjectable';
import { TableSheetPdf } from '../TableSheetPdf';
import { ICashFlowStatementQuery } from '@/interfaces';
import { HtmlTableCustomCss } from './constants';
export class CashflowTablePdfInjectable {
@Inject()
private cashflowTable: CashflowTableInjectable;
@Inject()
private tableSheetPdf: TableSheetPdf;
/**
* Converts the given cashflow sheet table to pdf.
* @param {number} tenantId - Tenant ID.
* @param {IBalanceSheetQuery} query - Balance sheet query.
* @returns {Promise<Buffer>}
*/
public async pdf(
tenantId: number,
query: ICashFlowStatementQuery
): Promise<Buffer> {
const table = await this.cashflowTable.table(tenantId, query);
return this.tableSheetPdf.convertToPdf(
tenantId,
table.table,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCustomCss
);
}
}

View File

@@ -1,33 +0,0 @@
export const DISPLAY_COLUMNS_BY = {
DATE_PERIODS: 'date_periods',
TOTAL: 'total',
};
export const MAP_CONFIG = { childrenPath: 'children', pathFormat: 'array' };
export const HtmlTableCustomCss = `
table tr.row-type--accounts td {
border-top: 1px solid #bbb;
}
table tr.row-id--cash-end-period td {
border-bottom: 3px double #333;
}
table tr.row-type--total {
font-weight: 600;
}
table tr.row-type--total td {
color: #000;
}
table tr.row-type--total:not(:first-child) td {
border-top: 1px solid #bbb;
}
table .column--name,
table .cell--name {
width: 400px;
}
table .column--total,
table .cell--total,
table [class*="column--date-range"],
table [class*="cell--date-range"] {
text-align: right;
}
`;

View File

@@ -1,75 +0,0 @@
import { ICashFlowSchemaSection, CASH_FLOW_SECTION_ID, ICashFlowStatementSectionType } from '@/interfaces';
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
export default [
{
id: CASH_FLOW_SECTION_ID.OPERATING,
label: 'OPERATING ACTIVITIES',
sectionType: ICashFlowStatementSectionType.AGGREGATE,
children: [
{
id: CASH_FLOW_SECTION_ID.NET_INCOME,
label: 'Net income',
sectionType: ICashFlowStatementSectionType.NET_INCOME,
},
{
id: CASH_FLOW_SECTION_ID.OPERATING_ACCOUNTS,
label: 'Adjustments net income by operating activities.',
sectionType: ICashFlowStatementSectionType.ACCOUNTS,
accountsRelations: [
{ type: ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE, direction: 'mines' },
{ type: ACCOUNT_TYPE.INVENTORY, direction: 'mines' },
{ type: ACCOUNT_TYPE.NON_CURRENT_ASSET, direction: 'mines' },
{ type: ACCOUNT_TYPE.ACCOUNTS_PAYABLE, direction: 'plus' },
{ type: ACCOUNT_TYPE.CREDIT_CARD, direction: 'plus' },
{ type: ACCOUNT_TYPE.TAX_PAYABLE, direction: 'plus' },
{ type: ACCOUNT_TYPE.OTHER_CURRENT_ASSET, direction: 'mines' },
{ type: ACCOUNT_TYPE.OTHER_CURRENT_LIABILITY, direction: 'plus' },
{ type: ACCOUNT_TYPE.NON_CURRENT_LIABILITY, direction: 'plus' },
],
showAlways: true,
},
],
footerLabel: 'Net cash provided by operating activities',
},
{
id: CASH_FLOW_SECTION_ID.INVESTMENT,
sectionType: ICashFlowStatementSectionType.ACCOUNTS,
label: 'INVESTMENT ACTIVITIES',
accountsRelations: [
{ type: ACCOUNT_TYPE.FIXED_ASSET, direction: 'mines' }
],
footerLabel: 'Net cash provided by investing activities',
},
{
id: CASH_FLOW_SECTION_ID.FINANCIAL,
label: 'FINANCIAL ACTIVITIES',
sectionType: ICashFlowStatementSectionType.ACCOUNTS,
accountsRelations: [
{ type: ACCOUNT_TYPE.LOGN_TERM_LIABILITY, direction: 'plus' },
{ type: ACCOUNT_TYPE.EQUITY, direction: 'plus' },
],
footerLabel: 'Net cash provided by financing activities',
},
{
id: CASH_FLOW_SECTION_ID.CASH_BEGINNING_PERIOD,
sectionType: ICashFlowStatementSectionType.CASH_AT_BEGINNING,
label: 'Cash at beginning of period',
accountsRelations: [
{ type: ACCOUNT_TYPE.CASH, direction: 'plus' },
{ type: ACCOUNT_TYPE.BANK, direction: 'plus' },
],
},
{
id: CASH_FLOW_SECTION_ID.NET_CASH_INCREASE,
sectionType: ICashFlowStatementSectionType.TOTAL,
equation: 'OPERATING + INVESTMENT + FINANCIAL',
label: 'NET CASH INCREASE FOR PERIOD',
},
{
id: CASH_FLOW_SECTION_ID.CASH_END_PERIOD,
label: 'CASH AT END OF PERIOD',
sectionType: ICashFlowStatementSectionType.TOTAL,
equation: 'NET_CASH_INCREASE + CASH_BEGINNING_PERIOD',
},
] as ICashFlowSchemaSection[];

View File

@@ -0,0 +1,204 @@
import { sumBy, isEmpty } from 'lodash';
import * as R from 'ramda';
import {
IContactBalanceSummaryContact,
IContactBalanceSummaryTotal,
IContactBalanceSummaryAmount,
IContactBalanceSummaryPercentage,
IContactBalanceSummaryQuery,
} from './ContactBalanceSummary.types';
import { FinancialSheet } from '../../common/FinancialSheet';
import { Ledger } from '@/modules/Ledger/Ledger';
import { allPassedConditionsPass } from '@/utils/all-conditions-passed';
export class ContactBalanceSummaryReport extends FinancialSheet {
readonly baseCurrency: string;
readonly ledger: Ledger;
readonly filter: IContactBalanceSummaryQuery;
/**
* Calculates the contact percentage of column.
* @param {number} customerBalance - Contact balance.
* @param {number} totalBalance - Total contacts balance.
* @returns {number}
*/
protected getContactPercentageOfColumn = (
customerBalance: number,
totalBalance: number
): number => {
return totalBalance / customerBalance;
};
/**
* Retrieve the contacts total.
* @param {IContactBalanceSummaryContact} contacts
* @returns {number}
*/
protected getContactsTotal = (
contacts: IContactBalanceSummaryContact[]
): number => {
return sumBy(
contacts,
(contact: IContactBalanceSummaryContact) => contact.total.amount
);
};
/**
* Assoc total percentage of column.
* @param {IContactBalanceSummaryTotal} node
* @returns {IContactBalanceSummaryTotal}
*/
protected assocTotalPercentageOfColumn = (
node: IContactBalanceSummaryTotal
): IContactBalanceSummaryTotal => {
return R.assoc('percentageOfColumn', this.getPercentageMeta(1), node);
};
/**
* Retrieve the contacts total section.
* @param {IContactBalanceSummaryContact[]} contacts
* @returns {IContactBalanceSummaryTotal}
*/
protected getContactsTotalSection = (
contacts: IContactBalanceSummaryContact[]
): IContactBalanceSummaryTotal => {
const customersTotal = this.getContactsTotal(contacts);
const node = {
total: this.getTotalFormat(customersTotal),
};
return R.compose(
R.when(
R.always(this.filter.percentageColumn),
this.assocTotalPercentageOfColumn
)
)(node);
};
/**
* Retrieve the contact summary section with percentage of column.
* @param {number} total
* @param {IContactBalanceSummaryContact} contact
* @returns {IContactBalanceSummaryContact}
*/
private contactCamparsionPercentageOfColumnMapper = (
total: number,
contact: IContactBalanceSummaryContact
): IContactBalanceSummaryContact => {
const amount = this.getContactPercentageOfColumn(
total,
contact.total.amount
);
return {
...contact,
percentageOfColumn: this.getPercentageMeta(amount),
};
};
/**
* Mappes the contacts summary sections with percentage of column.
* @param {IContactBalanceSummaryContact[]} contacts -
* @return {IContactBalanceSummaryContact[]}
*/
protected contactCamparsionPercentageOfColumn = (
contacts: IContactBalanceSummaryContact[]
): IContactBalanceSummaryContact[] => {
const customersTotal = this.getContactsTotal(contacts);
const camparsionPercentageOfColummn = R.curry(
this.contactCamparsionPercentageOfColumnMapper
)(customersTotal);
return contacts.map(camparsionPercentageOfColummn);
};
/**
* Retrieve the contact total format.
* @param {number} amount -
* @return {IContactBalanceSummaryAmount}
*/
protected getContactTotalFormat = (
amount: number
): IContactBalanceSummaryAmount => {
return {
amount,
formattedAmount: this.formatNumber(amount, { money: true }),
currencyCode: this.baseCurrency,
};
};
/**
* Retrieve the total amount of contacts sections.
* @param {number} amount
* @returns {IContactBalanceSummaryAmount}
*/
protected getTotalFormat = (amount: number): IContactBalanceSummaryAmount => {
return {
amount,
formattedAmount: this.formatTotalNumber(amount, { money: true }),
currencyCode: this.baseCurrency,
};
};
/**
* Retrieve the percentage amount object.
* @param {number} amount
* @returns {IContactBalanceSummaryPercentage}
*/
protected getPercentageMeta = (
amount: number
): IContactBalanceSummaryPercentage => {
return {
amount,
formattedAmount: this.formatPercentage(amount),
};
};
/**
* Filters customer has none transactions.
* @param {ICustomerBalanceSummaryCustomer} customer -
* @returns {boolean}
*/
private filterContactNoneTransactions = (
contact: IContactBalanceSummaryContact
): boolean => {
const entries = this.ledger.whereContactId(contact.id).getEntries();
return !isEmpty(entries);
};
/**
* Filters the customer that has zero total amount.
* @param {ICustomerBalanceSummaryCustomer} customer
* @returns {boolean}
*/
private filterContactNoneZero = (
node: IContactBalanceSummaryContact
): boolean => {
return node.total.amount !== 0;
};
/**
* Filters the given customer node;
* @param {ICustomerBalanceSummaryCustomer} customer
*/
private contactNodeFilter = (contact: IContactBalanceSummaryContact) => {
const { noneTransactions, noneZero } = this.filter;
// Conditions pair filter detarminer.
const condsPairFilters = [
[noneTransactions, this.filterContactNoneTransactions],
[noneZero, this.filterContactNoneZero],
];
return allPassedConditionsPass(condsPairFilters)(contact);
};
/**
* Filters the given customers nodes.
* @param {ICustomerBalanceSummaryCustomer[]} nodes
* @returns {ICustomerBalanceSummaryCustomer[]}
*/
protected contactsFilter = (
nodes: IContactBalanceSummaryContact[]
): IContactBalanceSummaryContact[] => {
return nodes.filter(this.contactNodeFilter);
};
}

View File

@@ -0,0 +1,47 @@
import { INumberFormatQuery } from "../../types/Report.types";
export interface IContactBalanceSummaryQuery {
asDate: Date;
numberFormat: INumberFormatQuery;
percentageColumn: boolean;
noneTransactions: boolean;
noneZero: boolean;
}
export interface IContactBalanceSummaryAmount {
amount: number;
formattedAmount: string;
currencyCode: string;
}
export interface IContactBalanceSummaryPercentage {
amount: number;
formattedAmount: string;
}
export interface IContactBalanceSummaryContact {
total: IContactBalanceSummaryAmount;
percentageOfColumn?: IContactBalanceSummaryPercentage;
}
export interface IContactBalanceSummaryTotal {
total: IContactBalanceSummaryAmount;
percentageOfColumn?: IContactBalanceSummaryPercentage;
}
export interface ICustomerBalanceSummaryData {
customers: IContactBalanceSummaryContact[];
total: IContactBalanceSummaryTotal;
}
export interface ICustomerBalanceSummaryStatement {
data: ICustomerBalanceSummaryData;
columns: {};
query: IContactBalanceSummaryQuery;
}
export interface ICustomerBalanceSummaryService {
customerBalanceSummary(
tenantId: number,
query: IContactBalanceSummaryQuery
): Promise<ICustomerBalanceSummaryStatement>;
}

View File

@@ -0,0 +1,57 @@
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
import { ICustomerBalanceSummaryQuery } from './CustomerBalanceSummary.types';
import { CustomerBalanceSummaryApplication } from './CustomerBalanceSummaryApplication';
import { AcceptType } from '@/constants/accept-type';
import { Response } from 'express';
import { ApiResponse, ApiTags } from '@nestjs/swagger';
@Controller('/reports/customer-balance-summary')
@ApiTags('reports')
export class CustomerBalanceSummaryController {
constructor(
private readonly customerBalanceSummaryApp: CustomerBalanceSummaryApplication,
) {}
@Get()
@ApiResponse({ status: 200, description: 'Customer balance summary report' })
async customerBalanceSummary(
@Query() filter: ICustomerBalanceSummaryQuery,
@Res() res: Response,
@Headers('accept') acceptHeader: string,
) {
// Retrieves the xlsx format.
if (acceptHeader.includes(AcceptType.ApplicationXlsx)) {
const buffer = await this.customerBalanceSummaryApp.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 csv format.
} else if (acceptHeader.includes(AcceptType.ApplicationCsv)) {
const buffer = await this.customerBalanceSummaryApp.csv(filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
// Retrieves the json table format.
} else if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) {
const table = await this.customerBalanceSummaryApp.table(filter);
return res.status(200).send(table);
// Retrieves the pdf format.
} else if (acceptHeader.includes(AcceptType.ApplicationPdf)) {
const buffer = await this.customerBalanceSummaryApp.pdf(filter);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': buffer.length,
});
return res.send(buffer);
// Retrieves the json format.
} else {
const sheet = await this.customerBalanceSummaryApp.sheet(filter);
return res.status(200).send(sheet);
}
}
}

View File

@@ -0,0 +1,29 @@
import { Module } from '@nestjs/common';
import { CustomerBalanceSummaryApplication } from './CustomerBalanceSummaryApplication';
import { CustomerBalanceSummaryExportInjectable } from './CustomerBalanceSummaryExportInjectable';
import { CustomerBalanceSummaryMeta } from './CustomerBalanceSummaryMeta';
import { CustomerBalanceSummaryPdf } from './CustomerBalanceSummaryPdf';
import { CustomerBalanceSummaryService } from './CustomerBalanceSummaryService';
import { CustomerBalanceSummaryTableInjectable } from './CustomerBalanceSummaryTableInjectable';
import { CustomerBalanceSummaryController } from './CustomerBalanceSummary.controller';
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
import { CustomerBalanceSummaryRepository } from './CustomerBalanceSummaryRepository';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Module({
imports: [
FinancialSheetCommonModule,
],
controllers: [CustomerBalanceSummaryController],
providers: [
CustomerBalanceSummaryApplication,
CustomerBalanceSummaryExportInjectable,
CustomerBalanceSummaryMeta,
CustomerBalanceSummaryPdf,
CustomerBalanceSummaryService,
CustomerBalanceSummaryTableInjectable,
CustomerBalanceSummaryRepository,
TenancyContext
],
})
export class CustomerBalanceSummaryModule {}

View File

@@ -0,0 +1,112 @@
import { isEmpty } from 'lodash';
import * as R from 'ramda';
import {
ICustomerBalanceSummaryCustomer,
ICustomerBalanceSummaryQuery,
ICustomerBalanceSummaryData,
} from './CustomerBalanceSummary.types';
import { ContactBalanceSummaryReport } from '../ContactBalanceSummary/ContactBalanceSummary';
import { ILedger } from '@/modules/Ledger/types/Ledger.types';
import { ModelObject } from 'objection';
import { INumberFormatQuery } from '../../types/Report.types';
import { Customer } from '@/modules/Customers/models/Customer';
export class CustomerBalanceSummaryReport extends ContactBalanceSummaryReport {
readonly ledger: ILedger;
readonly baseCurrency: string;
readonly customers: ModelObject<Customer>[];
readonly filter: ICustomerBalanceSummaryQuery;
readonly numberFormat: INumberFormatQuery;
/**
* Constructor method.
* @param {IJournalPoster} receivableLedger
* @param {ICustomer[]} customers
* @param {ICustomerBalanceSummaryQuery} filter
* @param {string} baseCurrency
*/
constructor(
ledger: ILedger,
customers: ModelObject<Customer>[],
filter: ICustomerBalanceSummaryQuery,
baseCurrency: string
) {
super();
this.ledger = ledger;
this.baseCurrency = baseCurrency;
this.customers = customers;
this.filter = filter;
this.numberFormat = this.filter.numberFormat;
}
/**
* Customer section mapper.
* @param {ModelObject<Customer>} customer
* @returns {ICustomerBalanceSummaryCustomer}
*/
private customerMapper = (
customer: ModelObject<Customer>
): ICustomerBalanceSummaryCustomer => {
const closingBalance = this.ledger
.whereContactId(customer.id)
.getClosingBalance();
return {
id: customer.id,
customerName: customer.displayName,
total: this.getContactTotalFormat(closingBalance),
};
};
/**
* Mappes the customer model object to customer balance summary section.
* @param {ModelObject<Customer>[]} customers - Customers.
* @returns {ICustomerBalanceSummaryCustomer[]}
*/
private customersMapper = (
customers: ModelObject<Customer>[]
): ICustomerBalanceSummaryCustomer[] => {
return customers.map(this.customerMapper);
};
/**
* Detarmines whether the customers post filter is active.
* @returns {boolean}
*/
private isCustomersPostFilter = () => {
return isEmpty(this.filter.customersIds);
};
/**
* Retrieve the customers sections of the report.
* @param {ModelObject<Customer>[]} customers
* @returns {ICustomerBalanceSummaryCustomer[]}
*/
private getCustomersSection = (
customers: ModelObject<Customer>[]
): ICustomerBalanceSummaryCustomer[] => {
return R.compose(
R.when(this.isCustomersPostFilter, this.contactsFilter),
R.when(
R.always(this.filter.percentageColumn),
this.contactCamparsionPercentageOfColumn
),
this.customersMapper
)(customers);
};
/**
* Retrieve the report statement data.
* @returns {ICustomerBalanceSummaryData}
*/
public reportData = (): ICustomerBalanceSummaryData => {
const customersSections = this.getCustomersSection(this.customers);
const customersTotal = this.getContactsTotalSection(customersSections);
return {
customers: customersSections,
total: customersTotal,
};
};
}

View File

@@ -0,0 +1,60 @@
import { IFinancialSheetCommonMeta } from '../../types/Report.types';
import { IFinancialTable } from '../../types/Table.types';
import {
IContactBalanceSummaryQuery,
IContactBalanceSummaryAmount,
IContactBalanceSummaryPercentage,
IContactBalanceSummaryTotal,
} from '../ContactBalanceSummary/ContactBalanceSummary.types';
export interface ICustomerBalanceSummaryQuery
extends IContactBalanceSummaryQuery {
customersIds?: number[];
}
export interface ICustomerBalanceSummaryAmount
extends IContactBalanceSummaryAmount {}
export interface ICustomerBalanceSummaryPercentage
extends IContactBalanceSummaryPercentage {}
export interface ICustomerBalanceSummaryCustomer {
id: number;
customerName: string;
total: ICustomerBalanceSummaryAmount;
percentageOfColumn?: ICustomerBalanceSummaryPercentage;
}
export interface ICustomerBalanceSummaryTotal
extends IContactBalanceSummaryTotal {
total: ICustomerBalanceSummaryAmount;
percentageOfColumn?: ICustomerBalanceSummaryPercentage;
}
export interface ICustomerBalanceSummaryData {
customers: ICustomerBalanceSummaryCustomer[];
total: ICustomerBalanceSummaryTotal;
}
export interface ICustomerBalanceSummaryMeta extends IFinancialSheetCommonMeta {
formattedAsDate: string;
formattedDateRange: string;
}
export interface ICustomerBalanceSummaryStatement {
data: ICustomerBalanceSummaryData;
query: ICustomerBalanceSummaryQuery;
meta: ICustomerBalanceSummaryMeta;
}
export interface ICustomerBalanceSummaryService {
customerBalanceSummary(
tenantId: number,
query: ICustomerBalanceSummaryQuery
): Promise<ICustomerBalanceSummaryStatement>;
}
export interface ICustomerBalanceSummaryTable extends IFinancialTable {
query: ICustomerBalanceSummaryQuery;
meta: ICustomerBalanceSummaryMeta;
}

View File

@@ -0,0 +1,62 @@
import { CustomerBalanceSummaryExportInjectable } from './CustomerBalanceSummaryExportInjectable';
import { CustomerBalanceSummaryTableInjectable } from './CustomerBalanceSummaryTableInjectable';
import { ICustomerBalanceSummaryQuery } from './CustomerBalanceSummary.types';
import { CustomerBalanceSummaryService } from './CustomerBalanceSummaryService';
import { CustomerBalanceSummaryPdf } from './CustomerBalanceSummaryPdf';
import { Injectable } from '@nestjs/common';
@Injectable()
export class CustomerBalanceSummaryApplication {
constructor(
private readonly customerBalanceSummaryTable: CustomerBalanceSummaryTableInjectable,
private readonly customerBalanceSummaryExport: CustomerBalanceSummaryExportInjectable,
private readonly customerBalanceSummarySheet: CustomerBalanceSummaryService,
private readonly customerBalanceSummaryPdf: CustomerBalanceSummaryPdf,
) {}
/**
* Retrieves the customer balance sheet in json format.
* @param {ICustomerBalanceSummaryQuery} query
* @returns {Promise<ICustomerBalanceSummarySheet>}
*/
public sheet(query: ICustomerBalanceSummaryQuery) {
return this.customerBalanceSummarySheet.customerBalanceSummary(query);
}
/**
* Retrieves the customer balance sheet in json format.
* @param {ICustomerBalanceSummaryQuery} query
* @returns {Promise<ICustomerBalanceSummaryTable>}
*/
public table(query: ICustomerBalanceSummaryQuery) {
return this.customerBalanceSummaryTable.table(query);
}
/**
* Retrieves the customer balance sheet in XLSX format.
* @param {ICustomerBalanceSummaryQuery} query
* @returns {Promise<Buffer>}
*/
public xlsx(query: ICustomerBalanceSummaryQuery) {
return this.customerBalanceSummaryExport.xlsx(query);
}
/**
* Retrieves the customer balance sheet in CSV format.
* @param {ICustomerBalanceSummaryQuery} query
* @returns {Promise<Buffer>}
*/
public csv(query: ICustomerBalanceSummaryQuery) {
return this.customerBalanceSummaryExport.csv(query);
}
/**
* Retrieves the customer balance sheet in PDF format.
* @param {number} tenantId
* @param {ICustomerBalanceSummaryQuery} query
* @returns {Promise<Buffer>}
*/
public pdf(query: ICustomerBalanceSummaryQuery) {
return this.customerBalanceSummaryPdf.pdf(query);
}
}

View File

@@ -0,0 +1,43 @@
import { Injectable } from '@nestjs/common';
import { ICustomerBalanceSummaryQuery } from './CustomerBalanceSummary.types';
import { CustomerBalanceSummaryTableInjectable } from './CustomerBalanceSummaryTableInjectable';
import { TableSheet } from '../../common/TableSheet';
@Injectable()
export class CustomerBalanceSummaryExportInjectable {
/**
* Constructor method.
* @param {CustomerBalanceSummaryTableInjectable} customerBalanceSummaryTable
*/
constructor(
private readonly customerBalanceSummaryTable: CustomerBalanceSummaryTableInjectable,
) {}
/**
* Retrieves the cashflow sheet in XLSX format.
* @param {ICustomerBalanceSummaryQuery} query
* @returns {Promise<Buffer>}
*/
public async xlsx(query: ICustomerBalanceSummaryQuery) {
const table = await this.customerBalanceSummaryTable.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 {ICustomerBalanceSummaryQuery} query - Query.
* @returns {Promise<Buffer>}
*/
public async csv(query: ICustomerBalanceSummaryQuery): Promise<string> {
const table = await this.customerBalanceSummaryTable.table(query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
}

View File

@@ -0,0 +1,32 @@
import moment from 'moment';
import {
ICustomerBalanceSummaryMeta,
ICustomerBalanceSummaryQuery,
} from './CustomerBalanceSummary.types';
import { Injectable } from '@nestjs/common';
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
@Injectable()
export class CustomerBalanceSummaryMeta {
constructor(private readonly financialSheetMeta: FinancialSheetMeta) {}
/**
* Retrieves the customer balance summary meta.
* @param {ICustomerBalanceSummaryQuery} query
* @returns {Promise<ICustomerBalanceSummaryMeta>}
*/
async meta(
query: ICustomerBalanceSummaryQuery,
): Promise<ICustomerBalanceSummaryMeta> {
const commonMeta = await this.financialSheetMeta.meta();
const formattedAsDate = moment(query.asDate).format('YYYY/MM/DD');
const formattedDateRange = `As ${formattedAsDate}`;
return {
...commonMeta,
sheetName: 'Customer Balance Summary',
formattedAsDate,
formattedDateRange,
};
}
}

View File

@@ -0,0 +1,29 @@
import { TableSheetPdf } from '../../common/TableSheetPdf';
import { ICustomerBalanceSummaryQuery } from './CustomerBalanceSummary.types';
import { CustomerBalanceSummaryTableInjectable } from './CustomerBalanceSummaryTableInjectable';
import { HtmlTableCustomCss } from './constants';
import { Injectable } from '@nestjs/common';
@Injectable()
export class CustomerBalanceSummaryPdf {
constructor(
private readonly customerBalanceSummaryTable: CustomerBalanceSummaryTableInjectable,
private readonly tableSheetPdf: TableSheetPdf,
) {}
/**
* Converts the given customer balance summary sheet table to pdf.
* @param {IAPAgingSummaryQuery} query - Balance sheet query.
* @returns {Promise<Buffer>}
*/
public async pdf(query: ICustomerBalanceSummaryQuery): Promise<Buffer> {
const table = await this.customerBalanceSummaryTable.table(query);
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCustomCss,
);
}
}

View File

@@ -0,0 +1,74 @@
import { map, isEmpty } from 'lodash';
import { Inject, Injectable } from '@nestjs/common';
import { Account } from '@/modules/Accounts/models/Account.model';
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
import { Customer } from '@/modules/Customers/models/Customer';
import { ModelObject } from 'objection';
import { ACCOUNT_TYPE } from '@/constants/accounts';
@Injectable()
export class CustomerBalanceSummaryRepository {
constructor(
@Inject(Account.name)
private readonly accountModel: typeof Account,
@Inject(AccountTransaction.name)
private readonly accountTransactionModel: typeof AccountTransaction,
@Inject(Customer.name)
private readonly customerModel: typeof Customer,
) {}
/**
* Retrieve the report customers.
* @param {number[]} customersIds
* @returns {ICustomer[]}
*/
public async getCustomers(
customersIds: number[],
): Promise<ModelObject<Customer>[]> {
return await this.customerModel
.query()
.orderBy('displayName')
.onBuild((query) => {
if (!isEmpty(customersIds)) {
query.whereIn('id', customersIds);
}
});
}
/**
* Retrieve the A/R accounts.
* @returns {Promise<IAccount[]>}
*/
public async getReceivableAccounts(): Promise<ModelObject<Account>[]> {
return await this.accountModel
.query()
.where('accountType', ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE);
}
/**
* Retrieve the customers credit/debit totals
* @returns
*/
public async getCustomersTransactions(
asDate: any,
): Promise<ModelObject<AccountTransaction>[]> {
// Retrieve the receivable accounts A/R.
const receivableAccounts = await this.getReceivableAccounts();
const receivableAccountsIds = map(receivableAccounts, 'id');
// Retrieve the customers transactions of A/R accounts.
const customersTranasctions = await this.accountTransactionModel
.query()
.onBuild((query) => {
query.whereIn('accountId', receivableAccountsIds);
query.modify('filterDateRange', null, asDate);
query.groupBy('contactId');
query.sum('credit as credit');
query.sum('debit as debit');
query.select('contactId');
});
return customersTranasctions;
}
}

View File

@@ -0,0 +1,89 @@
import * as R from 'ramda';
import {
ICustomerBalanceSummaryQuery,
ICustomerBalanceSummaryStatement,
} from './CustomerBalanceSummary.types';
import { CustomerBalanceSummaryReport } from './CustomerBalanceSummary';
import { CustomerBalanceSummaryRepository } from './CustomerBalanceSummaryRepository';
import { CustomerBalanceSummaryMeta } from './CustomerBalanceSummaryMeta';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Injectable } from '@nestjs/common';
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { Ledger } from '@/modules/Ledger/Ledger';
import { events } from '@/common/events/events';
import { getCustomerBalanceSummaryDefaultQuery } from './_utils';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable()
export class CustomerBalanceSummaryService {
constructor(
private readonly reportRepository: CustomerBalanceSummaryRepository,
private readonly customerBalanceSummaryMeta: CustomerBalanceSummaryMeta,
private readonly eventPublisher: EventEmitter2,
private readonly tenancyContext: TenancyContext,
) {}
/**
* Retrieve the customers ledger entries mapped from accounts transactions.
* @param {Date|string} asDate - The date to retrieve the ledger entries.
* @returns {Promise<ILedgerEntry[]>}
*/
private async getReportCustomersEntries(
asDate: Date | string,
): Promise<ILedgerEntry[]> {
const transactions =
await this.reportRepository.getCustomersTransactions(asDate);
const commonProps = { accountNormal: 'debit', date: asDate };
return R.map(R.merge(commonProps))(transactions);
}
/**
* Retrieve the statment of customer balance summary report.
* @param {ICustomerBalanceSummaryQuery} query - The customer balance summary query.
* @return {Promise<ICustomerBalanceSummaryStatement>}
*/
public async customerBalanceSummary(
query: ICustomerBalanceSummaryQuery,
): Promise<ICustomerBalanceSummaryStatement> {
const tenantMetadata = await this.tenancyContext.getTenantMetadata();
// Merges the default query and request query.
const filter = { ...getCustomerBalanceSummaryDefaultQuery(), ...query };
// Retrieve the customers list ordered by the display name.
const customers = await this.reportRepository.getCustomers(
query.customersIds,
);
// Retrieve the customers debit/credit totals.
const customersEntries = await this.getReportCustomersEntries(
filter.asDate,
);
// Ledger query.
const ledger = new Ledger(customersEntries);
// Report instance.
const report = new CustomerBalanceSummaryReport(
ledger,
customers,
filter,
tenantMetadata.baseCurrency,
);
// Retrieve the customer balance summary meta.
const meta = await this.customerBalanceSummaryMeta.meta(filter);
// Triggers `onCustomerBalanceSummaryViewed` event.
await this.eventPublisher.emitAsync(
events.reports.onCustomerBalanceSummaryViewed,
{
query,
},
);
return {
data: report.reportData(),
query: filter,
meta,
};
}
}

View File

@@ -0,0 +1,38 @@
import { CustomerBalanceSummaryService } from './CustomerBalanceSummaryService';
import {
ICustomerBalanceSummaryQuery,
ICustomerBalanceSummaryTable,
} from './CustomerBalanceSummary.types';
import { CustomerBalanceSummaryTable } from './CustomerBalanceSummaryTableRows';
import { Injectable } from '@nestjs/common';
import { I18nService } from 'nestjs-i18n';
@Injectable()
export class CustomerBalanceSummaryTableInjectable {
constructor(
private readonly customerBalanceSummaryService: CustomerBalanceSummaryService,
private readonly i18n: I18nService,
) {}
/**
* Retrieves the customer balance sheet in table format.
* @param {ICustomerBalanceSummaryQuery} filter - The customer balance summary query.
* @returns {Promise<ICustomerBalanceSummaryTable>}
*/
public async table(
filter: ICustomerBalanceSummaryQuery,
): Promise<ICustomerBalanceSummaryTable> {
const { data, query, meta } =
await this.customerBalanceSummaryService.customerBalanceSummary(filter);
const table = new CustomerBalanceSummaryTable(data, filter, this.i18n);
return {
table: {
columns: table.tableColumns(),
rows: table.tableRows(),
},
query,
meta,
};
}
}

View File

@@ -0,0 +1,156 @@
import * as R from 'ramda';
import { I18nService } from 'nestjs-i18n';
import {
ICustomerBalanceSummaryData,
ICustomerBalanceSummaryCustomer,
ICustomerBalanceSummaryTotal,
ICustomerBalanceSummaryQuery,
} from './CustomerBalanceSummary.types';
import {
IColumnMapperMeta,
ITableColumn,
ITableRow,
} from '../../types/Table.types';
import { tableMapper, tableRowMapper } from '../../utils/Table.utils';
enum TABLE_ROWS_TYPES {
CUSTOMER = 'CUSTOMER',
TOTAL = 'TOTAL',
}
export class CustomerBalanceSummaryTable {
public readonly report: ICustomerBalanceSummaryData;
public readonly query: ICustomerBalanceSummaryQuery;
public readonly i18n: I18nService;
/**
* Constructor method.
* @param {ICustomerBalanceSummaryData} report - The report object.
* @param {ICustomerBalanceSummaryQuery} query - The query object.
* @param {I18nService} i18n - The i18n service.
*/
constructor(
report: ICustomerBalanceSummaryData,
query: ICustomerBalanceSummaryQuery,
i18n: I18nService,
) {
this.report = report;
this.i18n = i18n;
this.query = query;
}
/**
* Retrieve percentage columns accessor.
* @returns {IColumnMapperMeta[]}
*/
private getPercentageColumnsAccessor = (): IColumnMapperMeta[] => {
return [
{
key: 'percentageOfColumn',
accessor: 'percentageOfColumn.formattedAmount',
},
];
};
/**
* Retrieve customer node columns accessor.
* @returns {IColumnMapperMeta[]}
*/
private getCustomerColumnsAccessor = (): IColumnMapperMeta[] => {
const columns = [
{ key: 'name', accessor: 'customerName' },
{ key: 'total', accessor: 'total.formattedAmount' },
];
return R.compose(
R.concat(columns),
R.when(
R.always(this.query.percentageColumn),
R.concat(this.getPercentageColumnsAccessor()),
),
)([]);
};
/**
* Transformes the customers to table rows.
* @param {ICustomerBalanceSummaryCustomer[]} customers
* @returns {ITableRow[]}
*/
private customersTransformer(
customers: ICustomerBalanceSummaryCustomer[],
): ITableRow[] {
const columns = this.getCustomerColumnsAccessor();
return tableMapper(customers, columns, {
rowTypes: [TABLE_ROWS_TYPES.CUSTOMER],
});
}
/**
* Retrieve total node columns accessor.
* @returns {IColumnMapperMeta[]}
*/
private getTotalColumnsAccessor = (): IColumnMapperMeta[] => {
const columns = [
{ key: 'name', value: this.i18n.t('Total') },
{ key: 'total', accessor: 'total.formattedAmount' },
];
return R.compose(
R.concat(columns),
R.when(
R.always(this.query.percentageColumn),
R.concat(this.getPercentageColumnsAccessor()),
),
)([]);
};
/**
* Transformes the total to table row.
* @param {ICustomerBalanceSummaryTotal} total
* @returns {ITableRow}
*/
private totalTransformer = (
total: ICustomerBalanceSummaryTotal,
): ITableRow => {
const columns = this.getTotalColumnsAccessor();
return tableRowMapper(total, columns, {
rowTypes: [TABLE_ROWS_TYPES.TOTAL],
});
};
/**
* Transformes the customer balance summary to table rows.
* @param {ICustomerBalanceSummaryData} customerBalanceSummary
* @returns {ITableRow[]}
*/
public tableRows(): ITableRow[] {
const customers = this.customersTransformer(this.report.customers);
const total = this.totalTransformer(this.report.total);
return customers.length > 0 ? [...customers, total] : [];
}
/**
* Retrieve the report statement columns
* @returns {ITableColumn[]}
*/
public tableColumns = (): ITableColumn[] => {
const columns = [
{
key: 'name',
label: this.i18n.t('contact_summary_balance.account_name'),
},
{ key: 'total', label: this.i18n.t('contact_summary_balance.total') },
];
return R.compose(
R.when(
R.always(this.query.percentageColumn),
R.append({
key: 'percentage_of_column',
label: this.i18n.t('contact_summary_balance.percentage_column'),
}),
),
R.concat(columns),
)([]);
};
}

View File

@@ -0,0 +1,16 @@
export const getCustomerBalanceSummaryDefaultQuery = () => {
return {
asDate: moment().format('YYYY-MM-DD'),
numberFormat: {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'total',
negativeFormat: 'mines',
},
percentageColumn: false,
noneZero: false,
noneTransactions: true,
};
};

View File

@@ -0,0 +1,14 @@
export const HtmlTableCustomCss = `
table tr.row-type--total td {
font-weight: 600;
border-top: 1px solid #bbb;
border-bottom: 3px double #333;
}
table .column--name {
width: 65%;
}
table .column--total,
table .cell--total {
text-align: right;
}
`;

View File

@@ -1,38 +1,33 @@
import { Response } from 'express';
import { castArray } from 'lodash';
import { Headers, Injectable, Query, Res } from '@nestjs/common';
import { BalanceSheetInjectable } from './BalanceSheetInjectable';
import { IBalanceSheetQuery } from './BalanceSheet.types';
import { Controller, Get } from '@nestjs/common';
import { Headers, Query, Res } from '@nestjs/common';
import { IGeneralLedgerSheetQuery } from './GeneralLedger.types';
import { GeneralLedgerApplication } from './GeneralLedgerApplication';
import { AcceptType } from '@/constants/accept-type';
import { BalanceSheetApplication } from './BalanceSheetApplication';
import { Response } from 'express';
import { ApiResponse, ApiTags } from '@nestjs/swagger';
@Injectable()
export class BalanceSheetStatementController {
constructor(private readonly balanceSheetApp: BalanceSheetApplication) {}
@Controller('/reports/general-ledger')
@ApiTags('reports')
export class GeneralLedgerController {
constructor(
private readonly generalLedgerApplication: GeneralLedgerApplication,
) {}
/**
* Retrieve the balance sheet.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public async balanceSheet(
@Query() query: IBalanceSheetQuery,
@Get()
@ApiResponse({ status: 200, description: 'General ledger report' })
public async getGeneralLedger(
@Query() query: IGeneralLedgerSheetQuery,
@Res() res: Response,
@Headers('accept') acceptHeader: string,
) {
const filter = {
...query,
accountsIds: castArray(query.accountsIds),
};
// Retrieves the json table format.
// Retrieves the table format.
if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) {
const table = await this.balanceSheetApp.table(tenantId, filter);
const table = await this.generalLedgerApplication.table(query);
return res.status(200).send(table);
// Retrieves the csv format.
} else if (acceptHeader.includes(AcceptType.ApplicationCsv)) {
const buffer = await this.balanceSheetApp.csv(tenantId, filter);
const buffer = await this.generalLedgerApplication.csv(query);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
@@ -40,7 +35,7 @@ export class BalanceSheetStatementController {
return res.send(buffer);
// Retrieves the xlsx format.
} else if (acceptHeader.includes(AcceptType.ApplicationXlsx)) {
const buffer = await this.balanceSheetApp.xlsx(tenantId, filter);
const buffer = await this.generalLedgerApplication.xlsx(query);
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
res.setHeader(
@@ -50,15 +45,15 @@ export class BalanceSheetStatementController {
return res.send(buffer);
// Retrieves the pdf format.
} else if (acceptHeader.includes(AcceptType.ApplicationPdf)) {
const pdfContent = await this.balanceSheetApp.pdf(tenantId, filter);
const pdfContent = await this.generalLedgerApplication.pdf(query);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
return res.send(pdfContent);
// Retrieves the json format.
} else {
const sheet = await this.balanceSheetApp.sheet(tenantId, filter);
const sheet = await this.generalLedgerApplication.sheet(query);
return res.status(200).send(sheet);
}

View File

@@ -0,0 +1,31 @@
import { Module } from '@nestjs/common';
import { GeneralLedgerRepository } from './GeneralLedgerRepository';
import { GeneralLedgerApplication } from './GeneralLedgerApplication';
import { GeneralLedgerPdf } from './GeneralLedgerPdf';
import { GeneralLedgerExportInjectable } from './GeneralLedgerExport';
import { GeneralLedgerTableInjectable } from './GeneralLedgerTableInjectable';
import { GeneralLedgerService } from './GeneralLedgerService';
import { GeneralLedgerController } from './GeneralLedger.controller';
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
import { GeneralLedgerMeta } from './GeneralLedgerMeta';
import { AccountsModule } from '@/modules/Accounts/Accounts.module';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Module({
imports: [
FinancialSheetCommonModule,
AccountsModule
],
providers: [
GeneralLedgerRepository,
GeneralLedgerApplication,
GeneralLedgerPdf,
GeneralLedgerExportInjectable,
GeneralLedgerTableInjectable,
GeneralLedgerService,
GeneralLedgerMeta,
TenancyContext
],
controllers: [GeneralLedgerController],
})
export class GeneralLedgerModule {}

View File

@@ -0,0 +1,390 @@
import { isEmpty, get, last, head } from 'lodash';
import * as moment from 'moment';
import * as R from 'ramda';
import {
IGeneralLedgerSheetQuery,
IGeneralLedgerSheetAccount,
IGeneralLedgerSheetAccountBalance,
IGeneralLedgerSheetAccountTransaction,
} from './GeneralLedger.types';
import { GeneralLedgerRepository } from './GeneralLedgerRepository';
import { calculateRunningBalance } from './_utils';
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
import { FinancialSheet } from '../../common/FinancialSheet';
import { I18nService } from 'nestjs-i18n';
import { Ledger } from '@/modules/Ledger/Ledger';
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { Account } from '@/modules/Accounts/models/Account.model';
import { ModelObject } from 'objection';
import { flatToNestedArray } from '@/utils/flat-to-nested-array';
import { getTransactionTypeLabel } from '@/modules/BankingTransactions/utils';
export class GeneralLedgerSheet extends R.compose(FinancialSheetStructure)(
FinancialSheet,
) {
public query: IGeneralLedgerSheetQuery;
public baseCurrency: string;
public i18n: I18nService;
public repository: GeneralLedgerRepository;
/**
* Constructor method.
* @param {IGeneralLedgerSheetQuery} query -
* @param {GeneralLedgerRepository} repository -
* @param {I18nService} i18n -
*/
constructor(
query: IGeneralLedgerSheetQuery,
repository: GeneralLedgerRepository,
i18n,
) {
super();
this.query = query;
this.numberFormat = this.query.numberFormat;
this.repository = repository;
this.baseCurrency = this.repository.tenant.metadata.currencyCode;
this.i18n = i18n;
}
/**
* Entry mapper.
* @param {ILedgerEntry} entry -
* @return {IGeneralLedgerSheetAccountTransaction}
*/
private getEntryRunningBalance(
entry: ILedgerEntry,
openingBalance: number,
runningBalance?: number,
): number {
const lastRunningBalance = runningBalance || openingBalance;
const amount = Ledger.getAmount(
entry.credit,
entry.debit,
entry.accountNormal,
);
return calculateRunningBalance(amount, lastRunningBalance);
}
/**
* Maps the given ledger entry to G/L transaction.
* @param {ILedgerEntry} entry
* @param {number} runningBalance
* @returns {IGeneralLedgerSheetAccountTransaction}
*/
private transactionMapper(
entry: ILedgerEntry,
runningBalance: number,
): IGeneralLedgerSheetAccountTransaction {
const contact = this.repository.contactsById.get(entry.contactId);
const amount = Ledger.getAmount(
entry.credit,
entry.debit,
entry.accountNormal,
);
return {
id: entry.id,
date: entry.date,
dateFormatted: moment(entry.date).format('YYYY MMM DD'),
referenceType: entry.transactionType,
referenceId: entry.transactionId,
transactionNumber: entry.transactionNumber,
transactionTypeFormatted: this.i18n.t(
getTransactionTypeLabel(
entry.transactionType,
entry.transactionSubType,
),
),
contactName: get(contact, 'displayName'),
contactType: get(contact, 'contactService'),
transactionType: entry.transactionType,
index: entry.index,
note: entry.note,
credit: entry.credit,
debit: entry.debit,
amount,
runningBalance,
formattedAmount: this.formatNumber(amount, { excerptZero: false }),
formattedCredit: this.formatNumber(entry.credit, { excerptZero: false }),
formattedDebit: this.formatNumber(entry.debit, { excerptZero: false }),
formattedRunningBalance: this.formatNumber(runningBalance, {
excerptZero: false,
}),
currencyCode: this.baseCurrency,
} as IGeneralLedgerSheetAccountTransaction;
}
/**
* Mapping the account transactions to general ledger transactions of the given account.
* @param {IAccount} account
* @return {IGeneralLedgerSheetAccountTransaction[]}
*/
private accountTransactionsMapper(
account: ModelObject<Account>,
openingBalance: number,
): IGeneralLedgerSheetAccountTransaction[] {
const entries = this.repository.transactionsLedger
.whereAccountId(account.id)
.getEntries();
return entries
.reduce((prev: Array<[number, ILedgerEntry]>, current: ILedgerEntry) => {
const prevEntry = last(prev);
const prevRunningBalance = head(prevEntry) as number;
const amount = this.getEntryRunningBalance(
current,
openingBalance,
prevRunningBalance,
);
return [...prev, [amount, current]];
}, [])
.map((entryPair: [number, ILedgerEntry]) => {
const [runningBalance, entry] = entryPair;
return this.transactionMapper(entry, runningBalance);
});
}
/**
* Retrieves the given account opening balance.
* @param {number} accountId
* @returns {number}
*/
private accountOpeningBalance(accountId: number): number {
return this.repository.openingBalanceTransactionsLedger
.whereAccountId(accountId)
.getClosingBalance();
}
/**
* Retrieve the given account opening balance.
* @param {IAccount} account
* @return {IGeneralLedgerSheetAccountBalance}
*/
private accountOpeningBalanceTotal(
accountId: number,
): IGeneralLedgerSheetAccountBalance {
const amount = this.accountOpeningBalance(accountId);
const formattedAmount = this.formatTotalNumber(amount);
const currencyCode = this.baseCurrency;
const date = this.query.fromDate;
return { amount, formattedAmount, currencyCode, date };
}
/**
* Retrieves the given account closing balance.
* @param {number} accountId
* @returns {number}
*/
private accountClosingBalance(accountId: number): number {
const openingBalance = this.repository.openingBalanceTransactionsLedger
.whereAccountId(accountId)
.getClosingBalance();
const transactionsBalance = this.repository.transactionsLedger
.whereAccountId(accountId)
.getClosingBalance();
return openingBalance + transactionsBalance;
}
/**
* Retrieves the given account closing balance.
* @param {IAccount} account
* @return {IGeneralLedgerSheetAccountBalance}
*/
private accountClosingBalanceTotal(
accountId: number,
): IGeneralLedgerSheetAccountBalance {
const amount = this.accountClosingBalance(accountId);
const formattedAmount = this.formatTotalNumber(amount);
const currencyCode = this.baseCurrency;
const date = this.query.toDate;
return { amount, formattedAmount, currencyCode, date };
}
/**
* Retrieves the given account closing balance with subaccounts.
* @param {number} accountId
* @returns {number}
*/
private accountClosingBalanceWithSubaccounts = (
accountId: number,
): number => {
const depsAccountsIds =
this.repository.accountsGraph.dependenciesOf(accountId);
const openingBalance = this.repository.openingBalanceTransactionsLedger
.whereAccountsIds([...depsAccountsIds, accountId])
.getClosingBalance();
const transactionsBalanceWithSubAccounts =
this.repository.transactionsLedger
.whereAccountsIds([...depsAccountsIds, accountId])
.getClosingBalance();
const closingBalance = openingBalance + transactionsBalanceWithSubAccounts;
return closingBalance;
};
/**
* Retrieves the closing balance with subaccounts total node.
* @param {number} accountId
* @returns {IGeneralLedgerSheetAccountBalance}
*/
private accountClosingBalanceWithSubaccountsTotal = (
accountId: number,
): IGeneralLedgerSheetAccountBalance => {
const amount = this.accountClosingBalanceWithSubaccounts(accountId);
const formattedAmount = this.formatTotalNumber(amount);
const currencyCode = this.baseCurrency;
const date = this.query.toDate;
return { amount, formattedAmount, currencyCode, date };
};
/**
* Detarmines whether the closing balance subaccounts node should be exist.
* @param {number} accountId
* @returns {boolean}
*/
private isAccountNodeIncludesClosingSubaccounts = (accountId: number) => {
// Retrun early if there is no accounts in the filter so
// return closing subaccounts in all cases.
if (isEmpty(this.query.accountsIds)) {
return true;
}
// Returns true if the given account id includes transactions.
return this.repository.accountNodesIncludeTransactions.includes(accountId);
};
/**
* Retreive general ledger accounts sections.
* @param {IAccount} account
* @return {IGeneralLedgerSheetAccount}
*/
private accountMapper = (
account: ModelObject<Account>,
): IGeneralLedgerSheetAccount => {
const openingBalance = this.accountOpeningBalanceTotal(account.id);
const transactions = this.accountTransactionsMapper(
account,
openingBalance.amount,
);
const closingBalance = this.accountClosingBalanceTotal(account.id);
const closingBalanceSubaccounts =
this.accountClosingBalanceWithSubaccountsTotal(account.id);
const initialNode = {
id: account.id,
name: account.name,
code: account.code,
index: account.index,
parentAccountId: account.parentAccountId,
openingBalance,
transactions,
closingBalance,
};
return R.compose(
R.when(
() => this.isAccountNodeIncludesClosingSubaccounts(account.id),
R.assoc('closingBalanceSubaccounts', closingBalanceSubaccounts),
),
)(initialNode);
};
/**
* Maps over deep nodes to retrieve the G/L account node.
* @param {IAccount[]} accounts
* @returns {IGeneralLedgerSheetAccount[]}
*/
private accountNodesDeepMap = (
accounts: ModelObject<Account>[],
): IGeneralLedgerSheetAccount[] => {
return this.mapNodesDeep(accounts, this.accountMapper);
};
/**
* Transformes the flatten nodes to nested nodes.
* @param {ModelObject<Account>[]} flattenAccounts -
* @returns {ModelObject<Account>[]}
*/
private nestedAccountsNode = (
flattenAccounts: ModelObject<Account>[],
): ModelObject<Account>[] => {
return flatToNestedArray(flattenAccounts, {
id: 'id',
parentId: 'parentAccountId',
});
};
/**
* Filters account nodes.
* @param {IGeneralLedgerSheetAccount[]} nodes
* @returns {IGeneralLedgerSheetAccount[]}
*/
private filterAccountNodesByTransactionsFilter = (
nodes: IGeneralLedgerSheetAccount[],
): IGeneralLedgerSheetAccount[] => {
return this.filterNodesDeep(
nodes,
(account: IGeneralLedgerSheetAccount) =>
!(account.transactions.length === 0 && this.query.noneTransactions),
);
};
/**
* Filters account nodes by the acounts filter.
* @param {IAccount[]} nodes
* @returns {IAccount[]}
*/
private filterAccountNodesByAccountsFilter = (
nodes: ModelObject<Account>[],
): ModelObject<Account>[] => {
return this.filterNodesDeep(nodes, (node: IGeneralLedgerSheetAccount) => {
if (R.isEmpty(this.query.accountsIds)) {
return true;
}
// Returns true if the given account id exists in the filter.
return this.repository.accountNodeInclude?.includes(node.id);
});
};
/**
* Retrieves mapped accounts with general ledger transactions and
* opeing/closing balance.
* @param {ModelObject<Account>[]} accounts -
* @return {IGeneralLedgerSheetAccount[]}
*/
private accountsWalker(
accounts: ModelObject<Account>[],
): IGeneralLedgerSheetAccount[] {
return R.compose(
R.defaultTo([]),
this.filterAccountNodesByTransactionsFilter,
this.accountNodesDeepMap,
R.defaultTo([]),
this.filterAccountNodesByAccountsFilter,
this.nestedAccountsNode,
)(accounts);
}
/**
* Retrieves general ledger report data.
* @return {IGeneralLedgerSheetAccount[]}
*/
public reportData(): IGeneralLedgerSheetAccount[] {
return this.accountsWalker(this.repository.accounts);
}
}

View File

@@ -0,0 +1,92 @@
import { IFinancialSheetCommonMeta } from "../../types/Report.types";
import { IFinancialTable } from "../../types/Table.types";
export interface IGeneralLedgerSheetQuery {
fromDate: Date | string;
toDate: Date | string;
basis: string;
numberFormat: {
noCents: boolean;
divideOn1000: boolean;
};
noneTransactions: boolean;
accountsIds: number[];
branchesIds?: number[];
}
export interface IGeneralLedgerSheetAccountTransaction {
id: number;
amount: number;
runningBalance: number;
credit: number;
debit: number;
formattedAmount: string;
formattedCredit: string;
formattedDebit: string;
formattedRunningBalance: string;
currencyCode: string;
note?: string;
transactionTypeFormatted: string;
transactionNumber: string;
referenceId?: number;
referenceType?: string;
date: Date | string;
dateFormatted: string;
}
export interface IGeneralLedgerSheetAccountBalance {
date: Date | string;
amount: number;
formattedAmount: string;
currencyCode: string;
}
export interface IGeneralLedgerSheetAccount {
id: number;
name: string;
code: string;
index: number;
parentAccountId: number;
transactions: IGeneralLedgerSheetAccountTransaction[];
openingBalance: IGeneralLedgerSheetAccountBalance;
closingBalance: IGeneralLedgerSheetAccountBalance;
closingBalanceSubaccounts?: IGeneralLedgerSheetAccountBalance;
children?: IGeneralLedgerSheetAccount[];
}
export type IGeneralLedgerSheetData = IGeneralLedgerSheetAccount[];
export interface IAccountTransaction {
id: number;
index: number;
draft: boolean;
note: string;
accountId: number;
transactionType: string;
referenceType: string;
referenceId: number;
contactId: number;
contactType: string;
credit: number;
debit: number;
date: string | Date;
createdAt: string | Date;
updatedAt: string | Date;
}
export interface IGeneralLedgerMeta extends IFinancialSheetCommonMeta {
formattedFromDate: string;
formattedToDate: string;
formattedDateRange: string;
}
export interface IGeneralLedgerTableData extends IFinancialTable {
meta: IGeneralLedgerMeta;
query: IGeneralLedgerSheetQuery;
}

View File

@@ -0,0 +1,70 @@
import {
IGeneralLedgerSheetQuery,
IGeneralLedgerTableData,
} from './GeneralLedger.types';
import { GeneralLedgerTableInjectable } from './GeneralLedgerTableInjectable';
import { GeneralLedgerExportInjectable } from './GeneralLedgerExport';
import { GeneralLedgerService } from './GeneralLedgerService';
import { GeneralLedgerPdf } from './GeneralLedgerPdf';
import { Injectable } from '@nestjs/common';
@Injectable()
export class GeneralLedgerApplication {
constructor(
private readonly GLTable: GeneralLedgerTableInjectable,
private readonly GLExport: GeneralLedgerExportInjectable,
private readonly GLSheet: GeneralLedgerService,
private readonly GLPdf: GeneralLedgerPdf,
) {}
/**
* Retrieves the G/L sheet in json format.
* @param {IGeneralLedgerSheetQuery} query
*/
public sheet(query: IGeneralLedgerSheetQuery) {
return this.GLSheet.generalLedger(query);
}
/**
* Retrieves the G/L sheet in table format.
* @param {IGeneralLedgerSheetQuery} query
* @returns {Promise<IGeneralLedgerTableData>}
*/
public table(
query: IGeneralLedgerSheetQuery,
): Promise<IGeneralLedgerTableData> {
return this.GLTable.table(query);
}
/**
* Retrieves the G/L sheet in xlsx format.
* @param {IGeneralLedgerSheetQuery} query
* @returns {}
*/
public xlsx(
query: IGeneralLedgerSheetQuery,
): Promise<Buffer> {
return this.GLExport.xlsx(query);
}
/**
* Retrieves the G/L sheet in csv format.
* @param {IGeneralLedgerSheetQuery} query -
*/
public csv(
query: IGeneralLedgerSheetQuery,
): Promise<string> {
return this.GLExport.csv(query);
}
/**
* Retrieves the G/L sheet in pdf format.
* @param {IGeneralLedgerSheetQuery} query
* @returns {Promise<Buffer>}
*/
public pdf(
query: IGeneralLedgerSheetQuery,
): Promise<Buffer> {
return this.GLPdf.pdf(query);
}
}

View File

@@ -0,0 +1,42 @@
import { Injectable } from '@nestjs/common';
import { GeneralLedgerTableInjectable } from './GeneralLedgerTableInjectable';
import { IGeneralLedgerSheetQuery } from './GeneralLedger.types';
import { TableSheet } from '../../common/TableSheet';
@Injectable()
export class GeneralLedgerExportInjectable {
constructor(
private readonly generalLedgerTable: GeneralLedgerTableInjectable
) {}
/**
* Retrieves the general ledger sheet in XLSX format.
* @param {IGeneralLedgerSheetQuery} query - General ledger sheet query.
* @returns {Promise<Buffer>}
*/
public async xlsx(query: IGeneralLedgerSheetQuery) {
const table = await this.generalLedgerTable.table(query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToXLSX();
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
}
/**
* Retrieves the general ledger sheet in CSV format.
* @param {IGeneralLedgerSheetQuery} query - General ledger sheet query.
* @returns {Promise<Buffer>}
*/
public async csv(
query: IGeneralLedgerSheetQuery
): Promise<string> {
const table = await this.generalLedgerTable.table(query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
}

View File

@@ -0,0 +1,34 @@
import {
IGeneralLedgerMeta,
IGeneralLedgerSheetQuery,
} from './GeneralLedger.types';
import moment from 'moment';
import { Injectable } from '@nestjs/common';
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
@Injectable()
export class GeneralLedgerMeta {
constructor(private readonly financialSheetMeta: FinancialSheetMeta) {}
/**
* Retrieve the general ledger meta.
* @returns {IGeneralLedgerMeta}
*/
public async meta(
query: IGeneralLedgerSheetQuery,
): Promise<IGeneralLedgerMeta> {
const commonMeta = 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}`;
return {
...commonMeta,
sheetName: 'Balance Sheet',
formattedFromDate,
formattedToDate,
formattedDateRange,
};
}
}

View File

@@ -0,0 +1,29 @@
import { TableSheetPdf } from '../../common/TableSheetPdf';
import { GeneralLedgerTableInjectable } from './GeneralLedgerTableInjectable';
import { IGeneralLedgerSheetQuery } from './GeneralLedger.types';
import { HtmlTableCustomCss } from './constants';
import { Injectable } from '@nestjs/common';
@Injectable()
export class GeneralLedgerPdf {
constructor(
private readonly generalLedgerTable: GeneralLedgerTableInjectable,
private readonly tableSheetPdf: TableSheetPdf,
) {}
/**
* Converts the general ledger sheet table to pdf.
* @param {IGeneralLedgerSheetQuery} query - General ledger sheet query.
* @returns {Promise<Buffer>}
*/
public async pdf(query: IGeneralLedgerSheetQuery): Promise<Buffer> {
const table = await this.generalLedgerTable.table(query);
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCustomCss,
);
}
}

View File

@@ -0,0 +1,180 @@
import moment from 'moment';
import * as R from 'ramda';
import {
IGeneralLedgerSheetQuery,
} from './GeneralLedger.types';
import { flatten, isEmpty, uniq } from 'lodash';
import { ModelObject } from 'objection';
import { Account } from '@/modules/Accounts/models/Account.model';
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
import { Contact } from '@/modules/Contacts/models/Contact';
import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository';
import { Inject } from '@nestjs/common';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { transformToMap } from '@/utils/transform-to-key';
import { Ledger } from '@/modules/Ledger/Ledger';
import { TenantModel } from '@/modules/System/models/TenantModel';
export class GeneralLedgerRepository {
public filter: IGeneralLedgerSheetQuery;
public accounts: Account[];
public transactions: AccountTransaction[];
public openingBalanceTransactions: AccountTransaction[];
public transactionsLedger: Ledger;
public openingBalanceTransactionsLedger: Ledger;
public repositories: any;
public models: any;
public accountsGraph: any;
public contacts: ModelObject<Contact>[];
public contactsById: Map<number, ModelObject<Contact>>;
public tenantId: number;
public tenant: TenantModel;
public accountNodesIncludeTransactions: Array<number> = [];
public accountNodeInclude: Array<number> = [];
@Inject(AccountRepository)
private readonly accountRepository: AccountRepository;
@Inject(TenancyContext)
private readonly tenancyContext: TenancyContext;
/**
* Set the filter.
* @param {IGeneralLedgerSheetQuery} filter - The filter.
*/
setFilter(filter: IGeneralLedgerSheetQuery) {
this.filter = filter;
}
/**
* Initialize the G/L report.
*/
public async asyncInitialize() {
await this.initTenant();
await this.initAccounts();
await this.initAccountsGraph();
await this.initContacts();
await this.initAccountsOpeningBalance();
this.initAccountNodesIncludeTransactions();
await this.initTransactions();
this.initAccountNodesIncluded();
}
/**
* Initialize the tenant.
*/
public async initTenant() {
this.tenant = await this.tenancyContext.getTenant(true);
}
/**
* Initialize the accounts.
*/
public async initAccounts() {
this.accounts = await this.accountRepository
.all()
.orderBy('name', 'ASC');
}
/**
* Initialize the accounts graph.
*/
public async initAccountsGraph() {
this.accountsGraph =
await this.repositories.accountRepository.getDependencyGraph();
}
/**
* Initialize the contacts.
*/
public async initContacts() {
this.contacts = await this.repositories.contactRepository.all();
this.contactsById = transformToMap(this.contacts, 'id');
}
/**
* Initialize the G/L transactions from/to the given date.
*/
public async initTransactions() {
this.transactions = await this.repositories.transactionsRepository
.journal({
fromDate: this.filter.fromDate,
toDate: this.filter.toDate,
branchesIds: this.filter.branchesIds,
})
.orderBy('date', 'ASC')
.onBuild((query) => {
if (this.filter.accountsIds?.length > 0) {
query.whereIn('accountId', this.accountNodesIncludeTransactions);
}
});
// Transform array transactions to journal collection.
this.transactionsLedger = Ledger.fromTransactions(this.transactions);
}
/**
* Initialize the G/L accounts opening balance.
*/
public async initAccountsOpeningBalance() {
// Retreive opening balance credit/debit sumation.
this.openingBalanceTransactions =
await this.repositories.transactionsRepository.journal({
toDate: moment(this.filter.fromDate).subtract(1, 'day'),
sumationCreditDebit: true,
branchesIds: this.filter.branchesIds,
});
// Accounts opening transactions.
this.openingBalanceTransactionsLedger = Ledger.fromTransactions(
this.openingBalanceTransactions
);
}
/**
* Initialize the account nodes that should include transactions.
* @returns {void}
*/
public initAccountNodesIncludeTransactions() {
if (isEmpty(this.filter.accountsIds)) {
return;
}
const childrenNodeIds = this.filter.accountsIds?.map(
(accountId: number) => {
return this.accountsGraph.dependenciesOf(accountId);
}
);
const nodeIds = R.concat(this.filter.accountsIds, childrenNodeIds);
this.accountNodesIncludeTransactions = uniq(flatten(nodeIds));
}
/**
* Initialize the account node ids should be included,
* if the filter by acounts is presented.
* @returns {void}
*/
public initAccountNodesIncluded() {
if (isEmpty(this.filter.accountsIds)) {
return;
}
const nodeIds = this.filter.accountsIds.map((accountId) => {
const childrenIds = this.accountsGraph.dependenciesOf(accountId);
const parentIds = this.accountsGraph.dependantsOf(accountId);
return R.concat(childrenIds, parentIds);
});
this.accountNodeInclude = R.compose(
R.uniq,
R.flatten,
R.concat(this.filter.accountsIds)
)(nodeIds);
}
}

View File

@@ -0,0 +1,64 @@
import * as moment from 'moment';
import { GeneralLedgerMeta } from './GeneralLedgerMeta';
import { GeneralLedgerRepository } from './GeneralLedgerRepository';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Injectable } from '@nestjs/common';
import { GeneralLedgerSheet } from './GeneralLedger';
import { events } from '@/common/events/events';
import { getGeneralLedgerReportQuery } from './_utils';
import {
IGeneralLedgerMeta,
IGeneralLedgerSheetQuery,
} from './GeneralLedger.types';
import { I18nService } from 'nestjs-i18n';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable()
export class GeneralLedgerService {
constructor(
private readonly generalLedgerMeta: GeneralLedgerMeta,
private readonly eventEmitter: EventEmitter2,
private readonly generalLedgerRepository: GeneralLedgerRepository,
private readonly i18n: I18nService,
) {}
/**
* Retrieve general ledger report statement.
* @param {IGeneralLedgerSheetQuery} query
* @return {Promise<IGeneralLedgerStatement>}
*/
public async generalLedger(query: IGeneralLedgerSheetQuery): Promise<{
data: any;
query: IGeneralLedgerSheetQuery;
meta: IGeneralLedgerMeta;
}> {
const filter = {
...getGeneralLedgerReportQuery(),
...query,
};
this.generalLedgerRepository.setFilter(filter);
await this.generalLedgerRepository.asyncInitialize();
// General ledger report instance.
const generalLedgerInstance = new GeneralLedgerSheet(
filter,
this.generalLedgerRepository,
this.i18n,
);
// Retrieve general ledger report data.
const reportData = generalLedgerInstance.reportData();
// Retrieve general ledger report metadata.
const meta = await this.generalLedgerMeta.meta(filter);
// Triggers `onGeneralLedgerViewed` event.
await this.eventEmitter.emitAsync(events.reports.onGeneralLedgerViewed, {
});
return {
data: reportData,
query: filter,
meta,
};
}
}

View File

@@ -0,0 +1,324 @@
import * as R from 'ramda';
import {
IGeneralLedgerMeta,
IGeneralLedgerSheetAccount,
IGeneralLedgerSheetAccountTransaction,
IGeneralLedgerSheetData,
IGeneralLedgerSheetQuery,
} from './GeneralLedger.types';
import { FinancialSheet } from '../../common/FinancialSheet';
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
import { FinancialTable } from '../../common/FinancialTable';
import { ROW_TYPE } from './utils';
import {
IColumnMapperMeta,
ITableColumn,
ITableColumnAccessor,
ITableRow,
} from '../../types/Table.types';
import { tableRowMapper } from '../../utils/Table.utils';
export class GeneralLedgerTable extends R.compose(
FinancialTable,
FinancialSheetStructure,
)(FinancialSheet) {
private data: IGeneralLedgerSheetData;
private query: IGeneralLedgerSheetQuery;
private meta: IGeneralLedgerMeta;
/**
* Creates an instance of `GeneralLedgerTable`.
* @param {IGeneralLedgerSheetData} data
* @param {IGeneralLedgerSheetQuery} query
*/
constructor(
data: IGeneralLedgerSheetData,
query: IGeneralLedgerSheetQuery,
meta: IGeneralLedgerMeta,
) {
super();
this.data = data;
this.query = query;
this.meta = meta;
}
/**
* Retrieves the common table accessors.
* @returns {ITableColumnAccessor[]}
*/
private accountColumnsAccessors(): ITableColumnAccessor[] {
return [
{ key: 'date', accessor: 'name' },
{ key: 'account_name', accessor: '_empty_' },
{ key: 'reference_type', accessor: '_empty_' },
{ key: 'reference_number', accessor: '_empty_' },
{ key: 'description', accessor: 'description' },
{ key: 'credit', accessor: '_empty_' },
{ key: 'debit', accessor: '_empty_' },
{ key: 'amount', accessor: 'amount.formattedAmount' },
{ key: 'running_balance', accessor: 'closingBalance.formattedAmount' },
];
}
/**
* Retrieves the transaction column accessors.
* @returns {ITableColumnAccessor[]}
*/
private transactionColumnAccessors(): ITableColumnAccessor[] {
return [
{ key: 'date', accessor: 'dateFormatted' },
{ key: 'account_name', accessor: 'account.name' },
{ key: 'reference_type', accessor: 'transactionTypeFormatted' },
{ key: 'reference_number', accessor: 'transactionNumber' },
{ key: 'description', accessor: 'note' },
{ key: 'credit', accessor: 'formattedCredit' },
{ key: 'debit', accessor: 'formattedDebit' },
{ key: 'amount', accessor: 'formattedAmount' },
{ key: 'running_balance', accessor: 'formattedRunningBalance' },
];
}
/**
* Retrieves the opening row column accessors.
* @returns {ITableRowIColumnMapperMeta[]}
*/
private openingBalanceColumnsAccessors(): IColumnMapperMeta[] {
return [
{ key: 'date', value: 'Opening Balance' },
{ key: 'account_name', value: '' },
{ key: 'reference_type', accessor: '_empty_' },
{ key: 'reference_number', accessor: '_empty_' },
{ key: 'description', accessor: 'description' },
{ key: 'credit', accessor: '_empty_' },
{ key: 'debit', accessor: '_empty_' },
{ key: 'amount', accessor: 'openingBalance.formattedAmount' },
{ key: 'running_balance', accessor: 'openingBalance.formattedAmount' },
];
}
/**
* Closing balance row column accessors.
* @param {IGeneralLedgerSheetAccount} account -
* @returns {ITableColumnAccessor[]}
*/
private closingBalanceColumnAccessors(
account: IGeneralLedgerSheetAccount,
): IColumnMapperMeta[] {
return [
{ key: 'date', value: `Closing balance for ${account.name}` },
{ key: 'account_name', value: `` },
{ key: 'reference_type', accessor: '_empty_' },
{ key: 'reference_number', accessor: '_empty_' },
{ key: 'description', accessor: '_empty_' },
{ key: 'credit', accessor: '_empty_' },
{ key: 'debit', accessor: '_empty_' },
{ key: 'amount', accessor: 'closingBalance.formattedAmount' },
{ key: 'running_balance', accessor: 'closingBalance.formattedAmount' },
];
}
/**
* Closing balance row column accessors.
* @param {IGeneralLedgerSheetAccount} account -
* @returns {ITableColumnAccessor[]}
*/
private closingBalanceWithSubaccountsColumnAccessors(
account: IGeneralLedgerSheetAccount,
): IColumnMapperMeta[] {
return [
{
key: 'date',
value: `Closing Balance for ${account.name} with sub-accounts`,
},
{
key: 'account_name',
value: ``,
},
{ key: 'reference_type', accessor: '_empty_' },
{ key: 'reference_number', accessor: '_empty_' },
{ key: 'description', accessor: '_empty_' },
{ key: 'credit', accessor: '_empty_' },
{ key: 'debit', accessor: '_empty_' },
{ key: 'amount', accessor: 'closingBalanceSubaccounts.formattedAmount' },
{
key: 'running_balance',
accessor: 'closingBalanceSubaccounts.formattedAmount',
},
];
}
/**
* Retrieves the common table columns.
* @returns {ITableColumn[]}
*/
private commonColumns(): ITableColumn[] {
return [
{ key: 'date', label: 'Date' },
{ key: 'account_name', label: 'Account Name' },
{ key: 'reference_type', label: 'Transaction Type' },
{ key: 'reference_number', label: 'Transaction #' },
{ key: 'description', label: 'Description' },
{ key: 'credit', label: 'Credit' },
{ key: 'debit', label: 'Debit' },
{ key: 'amount', label: 'Amount' },
{ key: 'running_balance', label: 'Running Balance' },
];
}
/**
* Maps the given transaction node to table row.
* @param {IGeneralLedgerSheetAccountTransaction} transaction
* @returns {ITableRow}
*/
private transactionMapper = R.curry(
(
account: IGeneralLedgerSheetAccount,
transaction: IGeneralLedgerSheetAccountTransaction,
): ITableRow => {
const columns = this.transactionColumnAccessors();
const data = { ...transaction, account };
const meta = {
rowTypes: [ROW_TYPE.TRANSACTION],
};
return tableRowMapper(data, columns, meta);
},
);
/**
* Maps the given transactions nodes to table rows.
* @param {IGeneralLedgerSheetAccountTransaction[]} transactions
* @returns {ITableRow[]}
*/
private transactionsMapper = (
account: IGeneralLedgerSheetAccount,
): ITableRow[] => {
const transactionMapper = this.transactionMapper(account);
return R.map(transactionMapper)(account.transactions);
};
/**
* Maps the given account node to opening balance table row.
* @param {IGeneralLedgerSheetAccount} account
* @returns {ITableRow}
*/
private openingBalanceMapper = (
account: IGeneralLedgerSheetAccount,
): ITableRow => {
const columns = this.openingBalanceColumnsAccessors();
const meta = {
rowTypes: [ROW_TYPE.OPENING_BALANCE],
};
return tableRowMapper(account, columns, meta);
};
/**
* Maps the given account node to closing balance table row.
* @param {IGeneralLedgerSheetAccount} account
* @returns {ITableRow}
*/
private closingBalanceMapper = (account: IGeneralLedgerSheetAccount) => {
const columns = this.closingBalanceColumnAccessors(account);
const meta = {
rowTypes: [ROW_TYPE.CLOSING_BALANCE],
};
return tableRowMapper(account, columns, meta);
};
/**
* Maps the given account node to opening balance table row.
* @param {IGeneralLedgerSheetAccount} account
* @returns {ITableRow}
*/
private closingBalanceWithSubaccountsMapper = (
account: IGeneralLedgerSheetAccount,
): ITableRow => {
const columns = this.closingBalanceWithSubaccountsColumnAccessors(account);
const meta = {
rowTypes: [ROW_TYPE.CLOSING_BALANCE],
};
return tableRowMapper(account, columns, meta);
};
/**
* Maps the given account node to transactions table rows.
* @param {IGeneralLedgerSheetAccount} account
* @returns {ITableRow[]}
*/
private transactionsNode = (
account: IGeneralLedgerSheetAccount,
): ITableRow[] => {
const openingBalance = this.openingBalanceMapper(account);
const transactions = this.transactionsMapper(account);
const closingBalance = this.closingBalanceMapper(account);
return R.when(
R.always(R.not(R.isEmpty(transactions))),
R.prepend(openingBalance),
)([...transactions, closingBalance]) as ITableRow[];
};
/**
* Maps the given account node to the table rows.
* @param {IGeneralLedgerSheetAccount} account
* @returns {ITableRow}
*/
private accountMapper = (account: IGeneralLedgerSheetAccount): ITableRow => {
const columns = this.accountColumnsAccessors();
const transactions = this.transactionsNode(account);
const meta = {
rowTypes: [ROW_TYPE.ACCOUNT],
};
const row = tableRowMapper(account, columns, meta);
const closingBalanceWithSubaccounts =
this.closingBalanceWithSubaccountsMapper(account);
// Appends the closing balance with sub-accounts row if the account
// has children accounts and the node is define.
const isAppendClosingSubaccounts = () =>
account.children?.length > 0 && !!account.closingBalanceSubaccounts;
const children = R.compose(
R.when(
isAppendClosingSubaccounts,
R.append(closingBalanceWithSubaccounts),
),
R.concat(R.defaultTo([], transactions)),
R.when(
() => account?.children?.length > 0,
R.concat(R.defaultTo([], account.children)),
),
)([]);
return R.assoc('children', children)(row);
};
/**
* Maps the given account node to table rows.
* @param {IGeneralLedgerSheetAccount[]} accounts
* @returns {ITableRow[]}
*/
private accountsMapper = (
accounts: IGeneralLedgerSheetAccount[],
): ITableRow[] => {
return this.mapNodesDeepReverse(accounts, this.accountMapper);
};
/**
* Retrieves the table rows.
* @returns {ITableRow[]}
*/
public tableRows(): ITableRow[] {
return R.compose(this.accountsMapper)(this.data);
}
/**
* Retrieves the table columns.
* @returns {ITableColumn[]}
*/
public tableColumns(): ITableColumn[] {
const columns = this.commonColumns();
return R.compose(this.tableColumnsCellIndexing)(columns);
}
}

View File

@@ -0,0 +1,38 @@
import {
IGeneralLedgerSheetQuery,
IGeneralLedgerTableData,
} from './GeneralLedger.types';
import { GeneralLedgerService } from './GeneralLedgerService';
import { GeneralLedgerTable } from './GeneralLedgerTable';
import { Injectable } from '@nestjs/common';
@Injectable()
export class GeneralLedgerTableInjectable {
constructor(private readonly GLSheet: GeneralLedgerService) {}
/**
* Retrieves the G/L table.
* @param {IGeneralLedgerSheetQuery} query
* @returns {Promise<IGeneralLedgerTableData>}
*/
public async table(
query: IGeneralLedgerSheetQuery,
): Promise<IGeneralLedgerTableData> {
const {
data: sheetData,
query: sheetQuery,
meta: sheetMeta,
} = await this.GLSheet.generalLedger(query);
const table = new GeneralLedgerTable(sheetData, sheetQuery, sheetMeta);
return {
table: {
columns: table.tableColumns(),
rows: table.tableRows(),
},
query: sheetQuery,
meta: sheetMeta,
};
}
}

View File

@@ -0,0 +1,28 @@
/**
* Calculate the running balance.
* @param {number} amount - Transaction amount.
* @param {number} lastRunningBalance - Last running balance.
* @param {number} openingBalance - Opening balance.
* @return {number} Running balance.
*/
export function calculateRunningBalance(
amount: number,
lastRunningBalance: number,
): number {
return amount + lastRunningBalance;
}
export const getGeneralLedgerReportQuery = (
) => {
return {
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'),
basis: 'cash',
numberFormat: {
noCents: false,
divideOn1000: false,
},
noneZero: false,
accountsIds: [],
};
};

View File

@@ -0,0 +1,29 @@
export const HtmlTableCustomCss = `
table tr:last-child td {
border-bottom: 1px solid #ececec;
}
table tr.row-type--account td,
table tr.row-type--opening-balance td,
table tr.row-type--closing-balance td{
font-weight: 600;
}
table tr.row-type--closing-balance td {
border-bottom: 1px solid #ececec;
}
table .column--debit,
table .column--credit,
table .column--amount,
table .column--running_balance,
table .cell--debit,
table .cell--credit,
table .cell--amount,
table .cell--running_balance{
text-align: right;
}
table tr.row-type--account .cell--date span,
table tr.row-type--opening-balance .cell--account_name span,
table tr.row-type--closing-balance .cell--account_name span{
white-space: nowrap;
}
`;

View File

@@ -0,0 +1,6 @@
export enum ROW_TYPE {
ACCOUNT = 'ACCOUNT',
OPENING_BALANCE = 'OPENING_BALANCE',
TRANSACTION = 'TRANSACTION',
CLOSING_BALANCE = 'CLOSING_BALANCE',
}

View File

@@ -3,14 +3,19 @@ import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
import { PurchasesByItemsApplication } from './PurchasesByItemsApplication';
import { IPurchasesByItemsReportQuery } from './types/PurchasesByItems.types';
import { AcceptType } from '@/constants/accept-type';
import { PublicRoute } from '@/modules/Auth/Jwt.guard';
import { ApiResponse, ApiTags } from '@nestjs/swagger';
@Controller('/reports/purchases-by-items')
@PublicRoute()
@ApiTags('reports')
export class PurchasesByItemReportController {
constructor(
private readonly purchasesByItemsApp: PurchasesByItemsApplication,
) {}
@Get()
@ApiResponse({ status: 200, description: 'Purchases by items report' })
async purchasesByItems(
@Query() filter: IPurchasesByItemsReportQuery,
@Res() res: Response,

View File

@@ -1,18 +1,25 @@
import { Module } from '@nestjs/common';
import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable';
import { PurchasesByItemsService } from './PurchasesByItemsService';
import { PurchasesByItemsService } from './PurchasesByItems.service';
import { PurchasesByItemsPdf } from './PurchasesByItemsPdf';
import { PurchasesByItemsExport } from './PurchasesByItemsExport';
import { PurchasesByItemsApplication } from './PurchasesByItemsApplication';
import { PurchasesByItemReportController } from './PurchasesByItems.controller';
import { PurchasesByItemsMeta } from './PurchasesByItemsMeta';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { InventoryCostModule } from '@/modules/InventoryCost/InventoryCost.module';
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
@Module({
imports: [InventoryCostModule, FinancialSheetCommonModule],
providers: [
PurchasesByItemsTableInjectable,
PurchasesByItemsService,
PurchasesByItemsExport,
PurchasesByItemsPdf,
PurchasesByItemsMeta,
PurchasesByItemsApplication,
TenancyContext,
],
exports: [PurchasesByItemsApplication],
controllers: [PurchasesByItemReportController],

View File

@@ -1,4 +1,3 @@
import moment from 'moment';
import { Inject, Injectable } from '@nestjs/common';
import { PurchasesByItems } from './PurchasesByItems';
import {
@@ -11,9 +10,17 @@ import { InventoryTransaction } from '@/modules/InventoryCost/models/InventoryTr
import { Item } from '@/modules/Items/models/Item';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { events } from '@/common/events/events';
import { getPurchasesByItemsDefaultQuery } from './utils';
@Injectable()
export class PurchasesByItemsService {
/**
* @param {PurchasesByItemsMeta} purchasesByItemsMeta - The purchases by items meta.
* @param {EventEmitter2} eventPublisher - The event emitter.
* @param {TenancyContext} tenancyContext - The tenancy context.
* @param {typeof InventoryTransaction} inventoryTransactionModel - The inventory transaction model.
* @param {typeof Item} itemModel - The item model.
*/
constructor(
private readonly purchasesByItemsMeta: PurchasesByItemsMeta,
private readonly eventPublisher: EventEmitter2,
@@ -27,39 +34,16 @@ export class PurchasesByItemsService {
) {}
/**
* Defaults purchases by items filter query.
* @return {IPurchasesByItemsReportQuery}
*/
private get defaultQuery(): IPurchasesByItemsReportQuery {
return {
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'),
itemsIds: [],
numberFormat: {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'always',
negativeFormat: 'mines',
},
noneTransactions: true,
onlyActive: false,
};
}
/**
* Retrieve balance sheet statement.
* -------------
* @param {number} tenantId
* @param {IPurchasesByItemsReportQuery} query
* @return {Promise<IPurchasesByItemsSheet>}
* Retrieve purchases by items statement.
* @param {IPurchasesByItemsReportQuery} query - Purchases by items report query.
* @return {Promise<IPurchasesByItemsSheet>} - Purchases by items sheet.
*/
public async purchasesByItems(
query: IPurchasesByItemsReportQuery,
): Promise<IPurchasesByItemsSheet> {
const tenant = await this.tenancyContext.getTenant();
const tenantMetadata = await this.tenancyContext.getTenantMetadata();
const filter = {
...this.defaultQuery,
...getPurchasesByItemsDefaultQuery(),
...query,
};
const inventoryItems = await this.itemModel.query().onBuild((q) => {
@@ -88,7 +72,7 @@ export class PurchasesByItemsService {
filter,
inventoryItems,
inventoryTransactions,
tenant.metadata.baseCurrency,
tenantMetadata.baseCurrency,
);
const purchasesByItemsData = purchasesByItemsInstance.reportData();

View File

@@ -7,7 +7,7 @@ import {
IPurchasesByItemsSheetData,
IPurchasesByItemsTotal,
} from './types/PurchasesByItems.types';
import FinancialSheet from '../../common/FinancialSheet';
import { FinancialSheet } from '../../common/FinancialSheet';
import { transformToMapBy } from '@/utils/transform-to-map-by';
import { Item } from '@/modules/Items/models/Item';
import { InventoryTransaction } from '@/modules/InventoryCost/models/InventoryTransaction';
@@ -48,7 +48,7 @@ export class PurchasesByItems extends FinancialSheet{
cost: number;
average: number;
} {
const transaction = this.itemsTransactions.get(itemId);
const transaction = this.itemsTransactions.get(itemId.toString());
const quantity = get(transaction, 'quantity', 0);
const cost = get(transaction, 'cost', 0);
@@ -105,14 +105,17 @@ export class PurchasesByItems extends FinancialSheet{
id: item.id,
name: item.name,
code: item.code,
quantityPurchased: meta.quantity,
purchaseCost: meta.cost,
averageCostPrice: meta.average,
quantityPurchasedFormatted: this.formatNumber(meta.quantity, {
money: false,
}),
purchaseCostFormatted: this.formatNumber(meta.cost),
averageCostPriceFormatted: this.formatNumber(meta.average),
currencyCode: this.baseCurrency,
};
};

View File

@@ -6,11 +6,17 @@ import {
IPurchasesByItemsTable,
} from './types/PurchasesByItems.types';
import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable';
import { PurchasesByItemsService } from './PurchasesByItemsService';
import { PurchasesByItemsService } from './PurchasesByItems.service';
import { PurchasesByItemsPdf } from './PurchasesByItemsPdf';
@Injectable()
export class PurchasesByItemsApplication {
/**
* @param {PurchasesByItemsService} purchasesByItemsSheetService - Purchases by items sheet service.
* @param {PurchasesByItemsTableInjectable} purchasesByItemsTableService - Purchases by items table service.
* @param {PurchasesByItemsExport} purchasesByItemsExportService - Purchases by items export service.
* @param {PurchasesByItemsPdf} purchasesByItemsPdfService - Purchases by items pdf service.
*/
constructor(
private readonly purchasesByItemsSheetService: PurchasesByItemsService,
private readonly purchasesByItemsTableService: PurchasesByItemsTableInjectable,
@@ -21,7 +27,7 @@ export class PurchasesByItemsApplication {
/**
* Retrieves the purchases by items in json format.
* @param {IPurchasesByItemsReportQuery} query
* @returns
* @returns {Promise<IPurchasesByItemsSheet>}
*/
public sheet(
query: IPurchasesByItemsReportQuery,

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { TableSheetPdf } from '../../TableSheetPdf';
import { TableSheetPdf } from '../../common/TableSheetPdf';
import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable';
import { IPurchasesByItemsReportQuery } from './types/PurchasesByItems.types';
import { HtmlTableCustomCss } from './_types';

View File

@@ -8,7 +8,7 @@ import {
import { ITableColumn, ITableColumnAccessor, ITableRow } from '../../types/Table.types';
import { FinancialTable } from '../../common/FinancialTable';
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
import FinancialSheet from '../../common/FinancialSheet';
import { FinancialSheet } from '../../common/FinancialSheet';
import { tableRowMapper } from '../../utils/Table.utils';
export class PurchasesByItemsTable extends R.compose(

Some files were not shown because too many files have changed in this diff Show More