refactor: nestjs

This commit is contained in:
Ahmed Bouhuolia
2025-02-04 13:17:25 +02:00
parent c4692d1716
commit 2017539032
76 changed files with 4294 additions and 2734 deletions

View File

@@ -17,7 +17,7 @@ export class ExpenseGLEntriesService {
/**
* Retrieves the expense G/L of the given id.
* @param {number} expenseId
* @param {Knex.Transaction} trx
* @param {Knex.Transaction} trx - Knex transaction.
* @returns {Promise<ILedger>}
*/
public getExpenseLedgerById = async (

View File

@@ -13,6 +13,8 @@ import { InventoryItemDetailsModule } from './modules/InventoryItemDetails/Inven
import { InventoryValuationSheetModule } from './modules/InventoryValuationSheet/InventoryValuationSheet.module';
import { SalesTaxLiabilityModule } from './modules/SalesTaxLiabilitySummary/SalesTaxLiability.module';
import { JournalSheetModule } from './modules/JournalSheet/JournalSheet.module';
import { ProfitLossSheetModule } from './modules/ProfitLossSheet/ProfitLossSheet.module';
import { CashflowStatementModule } from './modules/CashFlowStatement/CashflowStatement.module';
@Module({
providers: [],
@@ -31,6 +33,8 @@ import { JournalSheetModule } from './modules/JournalSheet/JournalSheet.module';
InventoryValuationSheetModule,
SalesTaxLiabilityModule,
JournalSheetModule,
ProfitLossSheetModule,
CashflowStatementModule,
],
})
export class FinancialStatementsModule {}

View File

@@ -9,11 +9,12 @@ import { dateRangeFromToCollection } from '@/utils/date-range-collection';
import { FinancialDateRanges } from './FinancialDateRanges';
import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from './FinancialSheet';
import { IFinancialSheetTotalPeriod } from '../modules/BalanceSheet/BalanceSheet.types';
export const FinancialDatePeriods = <T extends GConstructor<FinancialSheet>>(
Base: T,
) =>
class extends R.compose(FinancialDateRanges)(Base) {
class extends R.pipe(FinancialDateRanges)(Base) {
/**
* Retrieves the date ranges from the given from date to the given to date.
* @param {Date} fromDate -

View File

@@ -2,6 +2,7 @@ import * as moment from 'moment';
import { IDateRange, IFinancialDatePeriodsUnit } from '../types/Report.types';
import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from './FinancialSheet';
import { DateInput } from '@/common/types/Date';
export const FinancialDateRanges = <T extends GConstructor<FinancialSheet>>(
Base: T,
@@ -15,7 +16,7 @@ export const FinancialDateRanges = <T extends GConstructor<FinancialSheet>>(
* @returns {Date}
*/
public getPreviousPeriodDate = (
date: Date,
date: DateInput,
value: number = 1,
unit: IFinancialDatePeriodsUnit = IFinancialDatePeriodsUnit.Day,
): Date => {
@@ -28,21 +29,21 @@ export const FinancialDateRanges = <T extends GConstructor<FinancialSheet>>(
* @param {Date} toDate
* @returns {number}
*/
public getPreviousPeriodDiff = (fromDate: Date, toDate: Date) => {
public getPreviousPeriodDiff = (fromDate: DateInput, toDate: DateInput) => {
return moment(toDate).diff(fromDate, 'days') + 1;
};
/**
* Retrieves the periods period dates.
* @param {Date} fromDate - From date.
* @param {Date} toDate - To date.
* @param {DateInput} fromDate - From date.
* @param {DateInput} toDate - To date.
* @param {IFinancialDatePeriodsUnit} unit - Unit of time.
* @param {number} amount - Amount of time.
* @returns {IDateRange}
*/
public getPreviousPeriodDateRange = (
fromDate: Date,
toDate: Date,
fromDate: DateInput,
toDate: DateInput,
unit: IFinancialDatePeriodsUnit,
amount: number = 1,
): IDateRange => {
@@ -54,11 +55,14 @@ export const FinancialDateRanges = <T extends GConstructor<FinancialSheet>>(
/**
* Retrieves the previous period (PP) date range of total column.
* @param {Date} fromDate
* @param {Date} toDate
* @param {DateInput} fromDate - From date.
* @param {DateInput} toDate - To date.
* @returns {IDateRange}
*/
public getPPTotalDateRange = (fromDate: Date, toDate: Date): IDateRange => {
public getPPTotalDateRange = (
fromDate: DateInput,
toDate: DateInput,
): IDateRange => {
const unit = this.getPreviousPeriodDiff(fromDate, toDate);
return this.getPreviousPeriodDateRange(
@@ -77,8 +81,8 @@ export const FinancialDateRanges = <T extends GConstructor<FinancialSheet>>(
* @returns {IDateRange}
*/
public getPPDatePeriodDateRange = (
fromDate: Date,
toDate: Date,
fromDate: DateInput,
toDate: DateInput,
unit: IFinancialDatePeriodsUnit,
): IDateRange => {
return this.getPreviousPeriodDateRange(fromDate, toDate, unit, 1);
@@ -89,22 +93,22 @@ export const FinancialDateRanges = <T extends GConstructor<FinancialSheet>>(
// ------------------------
/**
* Retrieve the previous year of the given date.
* @params {Date} date
* @param {DateInput} date
* @returns {Date}
*/
getPreviousYearDate = (date: Date) => {
getPreviousYearDate = (date: DateInput) => {
return moment(date).subtract(1, 'years').toDate();
};
/**
* Retrieves previous year date range.
* @param {Date} fromDate
* @param {Date} toDate
* @param {DateInput} fromDate - From date.
* @param {DateInput} toDate - To date.
* @returns {IDateRange}
*/
public getPreviousYearDateRange = (
fromDate: Date,
toDate: Date,
fromDate: DateInput,
toDate: DateInput,
): IDateRange => {
const PYFromDate = this.getPreviousYearDate(fromDate);
const PYToDate = this.getPreviousYearDate(toDate);

View File

@@ -10,7 +10,7 @@ export const FinancialEvaluateEquation = <
>(
Base: T
) =>
class FinancialEvaluateEquation extends R.compose(FinancialSheetStructure)(Base) {
class FinancialEvaluateEquation extends R.pipe(FinancialSheetStructure)(Base) {
/**
* Evauluate equaation string with the given scope table.
* @param {string} equation -

View File

@@ -1,12 +1,15 @@
import { Constructor } from '@/common/types/Constructor';
import { GConstructor } from '@/common/types/Constructor';
import { isEmpty } from 'lodash';
import { FinancialSheet } from './FinancialSheet';
import { IFinancialCommonNode } from '../types/Report.types';
export const FinancialFilter = <T extends Constructor>(Base: T) =>
export const FinancialFilter = <T extends GConstructor<FinancialSheet>>(
Base: T,
) =>
class extends Base {
/**
* Detarmines whether the given node has children.
* @param {IBalanceSheetCommonNode} node
* @param {IBalanceSheetCommonNode} node
* @returns {boolean}
*/
public isNodeHasChildren = (node: IFinancialCommonNode): boolean =>
@@ -17,7 +20,7 @@ export const FinancialFilter = <T extends Constructor>(Base: T) =>
* @param {IBalanceSheetCommonNode} node
* @returns {boolean}
*/
public isNodeNoneZero = (node) =>{
public isNodeNoneZero = (node) => {
return node.total.amount !== 0;
}
};
};

View File

@@ -8,7 +8,7 @@ export const FinancialHorizTotals = <T extends GConstructor<FinancialSheet>>(
) =>
class FinancialHorizTotals extends Base {
/**
*
* Associate percentage to the given node.
*/
public assocNodePercentage = R.curry(
(assocPath, parentTotal: number, node: any) => {
@@ -25,7 +25,7 @@ export const FinancialHorizTotals = <T extends GConstructor<FinancialSheet>>(
);
/**
*
* Associate horizontal percentage total to the given node.
* @param {} parentNode -
* @param {} horTotalNode -
* @param {number} index -

View File

@@ -4,20 +4,21 @@ import {
IFinancialNodeWithPreviousPeriod,
} from '../types/Report.types';
import * as R from 'ramda';
import { Constructor, GConstructor } from '@/common/types/Constructor';
import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from './FinancialSheet';
import { FinancialDatePeriods } from './FinancialDatePeriods';
import { IProfitLossSheetAccountNode } from '../modules/ProfitLossSheet/ProfitLossSheet.types';
export const FinancialPreviousPeriod = <T extends GConstructor<FinancialSheet>>(
Base: T,
) =>
class extends R.compose(FinancialDatePeriods)(Base) {
class extends R.pipe(FinancialDatePeriods)(Base) {
// ---------------------------
// # Common Node.
// ---------------------------
/**
* Assoc previous period percentage attribute to account node.
* @param {IProfitLossSheetAccountNode} accountNode
* @param {IProfitLossSheetAccountNode} accountNode
* @returns {IFinancialNodeWithPreviousPeriod}
*/
public assocPreviousPeriodPercentageNode = (

View File

@@ -4,9 +4,9 @@ import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from './FinancialSheet';
export const FinancialSchema = <T extends GConstructor<FinancialSheet>>(
Base: T
Base: T,
) =>
class FinancialSchema extends R.compose(FinancialSheetStructure)(Base) {
class FinancialSchema extends FinancialSheetStructure(Base) {
/**
*
* @returns

View File

@@ -12,10 +12,8 @@ import {
import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from './FinancialSheet';
export const FinancialSheetStructure = <
T extends GConstructor<FinancialSheet>,
>(
Base: T
export const FinancialSheetStructure = <T extends GConstructor<FinancialSheet>>(
Base: T,
) =>
class FinancialSheetStructure extends Base {
/**
@@ -30,7 +28,6 @@ export const FinancialSheetStructure = <
pathFormat: 'array',
});
};
/**
*
* @param nodes
@@ -43,18 +40,15 @@ export const FinancialSheetStructure = <
pathFormat: 'array',
});
};
public mapNodes = (nodes, callback) => {
return mapValues(nodes, callback, {
childrenPath: 'children',
pathFormat: 'array',
});
};
public filterNodesDeep2 = R.curry((predicate, nodes) => {
return filterNodesDeep(predicate, nodes);
});
/**
*
* @param
@@ -65,14 +59,12 @@ export const FinancialSheetStructure = <
pathFormat: 'array',
});
};
public findNodeDeep = (nodes, callback) => {
return findValueDeep(nodes, callback, {
childrenPath: 'children',
pathFormat: 'array',
});
};
public mapAccNodesDeep = (nodes, callback) => {
return reduceDeep(
nodes,
@@ -80,7 +72,7 @@ export const FinancialSheetStructure = <
set(
acc,
context.path,
callback(value, key, parentValue, acc, context)
callback(value, key, parentValue, acc, context),
);
return acc;
},
@@ -88,10 +80,9 @@ export const FinancialSheetStructure = <
{
childrenPath: 'children',
pathFormat: 'array',
}
},
);
};
/**
*
*/
@@ -101,11 +92,9 @@ export const FinancialSheetStructure = <
pathFormat: 'array',
});
};
public getTotalOfChildrenNodes = (node) => {
return this.getTotalOfNodes(node.children);
};
public getTotalOfNodes = (nodes) => {
return sumBy(nodes, 'total.amount');
};

View File

@@ -27,8 +27,8 @@ export const BalanceSheetAggregators = <T extends GConstructor<FinancialSheet>>(
BalanceSheetComparsionPreviousYear,
BalanceSheetPercentage,
BalanceSheetSchema,
BalanceSheetBase,
FinancialSheetStructure,
BalanceSheetBase,
)(Base) {
/**
* Balance sheet query.

View File

@@ -11,7 +11,7 @@ export const BalanceSheetBase = <T extends GConstructor<FinancialSheet>>(
) =>
class BalanceSheetBase extends Base {
/**
* Detarmines the node type of the given schema node.
* Determines the node type of the given schema node.
* @param {IBalanceSheetStructureSection} node -
* @param {string} type -
* @return {boolean}
@@ -21,9 +21,8 @@ export const BalanceSheetBase = <T extends GConstructor<FinancialSheet>>(
return node.type === type;
},
);
/**
* Detarmines the node type of the given schema node.
* Determines the node type of the given schema node.
* @param {IBalanceSheetStructureSection} node -
* @param {string} type -
* @return {boolean}
@@ -33,9 +32,8 @@ export const BalanceSheetBase = <T extends GConstructor<FinancialSheet>>(
return node.nodeType === type;
},
);
/**
* Detarmines the given display columns by type.
* Determines the given display columns by type.
* @param {string} displayColumnsBy
* @returns {boolean}
*/

View File

@@ -62,8 +62,8 @@ export const BalanceSheetNetIncome = <T extends GConstructor<FinancialSheet>>(
};
/**
* Mappes the aggregate schema node type.
* @param {IBalanceSheetSchemaNetIncomeNode} node - Schema node.
* Maps the aggregate schema node type.
* @param {IBalanceSheetSchemaNetIncomeNode} node - Schema node.
* @return {IBalanceSheetAggregateNode}
*/
public schemaNetIncomeNodeMapper = (

View File

@@ -1,5 +1,4 @@
/* eslint-disable import/prefer-default-export */
import * as R from 'ramda';
import {
BALANCE_SHEET_SCHEMA_NODE_ID,
BALANCE_SHEET_SCHEMA_NODE_TYPE,
@@ -12,7 +11,7 @@ import { FinancialSheet } from '../../common/FinancialSheet';
export const BalanceSheetSchema = <T extends GConstructor<FinancialSheet>>(
Base: T,
) =>
class extends R.pipe(FinancialSchema)(Base) {
class extends FinancialSchema(Base) {
/**
* Retrieves the balance sheet schema.
* @returns

View File

@@ -1,10 +1,15 @@
import * as R from 'ramda';
import { ITableColumn } from '../../types/Table.types';
import { Constructor } from '@/common/types/Constructor';
import { BalanceSheetQuery } from './BalanceSheetQuery';
import { I18nService } from 'nestjs-i18n';
import { ITableColumn } from '../../types/Table.types';
import { GConstructor } from '@/common/types/Constructor';
import { BalanceSheetQuery } from './BalanceSheetQuery';
import { FinancialSheet } from '../../common/FinancialSheet';
export const BalanceSheetTablePercentage = <T extends Constructor>(Base: T) =>
export const BalanceSheetTablePercentage = <
T extends GConstructor<FinancialSheet>,
>(
Base: T,
) =>
class BalanceSheetComparsionPreviousYear extends Base {
public readonly query: BalanceSheetQuery;
public readonly i18n: I18nService;
@@ -23,15 +28,15 @@ export const BalanceSheetTablePercentage = <T extends Constructor>(Base: T) =>
R.append({
key: 'percentage_of_column',
label: this.i18n.t('balance_sheet.percentage_of_column'),
})
}),
),
R.when(
this.query.isRowsPercentageActive,
R.append({
key: 'percentage_of_row',
label: this.i18n.t('balance_sheet.percentage_of_row'),
})
)
}),
),
)([]);
};
@@ -49,15 +54,15 @@ export const BalanceSheetTablePercentage = <T extends Constructor>(Base: T) =>
R.append({
key: 'percentage_of_column',
accessor: 'percentageColumn.formattedAmount',
})
}),
),
R.when(
this.query.isRowsPercentageActive,
R.append({
key: 'percentage_of_row',
accessor: 'percentageRow.formattedAmount',
})
)
}),
),
)([]);
};
@@ -67,7 +72,7 @@ export const BalanceSheetTablePercentage = <T extends Constructor>(Base: T) =>
* @returns {ITableColumn[]}
*/
public percetangeDatePeriodColumnsAccessor = (
index: number
index: number,
): ITableColumn[] => {
return R.pipe(
R.when(
@@ -75,15 +80,15 @@ export const BalanceSheetTablePercentage = <T extends Constructor>(Base: T) =>
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

@@ -3,9 +3,14 @@ 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';
import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from '../../common/FinancialSheet';
export const BalanceSheetTablePreviousYear = <T extends Constructor>(Base: T) =>
export const BalanceSheetTablePreviousYear = <
T extends GConstructor<FinancialSheet>,
>(
Base: T,
) =>
class extends R.pipe(FinancialTablePreviousYear, FinancialDateRanges)(Base) {
// --------------------
// # Columns.

View File

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

View File

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

View File

@@ -0,0 +1,167 @@
import { Inject, Injectable } from '@nestjs/common';
import * as moment from 'moment';
import { Knex } from 'knex';
import { isEmpty } from 'lodash';
import { ModelObject } from 'objection';
import { ICashFlowStatementQuery } from './Cashflow.types';
import { Account } from '@/modules/Accounts/models/Account.model';
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
@Injectable()
export class CashFlowRepository {
/**
* @param {typeof Account} accountModel - Account model.
* @param {typeof AccountTransaction} accountTransactionModel - Account transaction model.
*/
constructor(
@Inject(Account.name)
private readonly accountModel: typeof Account,
@Inject(AccountTransaction.name)
private readonly accountTransactionModel: typeof AccountTransaction,
) {}
/**
* Retrieve the group type from periods type.
* @param {string} displayType
* @returns {string}
*/
protected getGroupTypeFromPeriodsType(displayType: string) {
const displayTypes = {
year: 'year',
day: 'day',
month: 'month',
quarter: 'month',
week: 'day',
};
return displayTypes[displayType] || 'month';
}
/**
* Retrieve the cashflow accounts.
* @returns {Promise<IAccount[]>}
*/
public async cashFlowAccounts(): Promise<Account[]> {
const accounts = await this.accountModel.query();
return accounts;
}
/**
* Retrieve total of csah at beginning transactions.
* @param {ICashFlowStatementQuery} filter -
* @return {Promise<IAccountTransaction[]>}
*/
public async cashAtBeginningTotalTransactions(
filter: ICashFlowStatementQuery,
): Promise<ModelObject<AccountTransaction>[]> {
const cashBeginningPeriod = moment(filter.fromDate)
.subtract(1, 'day')
.toDate();
const transactions = await this.accountTransactionModel
.query()
.onBuild((query) => {
query.modify('creditDebitSummation');
query.select('accountId');
query.groupBy('accountId');
query.withGraphFetched('account');
query.modify('filterDateRange', null, cashBeginningPeriod);
this.commonFilterBranchesQuery(filter, query);
});
return transactions;
}
/**
* Retrieve accounts transactions.
* @param {ICashFlowStatementQuery} filter
* @return {Promise<IAccountTransaction>}
*/
public async getAccountsTransactions(
filter: ICashFlowStatementQuery,
): Promise<ModelObject<AccountTransaction>[]> {
const groupByDateType = this.getGroupTypeFromPeriodsType(
filter.displayColumnsBy,
);
return await this.accountTransactionModel.query().onBuild((query) => {
query.modify('creditDebitSummation');
query.modify('groupByDateFormat', groupByDateType);
query.select('accountId');
query.groupBy('accountId');
query.withGraphFetched('account');
query.modify('filterDateRange', filter.fromDate, filter.toDate);
this.commonFilterBranchesQuery(filter, query);
});
}
/**
* Retrieve the net income tranasctions.
* @param {number} tenantId -
* @param {ICashFlowStatementQuery} query -
* @return {Promise<IAccountTransaction[]>}
*/
public async getNetIncomeTransactions(
filter: ICashFlowStatementQuery,
): Promise<AccountTransaction[]> {
const groupByDateType = this.getGroupTypeFromPeriodsType(
filter.displayColumnsBy,
);
return await this.accountTransactionModel.query().onBuild((query) => {
query.modify('creditDebitSummation');
query.modify('groupByDateFormat', groupByDateType);
query.select('accountId');
query.groupBy('accountId');
query.withGraphFetched('account');
query.modify('filterDateRange', filter.fromDate, filter.toDate);
this.commonFilterBranchesQuery(filter, query);
});
}
/**
* Retrieve peridos of cash at beginning transactions.
* @param {ICashFlowStatementQuery} filter -
* @return {Promise<ModelObject<AccountTransaction>[]>}
*/
public async cashAtBeginningPeriodTransactions(
filter: ICashFlowStatementQuery,
): Promise<ModelObject<AccountTransaction>[]> {
const groupByDateType = this.getGroupTypeFromPeriodsType(
filter.displayColumnsBy,
);
return await this.accountTransactionModel.query().onBuild((query) => {
query.modify('creditDebitSummation');
query.modify('groupByDateFormat', groupByDateType);
query.select('accountId');
query.groupBy('accountId');
query.withGraphFetched('account');
query.modify('filterDateRange', filter.fromDate, filter.toDate);
this.commonFilterBranchesQuery(filter, query);
});
}
/**
* Common branches filter query.
* @param {Knex.QueryBuilder} query
*/
private commonFilterBranchesQuery = (
query: ICashFlowStatementQuery,
knexQuery: Knex.QueryBuilder,
) => {
if (!isEmpty(query.branchesIds)) {
knexQuery.modify('filterByBranches', query.branchesIds);
}
};
}

View File

@@ -0,0 +1,107 @@
import { ModelObject } from 'objection';
import * as R from 'ramda';
import {
ICashFlowStatementQuery,
ICashFlowStatementDOO,
} from './Cashflow.types';
import { CashFlowStatement } from './CashFlow';
import { CashflowSheetMeta } from './CashflowSheetMeta';
import { Injectable } from '@nestjs/common';
import { CashFlowRepository } from './CashFlowRepository';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { Ledger } from '@/modules/Ledger/Ledger';
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
import { I18nService } from 'nestjs-i18n';
import { getDefaultCashflowQuery } from './constants';
@Injectable()
export class CashFlowStatementService {
/**
* @param {CashFlowRepository} cashFlowRepo - Cash flow repository.
* @param {CashflowSheetMeta} cashflowSheetMeta - Cashflow sheet meta.
* @param {TenancyContext} tenancyContext - Tenancy context.
*/
constructor(
private readonly cashFlowRepo: CashFlowRepository,
private readonly cashflowSheetMeta: CashflowSheetMeta,
private readonly tenancyContext: TenancyContext,
private readonly i18n: I18nService,
) {}
/**
* Retrieves cash at beginning transactions.
* @param {ICashFlowStatementQuery} filter - Cash flow statement query.
* @returns {Promise<ModelObject<AccountTransaction>[]>}
*/
private async cashAtBeginningTransactions(
filter: ICashFlowStatementQuery,
): Promise<ModelObject<AccountTransaction>[]> {
const appendPeriodsOperToChain = (trans) =>
R.append(
this.cashFlowRepo.cashAtBeginningPeriodTransactions(filter),
trans,
);
const promisesChain = R.pipe(
R.append(this.cashFlowRepo.cashAtBeginningTotalTransactions(filter)),
R.when(
R.always(R.equals(filter.displayColumnsType, 'date_periods')),
appendPeriodsOperToChain,
),
)([]);
const promisesResults = await Promise.all(promisesChain);
const transactions = R.flatten(promisesResults);
return transactions;
}
/**
* Retrieve the cash flow sheet statement.
* @param {ICashFlowStatementQuery} query - Cashflow query.
* @returns {Promise<ICashFlowStatementDOO>}
*/
public async cashFlow(
query: ICashFlowStatementQuery,
): Promise<ICashFlowStatementDOO> {
// Retrieve all accounts on the storage.
const accounts = await this.cashFlowRepo.cashFlowAccounts();
const tenant = await this.tenancyContext.getTenant(true);
const filter = {
...getDefaultCashflowQuery(),
...query,
};
// Retrieve the accounts transactions.
const transactions =
await this.cashFlowRepo.getAccountsTransactions(filter);
// Retrieve the net income transactions.
const netIncome = await this.cashFlowRepo.getNetIncomeTransactions(filter);
// Retrieve the cash at beginning transactions.
const cashAtBeginningTransactions =
await this.cashAtBeginningTransactions(filter);
// Transformes the transactions to ledgers.
const ledger = Ledger.fromTransactions(transactions);
const cashLedger = Ledger.fromTransactions(cashAtBeginningTransactions);
const netIncomeLedger = Ledger.fromTransactions(netIncome);
// Cash flow statement.
const cashFlowInstance = new CashFlowStatement(
accounts,
ledger,
cashLedger,
netIncomeLedger,
filter,
tenant.metadata.baseCurrency,
this.i18n,
);
// Retrieve the cashflow sheet meta.
const meta = await this.cashflowSheetMeta.meta(filter);
return {
data: cashFlowInstance.reportData(),
query: filter,
meta,
};
}
}

View File

@@ -0,0 +1,375 @@
import * as R from 'ramda';
import { isEmpty } from 'lodash';
import moment from 'moment';
import { I18nService } from 'nestjs-i18n';
import {
ICashFlowStatementSection,
ICashFlowStatementSectionType,
IDateRange,
ICashFlowStatementDOO,
} from './Cashflow.types';
import { ITableRow, ITableColumn } from '../../types/Table.types';
import { dateRangeFromToCollection } from '@/utils/date-range-collection';
import { tableRowMapper } from '../../utils/Table.utils';
import { mapValuesDeep } from '@/utils/deepdash';
enum IROW_TYPE {
AGGREGATE = 'AGGREGATE',
NET_INCOME = 'NET_INCOME',
ACCOUNTS = 'ACCOUNTS',
ACCOUNT = 'ACCOUNT',
TOTAL = 'TOTAL',
}
const DEEP_CONFIG = { childrenPath: 'children', pathFormat: 'array' };
const DISPLAY_COLUMNS_BY = {
DATE_PERIODS: 'date_periods',
TOTAL: 'total',
};
export class CashFlowTable {
private report: ICashFlowStatementDOO;
private i18n: I18nService;
private dateRangeSet: IDateRange[];
/**
* Constructor method.
* @param {ICashFlowStatement} reportStatement - Statement.
* @param {I18nService} i18n - I18n service.
*/
constructor(reportStatement: ICashFlowStatementDOO, i18n: I18nService) {
this.report = reportStatement;
this.i18n = i18n;
this.dateRangeSet = [];
this.initDateRangeCollection();
}
/**
* Initialize date range set.
*/
private initDateRangeCollection() {
this.dateRangeSet = dateRangeFromToCollection(
this.report.query.fromDate,
this.report.query.toDate,
this.report.query.displayColumnsBy,
);
}
/**
* Retrieve the date periods columns accessors.
*/
private datePeriodsColumnsAccessors = () => {
return this.dateRangeSet.map((dateRange: IDateRange, index) => ({
key: `date-range-${index}`,
accessor: `periods[${index}].total.formattedAmount`,
}));
};
/**
* Retrieve the total column accessor.
*/
private totalColumnAccessor = () => {
return [{ key: 'total', accessor: 'total.formattedAmount' }];
};
/**
* Retrieve the common columns for all report nodes.
*/
private commonColumns = () => {
return R.compose(
R.concat([{ key: 'name', accessor: 'label' }]),
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
R.concat(this.datePeriodsColumnsAccessors()),
),
R.concat(this.totalColumnAccessor()),
)([]);
};
/**
* Retrieve the table rows of regular section.
* @param {ICashFlowStatementSection} section
* @returns {ITableRow[]}
*/
private regularSectionMapper = (
section: ICashFlowStatementSection,
): ITableRow => {
const columns = this.commonColumns();
return tableRowMapper(section, columns, {
rowTypes: [IROW_TYPE.AGGREGATE],
id: section.id,
});
};
/**
* Retrieve the net income table rows of the section.
* @param {ICashFlowStatementSection} section
* @returns {ITableRow}
*/
private netIncomeSectionMapper = (
section: ICashFlowStatementSection,
): ITableRow => {
const columns = this.commonColumns();
return tableRowMapper(section, columns, {
rowTypes: [IROW_TYPE.NET_INCOME, IROW_TYPE.TOTAL],
id: section.id,
});
};
/**
* Retrieve the accounts table rows of the section.
* @param {ICashFlowStatementSection} section
* @returns {ITableRow}
*/
private accountsSectionMapper = (
section: ICashFlowStatementSection,
): ITableRow => {
const columns = this.commonColumns();
return tableRowMapper(section, columns, {
rowTypes: [IROW_TYPE.ACCOUNTS],
id: section.id,
});
};
/**
* Retrieve the account table row of account section.
* @param {ICashFlowStatementSection} section
* @returns {ITableRow}
*/
private accountSectionMapper = (
section: ICashFlowStatementSection,
): ITableRow => {
const columns = this.commonColumns();
return tableRowMapper(section, columns, {
rowTypes: [IROW_TYPE.ACCOUNT],
id: `account-${section.id}`,
});
};
/**
* Retrieve the total table rows from the given total section.
* @param {ICashFlowStatementSection} section
* @returns {ITableRow}
*/
private totalSectionMapper = (
section: ICashFlowStatementSection,
): ITableRow => {
const columns = this.commonColumns();
return tableRowMapper(section, columns, {
rowTypes: [IROW_TYPE.TOTAL],
id: section.id,
});
};
/**
* Detarmines the schema section type.
* @param {string} type
* @param {ICashFlowSchemaSection} section
* @returns {boolean}
*/
private isSectionHasType = (
type: string,
section: ICashFlowStatementSection,
): boolean => {
return type === section.sectionType;
};
/**
* The report section mapper.
* @param {ICashFlowStatementSection} section
* @returns {ITableRow}
*/
private sectionMapper = (
section: ICashFlowStatementSection,
key: string,
parentSection: ICashFlowStatementSection,
): ITableRow => {
const isSectionHasType = R.curry(this.isSectionHasType);
return R.pipe(
R.when(
isSectionHasType(ICashFlowStatementSectionType.AGGREGATE),
this.regularSectionMapper,
),
R.when(
isSectionHasType(ICashFlowStatementSectionType.CASH_AT_BEGINNING),
this.regularSectionMapper,
),
R.when(
isSectionHasType(ICashFlowStatementSectionType.NET_INCOME),
this.netIncomeSectionMapper,
),
R.when(
isSectionHasType(ICashFlowStatementSectionType.ACCOUNTS),
this.accountsSectionMapper,
),
R.when(
isSectionHasType(ICashFlowStatementSectionType.ACCOUNT),
this.accountSectionMapper,
),
R.when(
isSectionHasType(ICashFlowStatementSectionType.TOTAL),
this.totalSectionMapper,
),
)(section);
};
/**
* Mappes the sections to the table rows.
* @param {ICashFlowStatementSection[]} sections
* @returns {ITableRow[]}
*/
private mapSectionsToTableRows = (
sections: ICashFlowStatementSection[],
): ITableRow[] => {
return mapValuesDeep(sections, this.sectionMapper.bind(this), DEEP_CONFIG);
};
/**
* Appends the total to section's children.
* @param {ICashFlowStatementSection} section
* @returns {ICashFlowStatementSection}
*/
private appendTotalToSectionChildren = (
section: ICashFlowStatementSection,
): ICashFlowStatementSection => {
const label = section.footerLabel
? section.footerLabel
: this.i18n.__('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),
}));
};
/**
* Determines the given column type is the current.
* @reutrns {boolean}
*/
private isDisplayColumnsBy = (displayColumnsType: string): Boolean => {
return this.report.query.displayColumnsType === displayColumnsType;
};
/**
* Determines whether the given display columns type is the current.
* @param {string} displayColumnsBy
* @returns {boolean}
*/
private isDisplayColumnsType = (displayColumnsBy: string): Boolean => {
return this.report.query.displayColumnsBy === displayColumnsBy;
};
/**
* Retrieve the table columns.
* @return {ITableColumn[]}
*/
public tableColumns = (): ITableColumn[] => {
return R.compose(
R.concat([{ key: 'name', label: this.i18n.t('Account name') }]),
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
R.concat(this.datePeriodsColumns()),
),
R.concat(this.totalColumns()),
)([]);
};
}

View File

@@ -0,0 +1,58 @@
import { Response } from 'express';
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
import { ICashFlowStatementQuery } from './Cashflow.types';
import { AcceptType } from '@/constants/accept-type';
import { CashflowSheetApplication } from './CashflowSheetApplication';
import { PublicRoute } from '@/modules/Auth/Jwt.guard';
@Controller('reports/cashflow-statement')
@PublicRoute()
export class CashflowController {
constructor(private readonly cashflowSheetApp: CashflowSheetApplication) {}
@Get()
async getCashflow(
@Query() query: ICashFlowStatementQuery,
@Res() res: Response,
@Headers('accept') acceptHeader: string,
) {
// Retrieves the json table format.
if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) {
const table = await this.cashflowSheetApp.table(query);
return res.status(200).send(table);
// Retrieves the csv format.
} else if (acceptHeader.includes(AcceptType.ApplicationCsv)) {
const buffer = await this.cashflowSheetApp.csv(query);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.status(200).send(buffer);
// Retrieves the pdf format.
} else if (acceptHeader.includes(AcceptType.ApplicationXlsx)) {
const buffer = await this.cashflowSheetApp.xlsx(query);
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
);
return res.send(buffer);
// Retrieves the pdf format.
} else if (acceptHeader.includes(AcceptType.ApplicationPdf)) {
const pdfContent = await this.cashflowSheetApp.pdf(query);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
return res.send(pdfContent);
// Retrieves the json format.
} else {
const cashflow = await this.cashflowSheetApp.sheet(query);
return res.status(200).send(cashflow);
}
}
}

View File

@@ -0,0 +1,299 @@
import { Knex } from 'knex';
import { IFinancialSheetCommonMeta, INumberFormatQuery } from '../../types/Report.types';
import { Account } from '@/modules/Accounts/models/Account.model';
import { Ledger } from '@/modules/Ledger/Ledger';
import { IFinancialTable, ITableRow } from '../../types/Table.types';
export interface ICashFlowStatementQuery {
fromDate: Date | string;
toDate: Date | string;
displayColumnsBy: string;
displayColumnsType: string;
noneZero: boolean;
noneTransactions: boolean;
numberFormat: INumberFormatQuery;
basis: string;
branchesIds?: number[];
}
export interface ICashFlowStatementTotal {
amount: number;
formattedAmount: string;
currencyCode: string;
}
export interface ICashFlowStatementTotalPeriod {
fromDate: Date;
toDate: Date;
total: ICashFlowStatementTotal;
}
export interface ICashFlowStatementCommonSection {
id: string;
label: string;
total: ICashFlowStatementTotal;
footerLabel?: string;
}
export interface ICashFlowStatementAccountMeta {
id: number;
label: string;
code: string;
total: ICashFlowStatementTotal;
accountType: string;
adjustmentType: string;
sectionType: ICashFlowStatementSectionType.ACCOUNT;
}
export enum ICashFlowStatementSectionType {
REGULAR = 'REGULAR',
AGGREGATE = 'AGGREGATE',
NET_INCOME = 'NET_INCOME',
ACCOUNT = 'ACCOUNT',
ACCOUNTS = 'ACCOUNTS',
TOTAL = 'TOTAL',
CASH_AT_BEGINNING = 'CASH_AT_BEGINNING',
}
export interface ICashFlowStatementAccountSection
extends ICashFlowStatementCommonSection {
sectionType: ICashFlowStatementSectionType.ACCOUNTS;
children: ICashFlowStatementAccountMeta[];
total: ICashFlowStatementTotal;
}
export interface ICashFlowStatementNetIncomeSection
extends ICashFlowStatementCommonSection {
sectionType: ICashFlowStatementSectionType.NET_INCOME;
}
export interface ICashFlowStatementTotalSection
extends ICashFlowStatementCommonSection {
sectionType: ICashFlowStatementSectionType.TOTAL;
}
export interface ICashFlowStatementAggregateSection
extends ICashFlowStatementCommonSection {
sectionType: ICashFlowStatementSectionType.AGGREGATE;
}
export interface ICashFlowCashBeginningNode
extends ICashFlowStatementCommonSection {
sectionType: ICashFlowStatementSectionType.CASH_AT_BEGINNING;
}
export type ICashFlowStatementSection =
| ICashFlowStatementAccountSection
| ICashFlowStatementNetIncomeSection
| ICashFlowStatementTotalSection
| ICashFlowStatementCommonSection;
export interface ICashFlowStatementColumn {}
export interface ICashFlowStatementMeta extends IFinancialSheetCommonMeta {
formattedToDate: string;
formattedFromDate: string;
formattedDateRange: string;
}
export interface ICashFlowStatementDOO {
data: ICashFlowStatementData;
meta: ICashFlowStatementMeta;
query: ICashFlowStatementQuery;
}
export interface ICashFlowStatementTable extends IFinancialTable {
meta: ICashFlowStatementMeta;
query: ICashFlowStatementQuery;
}
export interface ICashFlowStatementService {
cashFlow(
tenantId: number,
query: ICashFlowStatementQuery
): Promise<ICashFlowStatementDOO>;
}
// CASH FLOW SCHEMA TYPES.
// -----------------------------
export interface ICashFlowSchemaCommonSection {
id: string;
label: string;
children: ICashFlowSchemaSection[];
footerLabel?: string;
}
export enum CASH_FLOW_ACCOUNT_RELATION {
MINES = 'mines',
PLUS = 'plus',
}
export enum CASH_FLOW_SECTION_ID {
NET_INCOME = 'NET_INCOME',
OPERATING = 'OPERATING',
OPERATING_ACCOUNTS = 'OPERATING_ACCOUNTS',
INVESTMENT = 'INVESTMENT',
FINANCIAL = 'FINANCIAL',
NET_OPERATING = 'NET_OPERATING',
NET_INVESTMENT = 'NET_INVESTMENT',
NET_FINANCIAL = 'NET_FINANCIAL',
CASH_BEGINNING_PERIOD = 'CASH_BEGINNING_PERIOD',
CASH_END_PERIOD = 'CASH_END_PERIOD',
NET_CASH_INCREASE = 'NET_CASH_INCREASE',
}
export interface ICashFlowSchemaAccountsSection
extends ICashFlowSchemaCommonSection {
sectionType: ICashFlowStatementSectionType.ACCOUNT;
accountsRelations: ICashFlowSchemaAccountRelation[];
}
export interface ICashFlowSchemaTotalSection
extends ICashFlowStatementCommonSection {
sectionType: ICashFlowStatementSectionType.TOTAL;
equation: string;
}
export type ICashFlowSchemaSection =
| ICashFlowSchemaAccountsSection
| ICashFlowSchemaTotalSection
| ICashFlowSchemaCommonSection;
export type ICashFlowStatementData = ICashFlowSchemaSection[];
export interface ICashFlowSchemaAccountRelation {
type: string;
direction: CASH_FLOW_ACCOUNT_RELATION.PLUS;
}
export interface ICashFlowSchemaSectionAccounts
extends ICashFlowStatementCommonSection {
type: ICashFlowStatementSectionType.ACCOUNT;
accountsRelations: ICashFlowSchemaAccountRelation[];
}
export interface ICashFlowSchemaSectionTotal {
type: ICashFlowStatementSectionType.TOTAL;
totalEquation: string;
}
export interface ICashFlowDatePeriod {
fromDate: ICashFlowDate;
toDate: ICashFlowDate;
total: ICashFlowStatementTotal;
}
export interface ICashFlowDate {
formattedDate: string;
date: Date;
}
export interface ICashFlowStatement {
/**
* Constructor method.
* @constructor
*/
constructor(
accounts: Account[],
ledger: Ledger,
cashLedger: Ledger,
netIncomeLedger: Ledger,
query: ICashFlowStatementQuery,
baseCurrency: string
): void;
reportData(): ICashFlowStatementData;
}
export interface ICashFlowTable {
constructor(reportStatement: ICashFlowStatement): void;
tableRows(): ITableRow[];
}
export interface IDateRange {
fromDate: Date;
toDate: Date;
}
export interface ICashflowTransactionSchema {
amount: number;
date: Date;
referenceNo?: string | null;
transactionNumber: string;
transactionType: string;
creditAccountId: number;
cashflowAccountId: number;
userId: number;
publishedAt?: Date | null;
branchId?: number;
}
export interface ICashflowTransactionInput extends ICashflowTransactionSchema {}
export interface ICategorizeCashflowTransactioDTO {
date: Date;
creditAccountId: number;
referenceNo: string;
transactionNumber: string;
transactionType: string;
exchangeRate: number;
description: string;
branchId: number;
}
export interface IUncategorizedCashflowTransaction {
id?: number;
amount: number;
date: Date;
currencyCode: string;
accountId: number;
description: string;
referenceNo: string;
categorizeRefType: string;
categorizeRefId: number;
categorized: boolean;
}
export interface CreateUncategorizedTransactionDTO {
date: Date | string;
accountId: number;
amount: number;
currencyCode: string;
payee?: string;
description?: string;
referenceNo?: string | null;
plaidTransactionId?: string | null;
pending?: boolean;
pendingPlaidTransactionId?: string | null;
batch?: string;
}
export interface IUncategorizedTransactionCreatingEventPayload {
tenantId: number;
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO;
trx: Knex.Transaction;
}
export interface IUncategorizedTransactionCreatedEventPayload {
tenantId: number;
uncategorizedTransaction: any;
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO;
trx: Knex.Transaction;
}
export interface IPendingTransactionRemovingEventPayload {
tenantId: number;
uncategorizedTransactionId: number;
pendingTransaction: IUncategorizedCashflowTransaction;
trx?: Knex.Transaction;
}
export interface IPendingTransactionRemovedEventPayload {
tenantId: number;
uncategorizedTransactionId: number;
pendingTransaction: IUncategorizedCashflowTransaction;
trx?: Knex.Transaction;
}

View File

@@ -0,0 +1,37 @@
import { Injectable } from '@nestjs/common';
import { CashflowTableInjectable } from './CashflowTableInjectable';
import { ICashFlowStatementQuery } from './Cashflow.types';
import { TableSheet } from '../../common/TableSheet';
@Injectable()
export class CashflowExportInjectable {
constructor(private readonly cashflowSheetTable: CashflowTableInjectable) {}
/**
* Retrieves the cashflow sheet in XLSX format.
* @param {ICashFlowStatementQuery} query - Cashflow statement query.
* @returns {Promise<Buffer>}
*/
public async xlsx(query: ICashFlowStatementQuery): Promise<Buffer> {
const table = await this.cashflowSheetTable.table(query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToXLSX();
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
}
/**
* Retrieves the cashflow sheet in CSV format.
* @param {ICashFlowStatementQuery} query - Cashflow statement query.
* @returns {Promise<Buffer>}
*/
public async csv(query: ICashFlowStatementQuery): Promise<string> {
const table = await this.cashflowSheetTable.table(query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
}

View File

@@ -0,0 +1,66 @@
import { CashflowExportInjectable } from './CashflowExportInjectable';
import { ICashFlowStatementQuery } from './Cashflow.types';
import { CashFlowStatementService } from './CashFlowService';
import { CashflowTableInjectable } from './CashflowTableInjectable';
import { CashflowTablePdfInjectable } from './CashflowTablePdfInjectable';
import { Injectable } from '@nestjs/common';
@Injectable()
export class CashflowSheetApplication {
/**
* Constructor method.
* @param {CashflowExportInjectable} cashflowExport
* @param {} cashflowSheet
* @param cashflowTable
* @param cashflowPdf
*/
constructor(
private readonly cashflowExport: CashflowExportInjectable,
private readonly cashflowSheet: CashFlowStatementService,
private readonly cashflowTable: CashflowTableInjectable,
private readonly cashflowPdf: CashflowTablePdfInjectable,
) {}
/**
* Retrieves the cashflow sheet
* @param {ICashFlowStatementQuery} query - Cashflow statement query.
*/
public async sheet(query: ICashFlowStatementQuery) {
return this.cashflowSheet.cashFlow(query);
}
/**
* Retrieves the cashflow sheet in table format.
* @param {ICashFlowStatementQuery} query - Cashflow statement query.
*/
public async table(query: ICashFlowStatementQuery) {
return this.cashflowTable.table(query);
}
/**
* Retrieves the cashflow sheet in XLSX format.
* @param {ICashFlowStatementQuery} query - Cashflow statement query.
* @returns {Promise<Buffer>}
*/
public async xlsx(query: ICashFlowStatementQuery) {
return this.cashflowExport.xlsx(query);
}
/**
* Retrieves the cashflow sheet in CSV format.
* @param {ICashFlowStatementQuery} query - Cashflow statement query.
* @returns {Promise<Buffer>}
*/
public async csv(query: ICashFlowStatementQuery): Promise<string> {
return this.cashflowExport.csv(query);
}
/**
* Retrieves the cashflow sheet in pdf format.
* @param {ICashFlowStatementQuery} query - Cashflow statement query.
* @returns {Promise<Buffer>}
*/
public async pdf(query: ICashFlowStatementQuery): Promise<Buffer> {
return this.cashflowPdf.pdf(query);
}
}

View File

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

View File

@@ -0,0 +1,27 @@
import { Module } from '@nestjs/common';
import { CashflowSheetMeta } from './CashflowSheetMeta';
import { CashFlowRepository } from './CashFlowRepository';
import { CashflowTablePdfInjectable } from './CashflowTablePdfInjectable';
import { CashflowExportInjectable } from './CashflowExportInjectable';
import { CashflowController } from './Cashflow.controller';
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
import { CashflowTableInjectable } from './CashflowTableInjectable';
import { CashFlowStatementService } from './CashFlowService';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { CashflowSheetApplication } from './CashflowSheetApplication';
@Module({
imports: [FinancialSheetCommonModule],
providers: [
CashFlowRepository,
CashflowSheetMeta,
CashFlowStatementService,
CashflowTablePdfInjectable,
CashflowExportInjectable,
CashflowTableInjectable,
CashflowSheetApplication,
TenancyContext,
],
controllers: [CashflowController],
})
export class CashflowStatementModule {}

View File

@@ -0,0 +1,46 @@
import * as R from 'ramda';
import { map } from 'lodash';
import { Account } from "@/modules/Accounts/models/Account.model";
import { ICashFlowStatementQuery } from './Cashflow.types';
import { FinancialSheet } from '../../common/FinancialSheet';
export class CashflowStatementBase extends FinancialSheet {
readonly accountsByRootType: Map<string, Account[]>;
readonly query: ICashFlowStatementQuery;
// --------------------------------------------
// # GENERAL UTILITIES
// --------------------------------------------
/**
* Retrieve the expense accounts ids.
* @return {number[]}
*/
public getAccountsIdsByType = (accountType: string): number[] => {
const expenseAccounts = this.accountsByRootType.get(accountType);
const expenseAccountsIds = map(expenseAccounts, 'id');
return expenseAccountsIds;
};
/**
* Detarmines the given display columns by type.
* @param {string} displayColumnsBy
* @returns {boolean}
*/
public isDisplayColumnsBy = (displayColumnsBy: string): boolean => {
return this.query.displayColumnsType === displayColumnsBy;
};
/**
* Adjustments the given amount.
* @param {string} direction
* @param {number} amount -
* @return {number}
*/
public amountAdjustment = (direction: 'mines' | 'plus', amount): number => {
return R.when(
R.always(R.equals(direction, 'mines')),
R.multiply(-1),
)(amount);
};
}

View File

@@ -0,0 +1,37 @@
import { Injectable } from '@nestjs/common';
import { CashFlowTable } from './CashFlowTable';
import { CashFlowStatementService } from './CashFlowService';
import { I18nService } from 'nestjs-i18n';
import {
ICashFlowStatementQuery,
ICashFlowStatementTable,
} from './Cashflow.types';
@Injectable()
export class CashflowTableInjectable {
constructor(
private readonly cashflowSheet: CashFlowStatementService,
private readonly i18n: I18nService,
) {}
/**
* Retrieves the cash flow table.
* @param {ICashFlowStatementQuery} query -
* @returns {Promise<ICashFlowStatementTable>}
*/
public async table(
query: ICashFlowStatementQuery,
): Promise<ICashFlowStatementTable> {
const cashflowDOO = await this.cashflowSheet.cashFlow(query);
const cashflowTable = new CashFlowTable(cashflowDOO, this.i18n);
return {
table: {
columns: cashflowTable.tableColumns(),
rows: cashflowTable.tableRows(),
},
query: cashflowDOO.query,
meta: cashflowDOO.meta,
};
}
}

View File

@@ -0,0 +1,28 @@
import { TableSheetPdf } from '../../common/TableSheetPdf';
import { ICashFlowStatementQuery } from './Cashflow.types';
import { CashflowTableInjectable } from './CashflowTableInjectable';
import { HtmlTableCustomCss } from './constants';
export class CashflowTablePdfInjectable {
constructor(
private readonly cashflowTable: CashflowTableInjectable,
private readonly 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(query: ICashFlowStatementQuery): Promise<Buffer> {
const table = await this.cashflowTable.table(query);
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCustomCss,
);
}
}

View File

@@ -0,0 +1,53 @@
import * as moment from 'moment';
import { ICashFlowStatementQuery } from "./Cashflow.types";
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;
}
`;
export const getDefaultCashflowQuery = (): ICashFlowStatementQuery => ({
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',
});

View File

@@ -0,0 +1,77 @@
import { ACCOUNT_TYPE } from '@/constants/accounts';
import {
ICashFlowSchemaSection,
CASH_FLOW_SECTION_ID,
ICashFlowStatementSectionType,
} from './Cashflow.types';
export const CASH_FLOW_SCHEMA = [
{
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

@@ -15,7 +15,7 @@ export class JournalSheetApplication {
/**
* Retrieves the journal sheet.
* @param {IJournalReportQuery} query
* @returns {}
* @returns {Promise<IJournalSheet>}
*/
public sheet(query: IJournalReportQuery) {
return this.journalSheet.journalSheet(query);

View File

@@ -0,0 +1,80 @@
import * as R from 'ramda';
import {
ProfitLossAggregateNodeId,
ProfitLossNodeType,
IProfitLossSchemaNode,
} from './ProfitLossSheet.types';
import { ACCOUNT_TYPE } from '@/constants/accounts';
import { GConstructor } from '@/common/types/Constructor';
import { FinancialSchema } from '../../common/FinancialSchema';
import { FinancialSheet } from '../../common/FinancialSheet';
export const ProfitLossShema = <T extends GConstructor<FinancialSheet>>(
Base: T,
) =>
class extends R.pipe(FinancialSchema)(Base) {
/**
* Retrieves the report schema.
* @returns {IProfitLossSchemaNode[]}
*/
getSchema = (): IProfitLossSchemaNode[] => {
return getProfitLossSheetSchema();
};
};
/**
* Retrieves P&L sheet schema.
* @returns {IProfitLossSchemaNode}
*/
export const getProfitLossSheetSchema = (): IProfitLossSchemaNode[] => [
{
id: ProfitLossAggregateNodeId.INCOME,
name: 'profit_loss_sheet.income',
nodeType: ProfitLossNodeType.ACCOUNTS,
accountsTypes: [ACCOUNT_TYPE.INCOME],
alwaysShow: true,
},
{
id: ProfitLossAggregateNodeId.COS,
name: 'profit_loss_sheet.cost_of_sales',
nodeType: ProfitLossNodeType.ACCOUNTS,
accountsTypes: [ACCOUNT_TYPE.COST_OF_GOODS_SOLD],
},
{
id: ProfitLossAggregateNodeId.GROSS_PROFIT,
name: 'profit_loss_sheet.gross_profit',
nodeType: ProfitLossNodeType.EQUATION,
equation: `${ProfitLossAggregateNodeId.INCOME} - ${ProfitLossAggregateNodeId.COS}`,
},
{
id: ProfitLossAggregateNodeId.EXPENSES,
name: 'profit_loss_sheet.expenses',
nodeType: ProfitLossNodeType.ACCOUNTS,
accountsTypes: [ACCOUNT_TYPE.EXPENSE],
alwaysShow: true,
},
{
id: ProfitLossAggregateNodeId.NET_OPERATING_INCOME,
name: 'profit_loss_sheet.net_operating_income',
nodeType: ProfitLossNodeType.EQUATION,
equation: `${ProfitLossAggregateNodeId.GROSS_PROFIT} - ${ProfitLossAggregateNodeId.EXPENSES}`,
},
{
id: ProfitLossAggregateNodeId.OTHER_INCOME,
name: 'profit_loss_sheet.other_income',
nodeType: ProfitLossNodeType.ACCOUNTS,
accountsTypes: [ACCOUNT_TYPE.OTHER_INCOME],
},
{
id: ProfitLossAggregateNodeId.OTHER_EXPENSES,
name: 'profit_loss_sheet.other_expenses',
nodeType: ProfitLossNodeType.ACCOUNTS,
accountsTypes: [ACCOUNT_TYPE.OTHER_EXPENSE],
},
{
id: ProfitLossAggregateNodeId.NET_INCOME,
name: 'profit_loss_sheet.net_income',
nodeType: ProfitLossNodeType.EQUATION,
equation: `${ProfitLossAggregateNodeId.NET_OPERATING_INCOME} + ${ProfitLossAggregateNodeId.OTHER_INCOME} - ${ProfitLossAggregateNodeId.OTHER_EXPENSES}`,
},
];

View File

@@ -0,0 +1,65 @@
import { Response } from 'express';
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
import { IProfitLossSheetQuery } from './ProfitLossSheet.types';
import { ProfitLossSheetApplication } from './ProfitLossSheetApplication';
import { AcceptType } from '@/constants/accept-type';
import { PublicRoute } from '@/modules/Auth/Jwt.guard';
@Controller('/reports/profit-loss-sheet')
@PublicRoute()
export class ProfitLossSheetController {
constructor(
private readonly profitLossSheetApp: ProfitLossSheetApplication,
) {}
/**
* Retrieves the profit/loss sheet.
* @param {IProfitLossSheetQuery} query
* @param {Response} res
* @param {string} acceptHeader
*/
@Get('/')
async profitLossSheet(
@Query() query: IProfitLossSheetQuery,
@Res() res: Response,
@Headers('accept') acceptHeader: string,
) {
// Retrieves the csv format.
if (acceptHeader.includes(AcceptType.ApplicationCsv)) {
const sheet = await this.profitLossSheetApp.csv(query);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(sheet);
// Retrieves the json table format.
} else if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) {
const table = await this.profitLossSheetApp.table(query);
return res.status(200).send(table);
// Retrieves the xlsx format.
} else if (acceptHeader.includes(AcceptType.ApplicationXlsx)) {
const sheet = await this.profitLossSheetApp.xlsx(query);
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
);
return res.send(sheet);
// Retrieves the json format.
} else if (acceptHeader.includes(AcceptType.ApplicationJson)) {
const pdfContent = await this.profitLossSheetApp.pdf(query);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
return res.send(pdfContent);
} else {
const sheet = await this.profitLossSheetApp.sheet(query);
return res.status(200).send(sheet);
}
}
}

View File

@@ -0,0 +1,28 @@
import { Module } from '@nestjs/common';
import { ProfitLossSheetService } from './ProfitLossSheetService';
import { ProfitLossSheetExportInjectable } from './ProfitLossSheetExportInjectable';
import { ProfitLossTablePdfInjectable } from './ProfitLossTablePdfInjectable';
import { ProfitLossSheetTableInjectable } from './ProfitLossSheetTableInjectable';
import { ProfitLossSheetMeta } from './ProfitLossSheetMeta';
import { ProfitLossSheetRepository } from './ProfitLossSheetRepository';
import { AccountsModule } from '@/modules/Accounts/Accounts.module';
import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { ProfitLossSheetController } from './ProfitLossSheet.controller';
import { ProfitLossSheetApplication } from './ProfitLossSheetApplication';
@Module({
imports: [FinancialSheetCommonModule, AccountsModule],
controllers: [ProfitLossSheetController],
providers: [
ProfitLossSheetApplication,
ProfitLossSheetService,
ProfitLossSheetExportInjectable,
ProfitLossTablePdfInjectable,
ProfitLossSheetTableInjectable,
ProfitLossSheetMeta,
ProfitLossSheetRepository,
TenancyContext,
],
})
export class ProfitLossSheetModule {}

View File

@@ -0,0 +1,341 @@
// @ts-nocheck
import * as R from 'ramda';
import { ModelObject } from 'objection';
import { I18nService } from 'nestjs-i18n';
import {
ProfitLossNodeType,
IProfitLossSheetEquationNode,
IProfitLossEquationSchemaNode,
IProfitLossSheetAccountsNode,
IProfitLossAccountsSchemaNode,
IProfitLossSchemaNode,
IProfitLossSheetNode,
IProfitLossSheetAccountNode,
IProfitLossSheetQuery,
} from './ProfitLossSheet.types';
import { ProfitLossShema } from './ProfitLossSchema';
import { ProfitLossSheetPercentage } from './ProfitLossSheetPercentage';
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
import { ProfitLossSheetRepository } from './ProfitLossSheetRepository';
import { ProfitLossSheetBase } from './ProfitLossSheetBase';
import { ProfitLossSheetDatePeriods } from './ProfitLossSheetDatePeriods';
import { ProfitLossSheetPreviousYear } from './ProfitLossSheetPreviousYear';
import { ProfitLossSheetPreviousPeriod } from './ProfitLossSheetPreviousPeriod';
import { ProfitLossSheetFilter } from './ProfitLossSheetFilter';
import { FinancialDateRanges } from '../../common/FinancialDateRanges';
import { FinancialEvaluateEquation } from '../../common/FinancialEvaluateEquation';
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
import { FinancialSheet } from '../../common/FinancialSheet';
import { Account } from '@/modules/Accounts/models/Account.model';
import { flatToNestedArray } from '@/utils/flat-to-nested-array';
export default class ProfitLossSheet extends R.pipe(
ProfitLossSheetPreviousYear,
ProfitLossSheetPreviousPeriod,
ProfitLossSheetPercentage,
ProfitLossSheetDatePeriods,
ProfitLossSheetFilter,
ProfitLossShema,
ProfitLossSheetBase,
FinancialDateRanges,
FinancialEvaluateEquation,
FinancialSheetStructure,
)(FinancialSheet) {
/**
* Profit/Loss sheet query.
* @param {ProfitLossSheetQuery}
*/
readonly query: ProfitLossSheetQuery;
/**
* @param {string}
*/
readonly comparatorDateType: string;
/**
* Organization's base currency.
* @param {string}
*/
readonly baseCurrency: string;
/**
* Profit/Loss repository.
* @param {ProfitLossSheetRepository}
*/
readonly repository: ProfitLossSheetRepository;
/**
* I18n service.
* @param {I18nService}
*/
readonly i18n: I18nService;
/**
* Constructor method.
* @param {IProfitLossSheetQuery} query -
* @param {IAccount[]} accounts -
* @param {IJournalPoster} transactionsJournal -
*/
constructor(
repository: ProfitLossSheetRepository,
query: IProfitLossSheetQuery,
i18n: I18nService,
) {
super();
this.query = new ProfitLossSheetQuery(query);
this.repository = repository;
this.numberFormat = this.query.query.numberFormat;
this.i18n = i18n;
}
/**
* Retrieve the sheet account node from the given account.
* @param {ModelObject<Account>} account
* @returns {IProfitLossSheetAccountNode}
*/
private accountNodeMapper = (
account: ModelObject<Account>,
): IProfitLossSheetAccountNode => {
// Retrieves the children account ids of the given account id.
const childrenAccountIds = this.repository.accountsGraph.dependenciesOf(
account.id,
);
// Concat the children and the given account id.
const accountIds = R.uniq(R.append(account.id, childrenAccountIds));
// Retrieves the closing balance of the account included children accounts.
const total = this.repository.totalAccountsLedger
.whereAccountsIds(accountIds)
.getClosingBalance();
return {
id: account.id,
name: account.name,
nodeType: ProfitLossNodeType.ACCOUNT,
total: this.getAmountMeta(total),
};
};
/**
* Compose account node.
* @param {ModelObject<Account>} node
* @returns {IProfitLossSheetAccountNode}
*/
private accountNodeCompose = (
account: ModelObject<Account>,
): IProfitLossSheetAccountNode => {
return R.compose(
R.when(
this.query.isPreviousPeriodActive,
this.previousPeriodAccountNodeCompose,
),
R.when(
this.query.isPreviousYearActive,
this.previousYearAccountNodeCompose,
),
R.when(
this.query.isDatePeriodsColumnsType,
this.assocAccountNodeDatePeriod,
),
this.accountNodeMapper,
)(account);
};
/**
* Retrieves report accounts nodes by the given accounts types.
* @param {string[]} types
* @returns {IBalanceSheetAccountNode}
*/
private getAccountsNodesByTypes = (
types: string[],
): IProfitLossSheetAccountNode[] => {
const accounts = this.repository.getAccountsByType(types);
const accountsTree = flatToNestedArray(accounts, {
id: 'id',
parentId: 'parentAccountId',
});
return this.mapNodesDeep(accountsTree, this.accountNodeCompose);
};
/**
* Mapps the accounts schema node to report node.
* @param {IProfitLossSchemaNode} node
* @returns {IProfitLossSheetNode}
*/
private accountsSchemaNodeMapper = (
node: IProfitLossAccountsSchemaNode,
): IProfitLossSheetNode => {
// Retrieve accounts node by the given types.
const children = this.getAccountsNodesByTypes(node.accountsTypes);
// Retrieve the total of the given nodes.
const total = this.getTotalOfNodes(children);
return {
id: node.id,
name: this.i18n.t(node.name),
nodeType: ProfitLossNodeType.ACCOUNTS,
total: this.getTotalAmountMeta(total),
children,
};
};
/**
* Accounts schema node composer.
* @param {IProfitLossSchemaNode} node
* @returns {IProfitLossSheetAccountsNode}
*/
private accountsSchemaNodeCompose = (
node: IProfitLossSchemaNode,
): IProfitLossSheetAccountsNode => {
return R.compose(
R.when(
this.query.isPreviousPeriodActive,
this.previousPeriodAggregateNodeCompose,
),
R.when(
this.query.isPreviousYearActive,
this.previousYearAggregateNodeCompose,
),
R.when(
this.query.isDatePeriodsColumnsType,
this.assocAggregateDatePeriod,
),
this.accountsSchemaNodeMapper,
)(node);
};
/**
* Equation schema node parser.
* @param {(IProfitLossSchemaNode | IProfitLossSheetNode)[]} accNodes -
* @param {IProfitLossEquationSchemaNode} node -
* @param {IProfitLossSheetEquationNode}
*/
private equationSchemaNodeParser = R.curry(
(
accNodes: (IProfitLossSchemaNode | IProfitLossSheetNode)[],
node: IProfitLossEquationSchemaNode,
): IProfitLossSheetEquationNode => {
const tableNodes = this.getNodesTableForEvaluating(
'total.amount',
accNodes,
);
// Evaluate the given equation.
const total = this.evaluateEquation(node.equation, tableNodes);
return {
id: node.id,
name: this.i18n.t(node.name),
nodeType: ProfitLossNodeType.EQUATION,
total: this.getTotalAmountMeta(total),
};
},
);
/**
* Equation schema node composer.
* @param {(IProfitLossSchemaNode | IProfitLossSheetNode)[]} accNodes -
* @param {IProfitLossSchemaNode} node -
* @returns {IProfitLossSheetEquationNode}
*/
private equationSchemaNodeCompose = R.curry(
(
accNodes: (IProfitLossSchemaNode | IProfitLossSheetNode)[],
node: IProfitLossEquationSchemaNode,
): IProfitLossSheetEquationNode => {
return R.compose(
R.when(
this.query.isPreviousPeriodActive,
this.previousPeriodEquationNodeCompose(accNodes, node.equation),
),
R.when(
this.query.isPreviousYearActive,
this.previousYearEquationNodeCompose(accNodes, node.equation),
),
R.when(
this.query.isDatePeriodsColumnsType,
this.assocEquationNodeDatePeriod(accNodes, node.equation),
),
this.equationSchemaNodeParser(accNodes),
)(node);
},
);
/**
* Parses accounts schema node to report node.
* @param {IProfitLossSchemaNode} schemaNode
* @returns {IProfitLossSheetNode | IProfitLossSchemaNode}
*/
private accountsSchemaNodeMap = (
schemaNode: IProfitLossSchemaNode,
): IProfitLossSheetNode | IProfitLossSchemaNode => {
return R.compose(
R.when(
this.isNodeType(ProfitLossNodeType.ACCOUNTS),
this.accountsSchemaNodeCompose,
),
)(schemaNode);
};
/**
* Composes schema equation node to report node.
* @param {IProfitLossSheetNode | IProfitLossSchemaNode} node
* @param {number} key
* @param {IProfitLossSheetNode | IProfitLossSchemaNode} parentValue
* @param {(IProfitLossSheetNode | IProfitLossSchemaNode)[]} accNodes
* @param context
* @returns {IProfitLossSheetEquationNode}
*/
private reportSchemaEquationNodeCompose = (
node: IProfitLossSheetNode | IProfitLossSchemaNode,
key: number,
parentValue: IProfitLossSheetNode | IProfitLossSchemaNode,
accNodes: (IProfitLossSheetNode | IProfitLossSchemaNode)[],
context,
): IProfitLossSheetEquationNode => {
return R.compose(
R.when(
this.isNodeType(ProfitLossNodeType.EQUATION),
this.equationSchemaNodeCompose(accNodes),
),
)(node);
};
/**
* Parses schema accounts nodes.
* @param {IProfitLossSchemaNode[]}
* @returns {(IProfitLossSheetNode | IProfitLossSchemaNode)[]}
*/
private reportSchemaAccountsNodesCompose = (
schemaNodes: IProfitLossSchemaNode[],
): (IProfitLossSheetNode | IProfitLossSchemaNode)[] => {
return this.mapNodesDeep(schemaNodes, this.accountsSchemaNodeMap);
};
/**
* Parses schema equation nodes.
* @param {(IProfitLossSheetNode | IProfitLossSchemaNode)[]} nodes
* @returns {(IProfitLossSheetNode | IProfitLossSchemaNode)[]}
*/
private reportSchemaEquationNodesCompose = (
nodes: (IProfitLossSheetNode | IProfitLossSchemaNode)[],
): (IProfitLossSheetNode | IProfitLossSchemaNode)[] => {
return this.mapAccNodesDeep(nodes, this.reportSchemaEquationNodeCompose);
};
/**
* Retrieve profit/loss report data.
* @return {IProfitLossSheetStatement}
*/
public reportData = (): Array<IProfitLossSheetNode> => {
const schema = this.getSchema();
return R.compose(
this.reportFilterPlugin,
this.reportRowsPercentageCompose,
this.reportColumnsPerentageCompose,
this.reportSchemaEquationNodesCompose,
this.reportSchemaAccountsNodesCompose,
)(schema);
};
}

View File

@@ -0,0 +1,195 @@
import * as moment from 'moment';
import {
IFinancialSheetBranchesQuery,
IFinancialSheetCommonMeta,
INumberFormatQuery,
} from '../../types/Report.types';
import { IFinancialTable } from '../../types/Table.types';
export enum ProfitLossAggregateNodeId {
INCOME = 'INCOME',
COS = 'COST_OF_SALES',
GROSS_PROFIT = 'GROSS_PROFIT',
EXPENSES = 'EXPENSES',
OTHER_INCOME = 'OTHER_INCOME',
OTHER_EXPENSES = 'OTHER_EXPENSES',
OPERATING_PROFIT = 'OPERATING_PROFIT',
NET_OTHER_INCOME = 'NET_OTHER_INCOME',
NET_INCOME = 'NET_INCOME',
NET_OPERATING_INCOME = 'NET_OPERATING_INCOME',
}
export enum ProfitLossNodeType {
EQUATION = 'EQUATION',
ACCOUNTS = 'ACCOUNTS',
ACCOUNT = 'ACCOUNT',
AGGREGATE = 'AGGREGATE',
}
interface FinancialDateMeta {
date: Date;
formattedDate: string;
}
export interface IFinancialNodeWithPreviousPeriod {
previousPeriodFromDate?: FinancialDateMeta;
previousPeriodToDate?: FinancialDateMeta;
previousPeriod?: IProfitLossSheetTotal;
previousPeriodChange?: IProfitLossSheetTotal;
previousPeriodPercentage?: IProfitLossSheetPercentage;
}
export interface IFinancialNodeWithPreviousYear {
previousYearFromDate: FinancialDateMeta;
previousYearToDate: FinancialDateMeta;
previousYear?: IProfitLossSheetTotal;
previousYearChange?: IProfitLossSheetTotal;
previousYearPercentage?: IProfitLossSheetPercentage;
}
export interface IFinancialCommonNode {
total: IProfitLossSheetTotal;
}
export interface IFinancialCommonHorizDatePeriodNode {
fromDate: FinancialDateMeta;
toDate: FinancialDateMeta;
total: IProfitLossSheetTotal;
}
export interface IProfitLossSheetQuery extends IFinancialSheetBranchesQuery {
basis: string;
fromDate: moment.MomentInput;
toDate: moment.MomentInput;
numberFormat: INumberFormatQuery;
noneZero: boolean;
noneTransactions: boolean;
accountsIds: number[];
displayColumnsType: 'total' | 'date_periods';
displayColumnsBy: string;
percentageColumn: boolean;
percentageRow: boolean;
percentageIncome: boolean;
percentageExpense: boolean;
previousPeriod: boolean;
previousPeriodAmountChange: boolean;
previousPeriodPercentageChange: boolean;
previousYear: boolean;
previousYearAmountChange: boolean;
previousYearPercentageChange: boolean;
}
export interface IProfitLossSheetTotal {
amount: number;
formattedAmount: string;
currencyCode: string;
}
export interface IProfitLossSheetPercentage {
amount: number;
formattedAmount: string;
}
export interface IProfitLossHorizontalDatePeriodNode
extends IFinancialNodeWithPreviousYear,
IFinancialNodeWithPreviousPeriod {
fromDate: FinancialDateMeta;
toDate: FinancialDateMeta;
total: IProfitLossSheetTotal;
percentageRow?: IProfitLossSheetPercentage;
percentageColumn?: IProfitLossSheetPercentage;
}
export interface IProfitLossSheetCommonNode
extends IFinancialNodeWithPreviousYear,
IFinancialNodeWithPreviousPeriod {
id: ProfitLossAggregateNodeId;
name: string;
children?: IProfitLossSheetNode[];
total: IProfitLossSheetTotal;
horizontalTotals?: IProfitLossHorizontalDatePeriodNode[];
percentageRow?: IProfitLossSheetPercentage;
percentageColumn?: IProfitLossSheetPercentage;
}
export interface IProfitLossSheetAccountNode
extends IProfitLossSheetCommonNode {
nodeType: ProfitLossNodeType.ACCOUNT;
}
export interface IProfitLossSheetEquationNode
extends IProfitLossSheetCommonNode {
nodeType: ProfitLossNodeType.EQUATION;
}
export interface IProfitLossSheetAccountsNode
extends IProfitLossSheetCommonNode {
nodeType: ProfitLossNodeType.ACCOUNTS;
}
export type IProfitLossSheetNode =
| IProfitLossSheetAccountsNode
| IProfitLossSheetEquationNode
| IProfitLossSheetAccountNode;
export interface IProfitLossSheetMeta extends IFinancialSheetCommonMeta {
formattedDateRange: string;
formattedFromDate: string;
formattedToDate: string;
}
// ------------------------------------------------
// # SCHEMA NODES
// ------------------------------------------------
export interface IProfitLossCommonSchemaNode {
id: ProfitLossAggregateNodeId;
name: string;
nodeType: ProfitLossNodeType;
children?: IProfitLossSchemaNode[];
alwaysShow?: boolean;
}
export interface IProfitLossEquationSchemaNode
extends IProfitLossCommonSchemaNode {
nodeType: ProfitLossNodeType.EQUATION;
equation: string;
}
export interface IProfitLossAccountsSchemaNode
extends IProfitLossCommonSchemaNode {
nodeType: ProfitLossNodeType.ACCOUNTS;
accountsTypes: string[];
}
export type IProfitLossSchemaNode =
| IProfitLossCommonSchemaNode
| IProfitLossAccountsSchemaNode
| IProfitLossEquationSchemaNode;
// ------------------------------
// # Table
// ------------------------------
export enum ProfitLossSheetRowType {
AGGREGATE = 'AGGREGATE',
ACCOUNTS = 'ACCOUNTS',
ACCOUNT = 'ACCOUNT',
TOTAL = 'TOTAL',
}
export interface IProfitLossSheetTable extends IFinancialTable {
meta: IProfitLossSheetMeta;
query: IProfitLossSheetQuery;
}

View File

@@ -0,0 +1,64 @@
import { Injectable } from '@nestjs/common';
import { ProfitLossSheetExportInjectable } from './ProfitLossSheetExportInjectable';
import { ProfitLossSheetTableInjectable } from './ProfitLossSheetTableInjectable';
import {
IProfitLossSheetQuery,
IProfitLossSheetTable,
} from './ProfitLossSheet.types';
import { ProfitLossTablePdfInjectable } from './ProfitLossTablePdfInjectable';
import { ProfitLossSheetService } from './ProfitLossSheetService';
@Injectable()
export class ProfitLossSheetApplication {
constructor(
private readonly profitLossTable: ProfitLossSheetTableInjectable,
private readonly profitLossExport: ProfitLossSheetExportInjectable,
private readonly profitLossSheet: ProfitLossSheetService,
private readonly profitLossPdf: ProfitLossTablePdfInjectable,
) {}
/**
* Retrieves the profit/loss sheet.
* @param {IProfitLossSheetQuery} query
* @returns {}
*/
public sheet(query: IProfitLossSheetQuery) {
return this.profitLossSheet.profitLossSheet(query);
}
/**
* Retrieves the profit/loss sheet table format.
* @param {IProfitLossSheetQuery} query - Profit/loss sheet query.
* @returns {Promise<IProfitLossSheetTable>}
*/
public table(query: IProfitLossSheetQuery): Promise<IProfitLossSheetTable> {
return this.profitLossTable.table(query);
}
/**
* Retrieves the profit/loss sheet in csv format.
* @param {IProfitLossSheetQuery} query
* @returns {Promise<string>}
*/
public csv(query: IProfitLossSheetQuery): Promise<string> {
return this.profitLossExport.csv(query);
}
/**
* Retrieves the profit/loss sheet in xlsx format.
* @param {IProfitLossSheetQuery} query
* @returns {Promise<Buffer>}
*/
public xlsx(query: IProfitLossSheetQuery): Promise<Buffer> {
return this.profitLossExport.xlsx(query);
}
/**
* Retrieves the profit/loss sheet in pdf format.
* @param {IProfitLossSheetQuery} query
* @returns {Promise<Buffer>}
*/
public pdf(query: IProfitLossSheetQuery): Promise<Buffer> {
return this.profitLossPdf.pdf(query);
}
}

View File

@@ -0,0 +1,42 @@
import * as R from 'ramda';
import { TOTAL_NODE_TYPES } from './constants';
import { FinancialSheet } from '../../common/FinancialSheet';
import { GConstructor } from '@/common/types/Constructor';
export const ProfitLossSheetBase = <T extends GConstructor<FinancialSheet>>(
Base: T,
) =>
class extends Base {
/**
*
* @param type
* @param node
* @returns
*/
public isNodeType = R.curry((type: string, node) => {
return node.nodeType === type;
});
/**
*
*/
protected isNodeTypeIn = R.curry((types: string[], node) => {
return types.indexOf(node.nodeType) !== -1;
});
/**
*
*/
protected findNodeById = R.curry((id, nodes) => {
return this.findNodeDeep(nodes, (node) => node.id === id);
});
/**
*
* @param node
* @returns
*/
isNodeTotal = (node) => {
return this.isNodeTypeIn(TOTAL_NODE_TYPES, node);
};
};

View File

@@ -0,0 +1,249 @@
// @ts-nocheck
import * as R from 'ramda';
import { sumBy } from 'lodash';
import {
IProfitLossHorizontalDatePeriodNode,
IProfitLossSheetAccountNode,
IProfitLossSheetAccountsNode,
IProfitLossSheetCommonNode,
IProfitLossSheetNode,
IProfitLossSheetQuery,
} from './ProfitLossSheet.types';
import { FinancialSheet } from '../../common/FinancialSheet';
import { GConstructor } from '@/common/types/Constructor';
import { FinancialDatePeriods } from '../../common/FinancialDatePeriods';
import { IDateRange } from '../../types/Report.types';
import { ProfitLossSheetRepository } from './ProfitLossSheetRepository';
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
export const ProfitLossSheetDatePeriods = <
T extends GConstructor<FinancialSheet>,
>(
Base: T,
) =>
class extends R.pipe(FinancialDatePeriods)(Base) {
query: ProfitLossSheetQuery;
repository: ProfitLossSheetRepository;
/**
* 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 {IProfitLossSheetCommonNode} node
* @param {Function} callback
* @returns {}
*/
protected getReportNodeDatePeriods = (
node: IProfitLossSheetCommonNode,
callback: (
node: IProfitLossSheetCommonNode,
fromDate: Date,
toDate: Date,
index: number,
) => any,
) => {
return this.getNodeDatePeriods(
this.query.fromDate,
this.query.toDate,
this.query.displayColumnsBy,
node,
callback,
);
};
// --------------------------
// # Account Nodes.
// --------------------------
/**
* Retrieve account node date period total.
* @param {IProfitLossSheetAccount} node
* @param {Date} fromDate
* @param {Date} toDate
* @returns {}
*/
private getAccountNodeDatePeriodTotal = (
node: IProfitLossSheetAccountNode,
fromDate: Date,
toDate: Date,
) => {
const periodTotal = this.repository.periodsAccountsLedger
.whereAccountId(node.id)
.whereFromDate(fromDate)
.whereToDate(toDate)
.getClosingBalance();
return this.getDatePeriodTotalMeta(periodTotal, fromDate, toDate);
};
/**
* Retrieve account node date period.
* @param {IProfitLossSheetAccountNode} node
* @returns {IProfitLossSheetAccountNode}
*/
public getAccountNodeDatePeriod = (node: IProfitLossSheetAccountNode) => {
return this.getReportNodeDatePeriods(
node,
this.getAccountNodeDatePeriodTotal,
);
};
/**
* Account date periods to the given account node.
* @param {IProfitLossSheetAccountNode} node
* @returns {IProfitLossSheetAccountNode}
*/
public assocAccountNodeDatePeriod = (
node: IProfitLossSheetAccountNode,
): IProfitLossSheetAccountNode => {
const datePeriods = this.getAccountNodeDatePeriod(node);
return R.assoc('horizontalTotals', datePeriods, node);
};
// --------------------------
// # Aggregate nodes.
// --------------------------
/**
* Retrieves sumation of the given aggregate node children totals.
* @param {IProfitLossSheetAccountsNode} node
* @param {number} index
* @returns {number}
*/
private getAggregateDatePeriodIndexTotal = (
node: IProfitLossSheetAccountsNode,
index: number,
): number => {
return sumBy(node.children, `horizontalTotals[${index}].total.amount`);
};
/**
*
* @param {IProfitLossSheetAccount} node
* @param {Date} fromDate
* @param {Date} toDate
* @param {number} index
* @returns {IProfitLossSheetAccount}
*/
private getAggregateNodeDatePeriodTotal = R.curry(
(
node: IProfitLossSheetAccountsNode,
fromDate: Date,
toDate: Date,
index: number,
): IProfitLossHorizontalDatePeriodNode => {
const periodTotal = this.getAggregateDatePeriodIndexTotal(node, index);
return this.getDatePeriodTotalMeta(periodTotal, fromDate, toDate);
},
);
/**
* Retrieves aggregate horizontal date periods.
* @param {IProfitLossSheetAccountsNode} node
* @returns {IProfitLossSheetAccountsNode}
*/
private getAggregateNodeDatePeriod = (
node: IProfitLossSheetAccountsNode,
): IProfitLossHorizontalDatePeriodNode[] => {
return this.getReportNodeDatePeriods(
node,
this.getAggregateNodeDatePeriodTotal,
);
};
/**
* Assoc horizontal date periods to aggregate node.
* @param {IProfitLossSheetAccountsNode} node
* @returns {IProfitLossSheetAccountsNode}
*/
protected assocAggregateDatePeriod = (
node: IProfitLossSheetAccountsNode,
): IProfitLossSheetAccountsNode => {
const datePeriods = this.getAggregateNodeDatePeriod(node);
return R.assoc('horizontalTotals', datePeriods, node);
};
// --------------------------
// # Equation nodes.
// --------------------------
/**
* Retrieves equation date period node.
* @param {IProfitLossSheetNode[]} accNodes
* @param {IProfitLossSheetNode} node
* @param {Date} fromDate
* @param {Date} toDate
* @param {number} index
* @returns {IProfitLossHorizontalDatePeriodNode}
*/
private getEquationNodeDatePeriod = R.curry(
(
accNodes: IProfitLossSheetNode[],
equation: string,
node: IProfitLossSheetNode,
fromDate: Date,
toDate: Date,
index: number,
): IProfitLossHorizontalDatePeriodNode => {
const tableNodes = this.getNodesTableForEvaluating(
`horizontalTotals[${index}].total.amount`,
accNodes,
);
// Evaluate the given equation.
const total = this.evaluateEquation(equation, tableNodes);
return this.getDatePeriodTotalMeta(total, fromDate, toDate);
},
);
/**
* Retrieves the equation node date periods.
* @param {IProfitLossSheetNode[]} node
* @param {string} equation
* @param {IProfitLossSheetNode} node
* @returns {IProfitLossHorizontalDatePeriodNode[]}
*/
private getEquationNodeDatePeriods = R.curry(
(
accNodes: IProfitLossSheetNode[],
equation: string,
node: IProfitLossSheetNode,
): IProfitLossHorizontalDatePeriodNode[] => {
return this.getReportNodeDatePeriods(
node,
this.getEquationNodeDatePeriod(accNodes, equation),
);
},
);
/**
* Assoc equation node date period.
* @param {IProfitLossSheetNode[]}
* @param {IProfitLossSheetNode} node
* @returns {IProfitLossSheetNode}
*/
protected assocEquationNodeDatePeriod = R.curry(
(
accNodes: IProfitLossSheetNode[],
equation: string,
node: IProfitLossSheetNode,
): IProfitLossSheetNode => {
const periods = this.getEquationNodeDatePeriods(
accNodes,
equation,
node,
);
return R.assoc('horizontalTotals', periods, node);
},
);
};

View File

@@ -0,0 +1,40 @@
import { Injectable } from '@nestjs/common';
import { TableSheet } from '../../common/TableSheet';
import { IProfitLossSheetQuery } from './ProfitLossSheet.types';
import { ProfitLossSheetTableInjectable } from './ProfitLossSheetTableInjectable';
@Injectable()
export class ProfitLossSheetExportInjectable {
constructor(
private readonly profitLossSheetTable: ProfitLossSheetTableInjectable,
) {}
/**
* Retrieves the profit/loss sheet in XLSX format.
* @param {IProfitLossSheetQuery} query - The profit/loss sheet query.
* @returns {Promise<Buffer>}
*/
public async xlsx(query: IProfitLossSheetQuery) {
const table = await this.profitLossSheetTable.table(query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToXLSX();
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
}
/**
* Retrieves the profit/loss sheet in CSV format.
* @param {IProfitLossSheetQuery} query
* @returns {Promise<Buffer>}
*/
public async csv(query: IProfitLossSheetQuery): Promise<string> {
const table = await this.profitLossSheetTable.table(query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
}

View File

@@ -0,0 +1,179 @@
import * as R from 'ramda';
import { get } from 'lodash';
import { ProfitLossSheetBase } from './ProfitLossSheetBase';
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from '../../common/FinancialSheet';
import { FinancialFilter } from '../../common/FinancialFilter';
import {
IProfitLossSheetNode,
ProfitLossNodeType,
} from './ProfitLossSheet.types';
import { ProfitLossSheetRepository } from './ProfitLossSheetRepository';
export const ProfitLossSheetFilter = <T extends GConstructor<FinancialSheet>>(
Base: T,
) =>
class extends R.pipe(FinancialFilter, ProfitLossSheetBase)(Base) {
query: ProfitLossSheetQuery;
repository: ProfitLossSheetRepository;
// ----------------
// # Account.
// ----------------
/**
* Filter report node detarmine.
* @param {IProfitLossSheetNode} node - Balance sheet node.
* @return {boolean}
*/
private accountNoneZeroNodesFilterDetarminer = (
node: IProfitLossSheetNode,
): boolean => {
return R.ifElse(
this.isNodeType(ProfitLossNodeType.ACCOUNT),
this.isNodeNoneZero,
R.always(true),
)(node);
};
/**
* Determines account none-transactions node.
* @param {IBalanceSheetDataNode} node
* @returns {boolean}
*/
private accountNoneTransFilterDetarminer = (
node: IProfitLossSheetNode,
): boolean => {
return R.ifElse(
this.isNodeType(ProfitLossNodeType.ACCOUNT),
this.isNodeNoneZero,
R.always(true),
)(node);
};
/**
* Report nodes filter.
* @param {IProfitLossSheetNode[]} nodes -
* @return {IProfitLossSheetNode[]}
*/
private accountsNoneZeroNodesFilter = (
nodes: IProfitLossSheetNode[],
): IProfitLossSheetNode[] => {
return this.filterNodesDeep(
nodes,
this.accountNoneZeroNodesFilterDetarminer,
);
};
/**
* Filters the accounts none-transactions nodes.
* @param {IProfitLossSheetNode[]} nodes
* @returns {IProfitLossSheetNode[]}
*/
private accountsNoneTransactionsNodesFilter = (
nodes: IProfitLossSheetNode[],
) => {
return this.filterNodesDeep(nodes, this.accountNoneTransFilterDetarminer);
};
// ----------------
// # Aggregate.
// ----------------
/**
* Determines aggregate none-children filtering.
* @param {IProfitLossSheetNode} node
* @returns {boolean}
*/
private aggregateNoneChildrenFilterDetarminer = (
node: IProfitLossSheetNode,
): boolean => {
const schemaNode = this.getSchemaNodeById(node.id);
// Determines whether the given node is aggregate node.
const isAggregateNode = this.isNodeType(
ProfitLossNodeType.ACCOUNTS,
node,
);
// Determines if the schema node is always should show.
const isSchemaAlwaysShow = get(schemaNode, 'alwaysShow', false);
// Should node has children if aggregate node or not always show.
return isAggregateNode && !isSchemaAlwaysShow
? this.isNodeHasChildren(node)
: true;
};
/**
* Filters aggregate none-children nodes.
* @param {IProfitLossSheetNode[]} nodes
* @returns {IProfitLossSheetNode[]}
*/
private aggregateNoneChildrenFilter = (
nodes: IProfitLossSheetNode[],
): IProfitLossSheetNode[] => {
return this.filterNodesDeep2(
this.aggregateNoneChildrenFilterDetarminer,
nodes,
);
};
// ----------------
// # Composers.
// ----------------
/**
* Filters none-zero nodes.
* @param {IProfitLossSheetNode[]} nodes
* @returns {IProfitLossSheetNode[]}
*/
private filterNoneZeroNodesCompose = (
nodes: IProfitLossSheetNode[],
): IProfitLossSheetNode[] => {
return R.compose(
this.aggregateNoneChildrenFilter,
this.accountsNoneZeroNodesFilter,
)(nodes);
};
/**
* Filters none-transactions nodes.
* @param {IProfitLossSheetNode[]} nodes
* @returns {IProfitLossSheetNode[]}
*/
private filterNoneTransNodesCompose = (
nodes: IProfitLossSheetNode[],
): IProfitLossSheetNode[] => {
return R.compose(
this.aggregateNoneChildrenFilter,
this.accountsNoneTransactionsNodesFilter,
)(nodes);
};
/**
* Supress nodes when total accounts range transactions is empty.
* @param {IProfitLossSheetNode[]} nodes
* @returns {IProfitLossSheetNode[]}
*/
private supressNodesWhenRangeTransactionsEmpty = (
nodes: IProfitLossSheetNode[],
) => {
return this.repository.totalAccountsLedger.isEmpty() ? [] : nodes;
};
/**
* Compose report nodes filtering.
* @param {IProfitLossSheetNode[]} nodes
* @returns {IProfitLossSheetNode[]}
*/
protected reportFilterPlugin = (
nodes: IProfitLossSheetNode[],
): IProfitLossSheetNode[] => {
return R.compose(
this.supressNodesWhenRangeTransactionsEmpty,
R.when(() => this.query.noneZero, this.filterNoneZeroNodesCompose),
R.when(
() => this.query.noneTransactions,
this.filterNoneTransNodesCompose,
),
)(nodes);
};
};

View File

@@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common';
import * as moment from 'moment';
import {
IProfitLossSheetMeta,
IProfitLossSheetQuery,
} from './ProfitLossSheet.types';
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
@Injectable()
export class ProfitLossSheetMeta {
constructor(private readonly financialSheetMeta: FinancialSheetMeta) {}
/**
* Retrieve the P/L sheet meta.
* @param {IProfitLossSheetQuery} query - P/L sheet query.
* @returns {Promise<IBalanceSheetMeta>}
*/
public async meta(
query: IProfitLossSheetQuery,
): Promise<IProfitLossSheetMeta> {
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}`;
const sheetName = 'Cashflow Statement';
return {
...commonMeta,
sheetName,
formattedFromDate,
formattedToDate,
formattedDateRange,
};
}
}

View File

@@ -0,0 +1,310 @@
// @ts-nocheck
import * as R from 'ramda';
import { GConstructor } from '@/common/types/Constructor';
import {
IProfitLossSheetNode,
ProfitLossAggregateNodeId,
} from './ProfitLossSheet.types';
import { FinancialHorizTotals } from '../../common/FinancialHorizTotals';
import { FinancialSheet } from '../../common/FinancialSheet';
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
export const ProfitLossSheetPercentage = <
T extends GConstructor<FinancialSheet>,
>(
Base: T,
) =>
class extends R.pipe(FinancialHorizTotals)(Base) {
query: ProfitLossSheetQuery;
/**
* Assoc column of percentage attribute to the given node.
* @param {IProfitLossSheetNode} netIncomeNode -
* @param {IProfitLossSheetNode} node -
* @return {IProfitLossSheetNode}
*/
private assocColumnPercentage = R.curry(
(
propertyPath: string,
parentNode: IProfitLossSheetNode,
node: IProfitLossSheetNode,
) => {
const percentage = this.getPercentageBasis(
parentNode.total.amount,
node.total.amount,
);
return R.assoc(
propertyPath,
this.getPercentageAmountMeta(percentage),
node,
);
},
);
/**
* Assoc column of percentage attribute to the given node.
* @param {IProfitLossSheetNode} netIncomeNode -
* @param {IProfitLossSheetNode} node -
* @return {IProfitLossSheetNode}
*/
private assocColumnTotalPercentage = R.curry(
(
propertyPath: string,
parentNode: IProfitLossSheetNode,
node: IProfitLossSheetNode,
) => {
const percentage = this.getPercentageBasis(
parentNode.total.amount,
node.total.amount,
);
return R.assoc(
propertyPath,
this.getPercentageTotalAmountMeta(percentage),
node,
);
},
);
/**
* Compose percentage of columns.
* @param {IProfitLossSheetNode[]} nodes
* @returns {IProfitLossSheetNode[]}
*/
private columnPercentageCompose = (
nodes: IProfitLossSheetNode[],
): IProfitLossSheetNode[] => {
const netIncomeNode = this.findNodeById(
ProfitLossAggregateNodeId.NET_INCOME,
nodes,
);
return this.mapNodesDeep(
nodes,
this.columnPercentageMapper(netIncomeNode),
);
};
/**
* Compose percentage of income.
* @param {IProfitLossSheetNode[]} nodes
* @returns {IProfitLossSheetNode[]}
*/
private incomePercetageCompose = (
nodes: IProfitLossSheetNode[],
): IProfitLossSheetNode[] => {
const incomeNode = this.findNodeById(
ProfitLossAggregateNodeId.INCOME,
nodes,
);
return this.mapNodesDeep(nodes, this.incomePercentageMapper(incomeNode));
};
/**
*
* @param {IProfitLossSheetNode[]} nodes
* @returns {IProfitLossSheetNode[]}
*/
private rowPercentageCompose = (
nodes: IProfitLossSheetNode[],
): IProfitLossSheetNode[] => {
return this.mapNodesDeep(nodes, this.rowPercentageMap);
};
/**
*
* @param {IProfitLossSheetNode} netIncomeNode -
* @param {IProfitLossSheetNode} node -
* @return {IProfitLossSheetNode}
*/
private columnPercentageMapper = R.curry(
(netIncomeNode: IProfitLossSheetNode, node: IProfitLossSheetNode) => {
const path = 'percentageColumn';
return R.compose(
R.when(
this.isNodeHasHorizTotals,
this.assocColumnPercentageHorizTotals(netIncomeNode),
),
R.ifElse(
this.isNodeTotal,
this.assocColumnTotalPercentage(path, netIncomeNode),
this.assocColumnPercentage(path, netIncomeNode),
),
)(node);
},
);
/**
*
* @param {IProfitLossSheetNode} node
* @returns {IProfitLossSheetNode}
*/
private rowPercentageMap = (
node: IProfitLossSheetNode,
): IProfitLossSheetNode => {
const path = 'percentageRow';
return R.compose(
R.when(this.isNodeHasHorizTotals, this.assocRowPercentageHorizTotals),
R.ifElse(
this.isNodeTotal,
this.assocColumnTotalPercentage(path, node),
this.assocColumnPercentage(path, node),
),
)(node);
};
/**
*
* @param {IProfitLossSheetNode} incomeNode -
* @param {IProfitLossSheetNode} node -
* @returns {IProfitLossSheetNode}
*/
private incomePercentageMapper = R.curry(
(incomeNode: IProfitLossSheetNode, node: IProfitLossSheetNode) => {
const path = 'percentageIncome';
return R.compose(
R.when(
this.isNodeHasHorizTotals,
this.assocIncomePercentageHorizTotals(incomeNode),
),
R.ifElse(
this.isNodeTotal,
this.assocColumnTotalPercentage(path, incomeNode),
this.assocColumnPercentage(path, incomeNode),
),
)(node);
},
);
/**
*
* @param {IProfitLossSheetNode} expenseNode -
* @param {IProfitLossSheetNode} node -
*/
private expensePercentageMapper = R.curry(
(expenseNode: IProfitLossSheetNode, node: IProfitLossSheetNode) => {
const path = 'percentageExpense';
return R.compose(
R.when(
this.isNodeHasHorizTotals,
this.assocExpensePercentageHorizTotals(expenseNode),
),
R.ifElse(
this.isNodeTotal,
this.assocColumnTotalPercentage(path, expenseNode),
this.assocColumnPercentage(path, expenseNode),
),
)(node);
},
);
/**
* Compose percentage of expense.
* @param {IProfitLossSheetNode[]} nodes
* @returns {IProfitLossSheetNode[]}
*/
private expensesPercentageCompose = (
nodes: IProfitLossSheetNode[],
): IProfitLossSheetNode[] => {
const expenseNode = this.findNodeById(
ProfitLossAggregateNodeId.EXPENSES,
nodes,
);
return this.mapNodesDeep(
nodes,
this.expensePercentageMapper(expenseNode),
);
};
/**
* Compose percentage attributes.
* @param {IProfitLossSheetNode[]} nodes
* @returns {IProfitLossSheetNode[]}
*/
protected reportColumnsPerentageCompose = (
nodes: IProfitLossSheetNode[],
): IProfitLossSheetNode[] => {
return R.compose(
R.when(this.query.isIncomePercentage, this.incomePercetageCompose),
R.when(this.query.isColumnPercentage, this.columnPercentageCompose),
R.when(this.query.isExpensesPercentage, this.expensesPercentageCompose),
R.when(this.query.isRowPercentage, this.rowPercentageCompose),
)(nodes);
};
/**
*
* @param {} nodes
* @returns {}
*/
protected reportRowsPercentageCompose = (nodes) => {
return nodes;
};
// ----------------------------------
// # Horizontal Nodes
// ----------------------------------
/**
* Assoc incomer percentage to horizontal totals nodes.
* @param {IProfitLossSheetNode} incomeNode -
* @param {IProfitLossSheetNode} node -
* @returns {IProfitLossSheetNode}
*/
private assocIncomePercentageHorizTotals = R.curry(
(incomeNode: IProfitLossSheetNode, node: IProfitLossSheetNode) => {
const horTotalsWithIncomePerc = this.assocPercentageHorizTotals(
'percentageIncome',
incomeNode,
node,
);
return R.assoc('horizontalTotals', horTotalsWithIncomePerc, node);
},
);
/**
* Assoc expense percentage to horizontal totals nodes.
* @param {IProfitLossSheetNode} expenseNode -
* @param {IProfitLossSheetNode} node -
* @returns {IProfitLossSheetNode}
*/
private assocExpensePercentageHorizTotals = R.curry(
(expenseNode: IProfitLossSheetNode, node: IProfitLossSheetNode) => {
const horTotalsWithExpensePerc = this.assocPercentageHorizTotals(
'percentageExpense',
expenseNode,
node,
);
return R.assoc('horizontalTotals', horTotalsWithExpensePerc, node);
},
);
/**
* Assoc net income percentage to horizontal totals nodes.
* @param {IProfitLossSheetNode} expenseNode -
* @param {IProfitLossSheetNode} node -
* @returns {IProfitLossSheetNode}
*/
private assocColumnPercentageHorizTotals = R.curry(
(netIncomeNode: IProfitLossSheetNode, node: IProfitLossSheetNode) => {
const horTotalsWithExpensePerc = this.assocPercentageHorizTotals(
'percentageColumn',
netIncomeNode,
node,
);
return R.assoc('horizontalTotals', horTotalsWithExpensePerc, node);
},
);
/**
*
*/
private assocRowPercentageHorizTotals = R.curry((node) => {
const horTotalsWithExpensePerc = this.assocHorizontalPercentageTotals(
'percentageRow',
node,
);
return R.assoc('horizontalTotals', horTotalsWithExpensePerc, node);
});
};

View File

@@ -0,0 +1,404 @@
// @ts-nocheck
import * as R from 'ramda';
import { sumBy } from 'lodash';
import {
IProfitLossHorizontalDatePeriodNode,
IProfitLossSchemaNode,
IProfitLossSheetAccountNode,
IProfitLossSheetAccountsNode,
IProfitLossSheetEquationNode,
IProfitLossSheetNode,
} from './ProfitLossSheet.types';
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from '../../common/FinancialSheet';
import { FinancialPreviousPeriod } from '../../common/FinancialPreviousPeriod';
import { ProfitLossSheetRepository } from './ProfitLossSheetRepository';
export const ProfitLossSheetPreviousPeriod = <
T extends GConstructor<FinancialSheet>,
>(
Base: T,
) =>
class extends R.pipe(FinancialPreviousPeriod)(Base) {
query: ProfitLossSheetQuery;
repository: ProfitLossSheetRepository;
// ---------------------------
// # Account
// ---------------------------
/**
* Assoc previous period change attribute to account node.
* @param {IProfitLossSheetAccountNode} accountNode
* @returns {IProfitLossSheetAccountNode}
*/
protected assocPreviousPeriodTotalAccountNode = (
node: IProfitLossSheetAccountNode,
): IProfitLossSheetAccountNode => {
const total = this.repository.PPTotalAccountsLedger.whereAccountId(
node.id,
).getClosingBalance();
return R.assoc('previousPeriod', this.getAmountMeta(total), node);
};
/**
* Compose previous period account node.
* @param {IProfitLossSheetAccountNode} accountNode
* @returns {IProfitLossSheetAccountNode}
*/
protected previousPeriodAccountNodeCompose = (
accountNode: IProfitLossSheetAccountNode,
): IProfitLossSheetAccountNode => {
return R.compose(
R.when(
this.isNodeHasHorizTotals,
this.assocPreviousPeriodAccountHorizNodeCompose,
),
R.when(
this.query.isPreviousPeriodPercentageActive,
this.assocPreviousPeriodPercentageNode,
),
R.when(
this.query.isPreviousPeriodChangeActive,
this.assocPreviousPeriodChangeNode,
),
this.assocPreviousPeriodTotalAccountNode,
)(accountNode);
};
// ---------------------------
// # Aggregate
// ---------------------------
/**
* Assoc previous period total attribute to aggregate node.
* @param {IProfitLossSheetAccountNode} accountNode
* @returns {IProfitLossSheetAccountNode}
*/
private assocPreviousPeriodTotalAggregateNode = (
node: IProfitLossSheetAccountNode,
) => {
const total = sumBy(node.children, 'previousPeriod.amount');
return R.assoc('previousPeriod', this.getTotalAmountMeta(total), node);
};
/**
* Compose previous period to aggregate node.
* @param {IProfitLossSheetAccountNode} accountNode
* @returns {IProfitLossSheetAccountNode}
*/
protected previousPeriodAggregateNodeCompose = (
accountNode: IProfitLossSheetAccountNode,
): IProfitLossSheetAccountNode => {
return R.compose(
R.when(
this.isNodeHasHorizTotals,
this.assocPreviousPeriodAggregateHorizNode,
),
R.when(
this.query.isPreviousPeriodPercentageActive,
this.assocPreviousPeriodTotalPercentageNode,
),
R.when(
this.query.isPreviousPeriodChangeActive,
this.assocPreviousPeriodTotalChangeNode,
),
this.assocPreviousPeriodTotalAggregateNode,
)(accountNode);
};
// ---------------------------
// # Equation
// --------------------------
/**
*
* @param {(IProfitLossSchemaNode | IProfitLossSheetNode)[]} accNodes
* @param {string} equation
* @param {IProfitLossSheetNode} node
* @returns {IProfitLossSheetEquationNode}
*/
private assocPreviousPeriodTotalEquationNode = R.curry(
(
accNodes: (IProfitLossSchemaNode | IProfitLossSheetNode)[],
equation: string,
node: IProfitLossSheetEquationNode,
): IProfitLossSheetEquationNode => {
const previousPeriodNodePath = 'previousPeriod.amount';
const tableNodes = this.getNodesTableForEvaluating(
previousPeriodNodePath,
accNodes,
);
// Evaluate the given equation.
const total = this.evaluateEquation(equation, tableNodes);
return R.assoc('previousPeriod', this.getTotalAmountMeta(total), node);
},
);
/**
*
* @param {(IProfitLossSchemaNode | IProfitLossSheetNode)[]} accNodes -
* @param {string} node
* @param {IProfitLossSheetEquationNode} node
* @returns {IProfitLossSheetEquationNode}
*/
protected previousPeriodEquationNodeCompose = R.curry(
(
accNodes: (IProfitLossSchemaNode | IProfitLossSheetNode)[],
equation: string,
node: IProfitLossSheetEquationNode,
): IProfitLossSheetEquationNode => {
return R.compose(
R.when(
this.isNodeHasHorizTotals,
this.assocPreviousPeriodEquationHorizNode(accNodes, equation),
),
R.when(
this.query.isPreviousPeriodPercentageActive,
this.assocPreviousPeriodTotalPercentageNode,
),
R.when(
this.query.isPreviousPeriodChangeActive,
this.assocPreviousPeriodTotalChangeNode,
),
this.assocPreviousPeriodTotalEquationNode(accNodes, equation),
)(node);
},
);
// ---------------------------
// # Horizontal Nodes - Account
// --------------------------
/**
* Assoc previous period to account horizontal node.
* @param {IProfitLossSheetAccountNode} node
* @param {IProfitLossHorizontalDatePeriodNode} totalNode
* @returns {IProfitLossHorizontalDatePeriodNode}
*/
private assocPerviousPeriodAccountHorizTotal = R.curry(
(
node: IProfitLossSheetAccountNode,
totalNode: IProfitLossHorizontalDatePeriodNode,
): IProfitLossHorizontalDatePeriodNode => {
const total = this.repository.PPPeriodsAccountsLedger.whereAccountId(
node.id,
)
.whereFromDate(totalNode.previousPeriodFromDate.date)
.whereToDate(totalNode.previousPeriodToDate.date)
.getClosingBalance();
return R.assoc('previousPeriod', this.getAmountMeta(total), totalNode);
},
);
/**
* @param {IProfitLossSheetAccountNode} node
* @param {IProfitLossSheetTotal}
*/
private previousPeriodAccountHorizNodeCompose = R.curry(
(
node: IProfitLossSheetAccountNode,
horizontalTotalNode: IProfitLossHorizontalDatePeriodNode,
index: number,
): IProfitLossHorizontalDatePeriodNode => {
return R.compose(
R.when(
this.query.isPreviousPeriodPercentageActive,
this.assocPreviousPeriodPercentageNode,
),
R.when(
this.query.isPreviousPeriodChangeActive,
this.assocPreviousPeriodChangeNode,
),
this.assocPerviousPeriodAccountHorizTotal(node),
this.assocPreviousPeriodHorizNodeFromToDates(
this.query.displayColumnsBy,
),
)(horizontalTotalNode);
},
);
/**
*
* @param {IProfitLossSheetAccountNode} node
* @returns {IProfitLossSheetAccountNode}
*/
private assocPreviousPeriodAccountHorizNodeCompose = (
node: IProfitLossSheetAccountNode,
): IProfitLossSheetAccountNode => {
const horizontalTotals = R.addIndex(R.map)(
this.previousPeriodAccountHorizNodeCompose(node),
node.horizontalTotals,
);
return R.assoc('horizontalTotals', horizontalTotals, node);
};
// ----------------------------------
// # Horizontal Nodes - Aggregate
// ----------------------------------
/**
* Assoc previous period total to aggregate horizontal nodes.
* @param {IProfitLossSheetAccountsNode} node
* @param {number} index
* @param {any} totalNode
* @return {}
*/
private assocPreviousPeriodAggregateHorizTotal = R.curry(
(
node: IProfitLossSheetAccountsNode,
index: number,
totalNode: IProfitLossHorizontalDatePeriodNode,
) => {
const total = this.getPPHorizNodesTotalSumation(index, node);
return R.assoc(
'previousPeriod',
this.getTotalAmountMeta(total),
totalNode,
);
},
);
/**
*
* @param {IProfitLossSheetAccountsNode} node
* @param {IProfitLossHorizontalDatePeriodNode} horizontalTotalNode -
* @param {number} index
* @returns {IProfitLossHorizontalDatePeriodNode}
*/
private previousPeriodAggregateHorizNodeCompose = R.curry(
(
node: IProfitLossSheetAccountsNode,
horizontalTotalNode: IProfitLossHorizontalDatePeriodNode,
index: number,
): IProfitLossHorizontalDatePeriodNode => {
return R.compose(
R.when(
this.query.isPreviousPeriodPercentageActive,
this.assocPreviousPeriodTotalPercentageNode,
),
R.when(
this.query.isPreviousPeriodChangeActive,
this.assocPreviousPeriodTotalChangeNode,
),
R.when(
this.query.isPreviousPeriodActive,
this.assocPreviousPeriodAggregateHorizTotal(node, index),
),
R.when(
this.query.isPreviousPeriodActive,
this.assocPreviousPeriodHorizNodeFromToDates(
this.query.displayColumnsBy,
),
),
)(horizontalTotalNode);
},
);
/**
* Assoc previous period to aggregate horizontal nodes.
* @param {IProfitLossSheetAccountsNode} node
* @returns
*/
private assocPreviousPeriodAggregateHorizNode = (
node: IProfitLossSheetAccountsNode,
): IProfitLossSheetAccountsNode => {
const horizontalTotals = R.addIndex(R.map)(
this.previousPeriodAggregateHorizNodeCompose(node),
node.horizontalTotals,
);
return R.assoc('horizontalTotals', horizontalTotals, node);
};
// ----------------------------------
// # Horizontal Nodes - Equation
// ----------------------------------
/**
*
* @param {IProfitLossSheetNode[]} accNodes -
* @param {string} equation
* @param {index} number
* @param {} totalNode
*/
private assocPreviousPeriodEquationHorizTotal = R.curry(
(
accNodes: IProfitLossSheetNode[],
equation: string,
index: number,
totalNode,
): IProfitLossSheetNode => {
const scopes = this.getNodesTableForEvaluating(
`horizontalTotals[${index}].previousPeriod.amount`,
accNodes,
);
const total = this.evaluateEquation(equation, scopes);
return R.assoc(
'previousPeriod',
this.getTotalAmountMeta(total),
totalNode,
);
},
);
/**
*
* @param {IProfitLossSheetNode[]} accNodes -
* @param {string} equation
* @param {} horizontalTotalNode
* @param {number} index
*/
private previousPeriodEquationHorizNodeCompose = R.curry(
(
accNodes: IProfitLossSheetNode[],
equation: string,
horizontalTotalNode,
index: number,
) => {
const assocHorizTotal = this.assocPreviousPeriodEquationHorizTotal(
accNodes,
equation,
index,
);
return R.compose(
R.when(
this.query.isPreviousPeriodPercentageActive,
this.assocPreviousPeriodTotalPercentageNode,
),
R.when(
this.query.isPreviousPeriodChangeActive,
this.assocPreviousPeriodTotalChangeNode,
),
R.when(this.query.isPreviousPeriodActive, assocHorizTotal),
R.when(
this.query.isPreviousPeriodActive,
this.assocPreviousPeriodHorizNodeFromToDates(
this.query.displayColumnsBy,
),
),
)(horizontalTotalNode);
},
);
/**
* Assoc previous period equation to horizontal nodes.
* @parma {IProfitLossSheetNode[]} accNodes -
* @param {string} equation
* @param {IProfitLossSheetEquationNode} node
* @return {IProfitLossSheetEquationNode}
*/
private assocPreviousPeriodEquationHorizNode = R.curry(
(
accNodes: IProfitLossSheetNode[],
equation: string,
node: IProfitLossSheetEquationNode,
): IProfitLossSheetEquationNode => {
const horizontalTotals = R.addIndex(R.map)(
this.previousPeriodEquationHorizNodeCompose(accNodes, equation),
node.horizontalTotals,
);
return R.assoc('horizontalTotals', horizontalTotals, node);
},
);
};

View File

@@ -0,0 +1,376 @@
// @ts-nocheck
import * as R from 'ramda';
import { sumBy } from 'lodash';
import {
IProfitLossSheetEquationNode,
IProfitLossSheetAccountNode,
IProfitLossSchemaNode,
IProfitLossSheetNode,
IProfitLossSheetTotal,
IProfitLossSheetQuery,
} from './ProfitLossSheet.types';
import { ProfitLossSheetRepository } from './ProfitLossSheetRepository';
import { FinancialPreviousYear } from '../../common/FinancialPreviousYear';
import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from '../../common/FinancialSheet';
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
export const ProfitLossSheetPreviousYear = <
T extends GConstructor<FinancialSheet>,
>(
Base: T,
) =>
class extends R.pipe(FinancialPreviousYear)(Base) {
repository: ProfitLossSheetRepository;
query: ProfitLossSheetQuery;
// ---------------------------
// # Account
// ---------------------------
/**
* Assoc previous year total attribute to account node.
* @param {IProfitLossSheetAccountNode} accountNode
* @returns {IProfitLossSheetAccountNode}
*/
private assocPreviousYearTotalAccountNode = (
accountNode: IProfitLossSheetAccountNode,
) => {
const total = this.repository.PYTotalAccountsLedger.whereAccountId(
accountNode.id,
).getClosingBalance();
return R.assoc('previousYear', this.getAmountMeta(total), accountNode);
};
/**
* Compose previous year account node.
* @param {IProfitLossSheetAccountNode} accountNode
* @returns {IProfitLossSheetAccountNode}
*/
protected previousYearAccountNodeCompose = (
accountNode: IProfitLossSheetAccountNode,
): IProfitLossSheetAccountNode => {
return R.compose(
R.when(
this.isNodeHasHorizTotals,
this.assocPreviousYearAccountHorizNodeCompose,
),
R.when(
this.query.isPreviousYearPercentageActive,
this.assocPreviousYearPercentageNode,
),
R.when(
this.query.isPreviousYearChangeActive,
this.assocPreviousYearChangetNode,
),
this.assocPreviousYearTotalAccountNode,
)(accountNode);
};
// ---------------------------
// # Aggregate
// ---------------------------
/**
* Assoc previous year change attribute to aggregate node.
* @param {IProfitLossSheetAccountNode} accountNode
* @returns {IProfitLossSheetAccountNode}
*/
private assocPreviousYearTotalAggregateNode = (
node: IProfitLossSheetAccountNode,
): IProfitLossSheetAccountNode => {
const total = sumBy(node.children, 'previousYear.amount');
return R.assoc('previousYear', this.getTotalAmountMeta(total), node);
};
/**
* Compose previous year to aggregate node.
* @param {IProfitLossSheetAccountNode} accountNode
* @returns {IProfitLossSheetAccountNode}
*/
protected previousYearAggregateNodeCompose = (
accountNode: IProfitLossSheetAccountNode,
): IProfitLossSheetAccountNode => {
return R.compose(
R.when(
this.isNodeHasHorizTotals,
this.assocPreviousYearAggregateHorizNode,
),
R.when(
this.query.isPreviousYearPercentageActive,
this.assocPreviousYearTotalPercentageNode,
),
R.when(
this.query.isPreviousYearChangeActive,
this.assocPreviousYearTotalChangeNode,
),
this.assocPreviousYearTotalAggregateNode,
)(accountNode);
};
// ---------------------------
// # Equation
// ---------------------------
/**
* Assoc previous year total to equation node.
* @param {(IProfitLossSchemaNode | IProfitLossSheetNode)[]} accNodes
* @param {string} equation
* @param {IProfitLossSheetNode} node
* @returns {IProfitLossSheetEquationNode}
*/
private assocPreviousYearTotalEquationNode = R.curry(
(
accNodes: (IProfitLossSchemaNode | IProfitLossSheetNode)[],
equation: string,
node: IProfitLossSheetNode,
) => {
const previousPeriodNodePath = 'previousYear.amount';
const tableNodes = this.getNodesTableForEvaluating(
previousPeriodNodePath,
accNodes,
);
// Evaluate the given equation.
const total = this.evaluateEquation(equation, tableNodes);
return R.assoc('previousYear', this.getTotalAmountMeta(total), node);
},
);
/**
* Previous year equation node.
* @param {(IProfitLossSchemaNode | IProfitLossSheetNode)[]} accNodes -
* @param {string} node
* @param {IProfitLossSheetEquationNode} node
* @returns {IProfitLossSheetEquationNode}
*/
protected previousYearEquationNodeCompose = R.curry(
(
accNodes: (IProfitLossSchemaNode | IProfitLossSheetNode)[],
equation: string,
node: IProfitLossSheetEquationNode,
) => {
return R.compose(
R.when(
this.isNodeHasHorizTotals,
this.assocPreviousYearEquationHorizNode(accNodes, equation),
),
R.when(
this.query.isPreviousYearPercentageActive,
this.assocPreviousYearTotalPercentageNode,
),
R.when(
this.query.isPreviousYearChangeActive,
this.assocPreviousYearTotalChangeNode,
),
this.assocPreviousYearTotalEquationNode(accNodes, equation),
)(node);
},
);
// ----------------------------------
// # Horizontal Nodes - Account
// ----------------------------------
/**
* Assoc preivous year to account horizontal total node.
* @param {IProfitLossSheetAccountNode} node
* @returns
*/
private assocPreviousYearAccountHorizTotal = R.curry(
(node: IProfitLossSheetAccountNode, totalNode) => {
const total = this.repository.PYPeriodsAccountsLedger.whereAccountId(
node.id,
)
.whereFromDate(totalNode.previousYearFromDate.date)
.whereToDate(totalNode.previousYearToDate.date)
.getClosingBalance();
return R.assoc('previousYear', this.getAmountMeta(total), totalNode);
},
);
/**
* Previous year account horizontal node composer.
* @param {IProfitLossSheetAccountNode} horizontalTotalNode
* @param {IProfitLossSheetTotal} horizontalTotalNode -
* @returns {IProfitLossSheetTotal}
*/
private previousYearAccountHorizNodeCompose = R.curry(
(
node: IProfitLossSheetAccountNode,
horizontalTotalNode: IProfitLossSheetTotal,
): IProfitLossSheetTotal => {
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);
},
);
/**
*
* @param {IProfitLossSheetAccountNode} node
* @returns {IProfitLossSheetAccountNode}
*/
private assocPreviousYearAccountHorizNodeCompose = (
node: IProfitLossSheetAccountNode,
): IProfitLossSheetAccountNode => {
const horizontalTotals = R.map(
this.previousYearAccountHorizNodeCompose(node),
node.horizontalTotals,
);
return R.assoc('horizontalTotals', horizontalTotals, node);
};
// ----------------------------------
// # Horizontal Nodes - Aggregate
// ----------------------------------
/**
*
*/
private assocPreviousYearAggregateHorizTotal = R.curry(
(node, index, totalNode) => {
const total = this.getPYHorizNodesTotalSumation(index, node);
return R.assoc(
'previousYear',
this.getTotalAmountMeta(total),
totalNode,
);
},
);
/**
*
*/
private previousYearAggregateHorizNodeCompose = R.curry(
(node, horizontalTotalNode, index: number) => {
return R.compose(
R.when(
this.query.isPreviousYearPercentageActive,
this.assocPreviousYearTotalPercentageNode,
),
R.when(
this.query.isPreviousYearChangeActive,
this.assocPreviousYearTotalChangeNode,
),
R.when(
this.query.isPreviousYearActive,
this.assocPreviousYearAggregateHorizTotal(node, index),
),
)(horizontalTotalNode);
},
);
/**
*
* @param {IProfitLossSheetAccountNode} node
* @returns {IProfitLossSheetAccountNode}
*/
private assocPreviousYearAggregateHorizNode = (
node: IProfitLossSheetAccountNode,
): IProfitLossSheetAccountNode => {
const horizontalTotals = R.addIndex(R.map)(
this.previousYearAggregateHorizNodeCompose(node),
node.horizontalTotals,
);
return R.assoc('horizontalTotals', horizontalTotals, node);
};
// ----------------------------------
// # Horizontal Nodes - Equation
// ----------------------------------
/**
*
* @param {IProfitLossSheetNode[]} accNodes -
* @param {string} equation
* @param {number} index
* @param {} totalNode -
*/
private assocPreviousYearEquationHorizTotal = R.curry(
(
accNodes: IProfitLossSheetNode[],
equation: string,
index: number,
totalNode,
) => {
const scopes = this.getNodesTableForEvaluating(
`horizontalTotals[${index}].previousYear.amount`,
accNodes,
);
const total = this.evaluateEquation(equation, scopes);
return R.assoc(
'previousYear',
this.getTotalAmountMeta(total),
totalNode,
);
},
);
/**
*
* @param {IProfitLossSheetNode[]} accNodes -
* @param {string} equation
* @param {} horizontalTotalNode
* @param {number} index
*/
private previousYearEquationHorizNodeCompose = R.curry(
(
accNodes: IProfitLossSheetNode[],
equation: string,
horizontalTotalNode,
index: number,
) => {
const assocHorizTotal = this.assocPreviousYearEquationHorizTotal(
accNodes,
equation,
index,
);
return R.compose(
R.when(
this.query.isPreviousYearPercentageActive,
this.assocPreviousYearTotalPercentageNode,
),
R.when(
this.query.isPreviousYearChangeActive,
this.assocPreviousYearTotalChangeNode,
),
R.when(this.query.isPreviousYearActive, assocHorizTotal),
)(horizontalTotalNode);
},
);
/**
*
* @param {IProfitLossSheetNode[]} accNodes
* @param {string} equation
* @param {IProfitLossSheetEquationNode} node
*/
private assocPreviousYearEquationHorizNode = R.curry(
(
accNodes: IProfitLossSheetNode[],
equation: string,
node: IProfitLossSheetEquationNode,
) => {
const horizontalTotals = R.addIndex(R.map)(
this.previousYearEquationHorizNodeCompose(accNodes, equation),
node.horizontalTotals,
);
return R.assoc('horizontalTotals', horizontalTotals, node);
},
);
};

View File

@@ -0,0 +1,210 @@
import { merge } from 'lodash';
import * as R from 'ramda';
import { IProfitLossSheetQuery } from './ProfitLossSheet.types';
import { FinancialDateRanges } from '../../common/FinancialDateRanges';
import { IFinancialDatePeriodsUnit } from '../../types/Report.types';
import { DISPLAY_COLUMNS_BY } from './constants';
export class ProfitLossSheetQuery extends R.compose(FinancialDateRanges)(
class {},
) {
/**
* P&L query.
* @param {IProfitLossSheetQuery}
*/
public readonly query: IProfitLossSheetQuery;
/**
* 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 {IProfitLossSheetQuery} query
*/
constructor(query: IProfitLossSheetQuery) {
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 periods 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);
}
/**
* Determines the given display columns type.
* @param {string} displayColumnsBy
* @returns {boolean}
*/
public isDisplayColumnsBy = (displayColumnsBy: string): boolean => {
return this.query.displayColumnsBy === displayColumnsBy;
};
/**
* Determines the given display columns by type.
* @param {string} displayColumnsBy
* @returns {boolean}
*/
public isDisplayColumnsType = (displayColumnsType: string): boolean => {
return this.query.displayColumnsType === displayColumnsType;
};
/**
* Determines whether the columns type is date periods.
* @returns {boolean}
*/
public isDatePeriodsColumnsType = (): boolean => {
return this.isDisplayColumnsType(DISPLAY_COLUMNS_BY.DATE_PERIODS);
};
/**
* Determines whether the columns type is total.
* @returns {boolean}
*/
public isTotalColumnType = (): boolean => {
return this.isDisplayColumnsType(DISPLAY_COLUMNS_BY.TOTAL);
};
// --------------------------------------
// # Previous Year (PY)
// --------------------------------------
/**
* Determines the report query has previous year enabled.
* @returns {boolean}
*/
public isPreviousYearActive = (): boolean => {
return this.query.previousYear;
};
/**
* Determines the report query has previous year percentage change active.
* @returns {boolean}
*/
public isPreviousYearPercentageActive = (): boolean => {
return this.query.previousYearPercentageChange;
};
/**
* Determines the report query has previous year change active.
* @returns {boolean}
*/
public isPreviousYearChangeActive = (): boolean => {
return this.query.previousYearAmountChange;
};
/**
* Retrieves PY date based on the current query.
* @returns {Date}
*/
public getTotalPreviousYear = (): Date => {
return this.PYFromDate;
};
// --------------------------------------
// # Previous Period (PP)
// --------------------------------------
/**
* Determines the report query has previous period enabled.
* @returns {boolean}
*/
public isPreviousPeriodActive = (): boolean => {
return this.query.previousPeriod;
};
/**
* Determines the report query has previous period percentage change active.
* @returns {boolean}
*/
public isPreviousPeriodPercentageActive = (): boolean => {
return this.query.previousPeriodPercentageChange;
};
/**
* Determines the report query has previous period change active.
* @returns {boolean}
*/
public isPreviousPeriodChangeActive = (): boolean => {
return this.query.previousPeriodAmountChange;
};
/**
* Retrieves previous period date based on the current query.
* @returns {Date}
*/
public getTotalPreviousPeriod = (): Date => {
return this.PPFromDate;
};
// --------------------------------------
// # Percentage vertical/horizontal.
// --------------------------------------
/**
* Determines whether percentage of expenses is active.
* @returns {boolean}
*/
public isExpensesPercentage = (): boolean => {
return this.query.percentageExpense;
};
/**
* Determines whether percentage of income is active.
* @returns {boolean}
*/
public isIncomePercentage = (): boolean => {
return this.query.percentageIncome;
};
/**
* Determines whether percentage of column is active.
* @returns {boolean}
*/
public isColumnPercentage = (): boolean => {
return this.query.percentageColumn;
};
/**
* Determines whether percentage of row is active.
* @returns {boolean}
*/
public isRowPercentage = (): boolean => {
return this.query.percentageRow;
};
}

View File

@@ -0,0 +1,380 @@
import { Inject, Injectable, Scope } from '@nestjs/common';
import { ModelObject } from 'objection';
import { castArray } from 'lodash';
import * as R from 'ramda';
import { Knex } from 'knex';
import { isEmpty } from 'lodash';
import { transformToMapBy } from '@/utils/transform-to-map-by';
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
import { Ledger } from '@/modules/Ledger/Ledger';
import { IProfitLossSheetQuery } from './ProfitLossSheet.types';
import { IAccountTransactionsGroupBy } from '../BalanceSheet/BalanceSheet.types';
import { Account } from '@/modules/Accounts/models/Account.model';
import { FinancialDatePeriods } from '../../common/FinancialDatePeriods';
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
@Injectable({ scope: Scope.TRANSIENT })
export class ProfitLossSheetRepository extends R.compose(FinancialDatePeriods)(
class {},
) {
@Inject(Account.name)
public accountModel: typeof Account;
@Inject(AccountTransaction.name)
public accountTransactionModel: typeof AccountTransaction;
@Inject(TenancyContext)
public tenancyContext: TenancyContext;
/**
* Tenancy base currency.
* @param {string}
*/
public baseCurrency: string;
/**
* Accounts by type.
*/
public accountsByType: any;
/**
* @param {ModelObject<Account>[]}
*/
public accounts: ModelObject<Account>[];
/**
* Accounts graph.
*/
public accountsGraph: any;
/**
* Transactions group type.
* @param {IAccountTransactionsGroupBy}
*/
public transactionsGroupType: IAccountTransactionsGroupBy =
IAccountTransactionsGroupBy.Month;
/**
* @param {IProfitLossSheetQuery}
*/
public query: ProfitLossSheetQuery;
/**
* Previous year to date.
* @param {Date}
*/
public PYToDate: Date;
/**
* Previous year from date.
* @param {Date}
*/
public PYFromDate: Date;
/**
* Previous year to date.
* @param {Date}
*/
public PPToDate: Date;
/**
* Previous year from date.
* @param {Date}
*/
public PPFromDate: Date;
// ------------------------
// # Total
// ------------------------
/**
* Accounts total.
* @param {Ledger}
*/
public totalAccountsLedger: Ledger;
// ------------------------
// # Date Periods.
// ------------------------
/**
* Accounts date periods.
* @param {Ledger}
*/
public periodsAccountsLedger: Ledger;
// ------------------------
// # Previous Year (PY)
// ------------------------
/**
* @param {Ledger}
*/
public PYTotalAccountsLedger: Ledger;
/**
*
* @param {Ledger}
*/
public PYPeriodsAccountsLedger: Ledger;
// ------------------------
// # Previous Period (PP).
// ------------------------
/**
* PP Accounts Periods.
* @param {Ledger}
*/
public PPPeriodsAccountsLedger: Ledger;
/**
* PP Accounts Total.
* @param {Ledger}
*/
public PPTotalAccountsLedger: Ledger;
/**
* Set the filter of the report.
* @param {IBalanceSheetQuery} query
*/
setFilter(query: IProfitLossSheetQuery) {
this.query = new ProfitLossSheetQuery(query);
this.transactionsGroupType = this.getGroupByFromDisplayColumnsBy(
this.query.displayColumnsBy,
);
}
/**
* Async report repository.
*/
public asyncInitialize = async () => {
await this.initBaseCurrency();
await this.initAccounts();
await this.initAccountsGraph();
await this.initAccountsTotalLedger();
// Date Periods.
if (this.query.isDatePeriodsColumnsType()) {
await this.initTotalDatePeriods();
}
// Previous Period (PP)
if (this.query.isPreviousPeriodActive()) {
await this.initTotalPreviousPeriod();
}
if (
this.query.isPreviousPeriodActive() &&
this.query.isDatePeriodsColumnsType()
) {
await this.initPeriodsPreviousPeriod();
}
// Previous Year (PY).
if (this.query.isPreviousYearActive()) {
await this.initTotalPreviousYear();
}
if (
this.query.isPreviousYearActive() &&
this.query.isDatePeriodsColumnsType()
) {
await this.initPeriodsPreviousYear();
}
};
/**
* Initialize the base currency.
*/
async initBaseCurrency() {
const metadata = await this.tenancyContext.getTenantMetadata();
this.baseCurrency = metadata.baseCurrency;
}
// ----------------------------
// # Accounts
// ----------------------------
/**
* Initialize accounts of the report.
*/
private initAccounts = async () => {
const accounts = await this.getAccounts();
// Inject to the repository.
this.accounts = accounts;
this.accountsByType = transformToMapBy(accounts, 'accountType');
};
/**
* Initialize accounts graph.
*/
private initAccountsGraph = async () => {
this.accountsGraph = this.accountModel.toDependencyGraph(this.accounts);
};
// ----------------------------
// # Closing Total.
// ----------------------------
/**
* Initialize accounts closing total based on the given query.
*/
private initAccountsTotalLedger = async (): Promise<void> => {
const totalByAccount = await this.accountsTotal(
this.query.fromDate,
this.query.toDate,
);
// Inject to the repository.
this.totalAccountsLedger = Ledger.fromTransactions(totalByAccount);
};
// ----------------------------
// # Date periods.
// ----------------------------
/**
* Initialize date periods total of accounts based on the given query.
*/
private initTotalDatePeriods = async (): Promise<void> => {
// Retrieves grouped transactions by given date group.
const periodsByAccount = await this.accountsDatePeriods(
this.query.query.fromDate,
this.query.query.toDate,
this.transactionsGroupType,
);
// Inject to the repository.
this.periodsAccountsLedger = Ledger.fromTransactions(periodsByAccount);
};
// ----------------------------
// # Previous Period (PP).
// ----------------------------
/**
* Initialize total of previous period (PP).
*/
private initTotalPreviousPeriod = async (): Promise<void> => {
const PPTotalsByAccounts = await this.accountsTotal(
this.query.PPFromDate,
this.query.PPToDate,
);
// Inject to the repository.
this.PPTotalAccountsLedger = Ledger.fromTransactions(PPTotalsByAccounts);
};
/**
* Initialize date periods of previous period (PP).
*/
private initPeriodsPreviousPeriod = async (): Promise<void> => {
// Retrieves grouped transactions by given date group.
const periodsByAccount = await this.accountsDatePeriods(
this.query.PPFromDate,
this.query.PPToDate,
this.transactionsGroupType,
);
// Inject to the repository.
this.PPPeriodsAccountsLedger = Ledger.fromTransactions(periodsByAccount);
};
// ----------------------------
// # Previous Year (PY).
// ----------------------------
/**
* Initialize total of previous year (PY).
*/
private initTotalPreviousYear = async (): Promise<void> => {
const PYTotalsByAccounts = await this.accountsTotal(
this.query.PYFromDate,
this.query.PYToDate,
);
// Inject to the repository.
this.PYTotalAccountsLedger = Ledger.fromTransactions(PYTotalsByAccounts);
};
/**
* Initialize periods of previous year (PY).
*/
private initPeriodsPreviousYear = async () => {
// Retrieves grouped transactions by given date group.
const periodsByAccount = await this.accountsDatePeriods(
this.query.PYFromDate,
this.query.PYToDate,
this.transactionsGroupType,
);
// Inject to the repository.
this.PYPeriodsAccountsLedger = Ledger.fromTransactions(periodsByAccount);
};
// ----------------------------
// # Utils
// ----------------------------
/**
* Retrieve the opening balance transactions of the report.
*/
public accountsTotal = async (fromDate: Date, toDate: Date) => {
return this.accountTransactionModel.query().onBuild((query) => {
query.sum('credit as credit');
query.sum('debit as debit');
query.groupBy('accountId');
query.select(['accountId']);
query.modify('filterDateRange', fromDate, toDate);
query.withGraphFetched('account');
this.commonFilterBranchesQuery(query);
});
};
/**
* Closing accounts date periods.
* @param openingDate
* @param datePeriodsType
* @returns
*/
public accountsDatePeriods = async (
fromDate: moment.MomentInput,
toDate: moment.MomentInput,
datePeriodsType,
) => {
return this.accountTransactionModel.query().onBuild((query) => {
query.sum('credit as credit');
query.sum('debit as debit');
query.groupBy('accountId');
query.select(['accountId']);
query.modify('groupByDateFormat', datePeriodsType);
query.modify('filterDateRange', fromDate, toDate);
query.withGraphFetched('account');
this.commonFilterBranchesQuery(query);
});
};
/**
* Common branches filter query.
* @param {Knex.QueryBuilder} query
*/
private commonFilterBranchesQuery = (query: Knex.QueryBuilder) => {
if (!isEmpty(this.query.query.branchesIds)) {
query.modify('filterByBranches', this.query.query.branchesIds);
}
};
/**
* Retrieve accounts of the report.
* @return {Promise<IAccount[]>}
*/
private getAccounts = () => {
return this.accountModel.query();
};
/**
*
* @param type
* @returns
*/
public getAccountsByType = (type: string[] | string) => {
return R.compose(
R.flatten,
R.map((accountType) =>
R.defaultTo([], this.accountsByType.get(accountType)),
),
castArray,
)(type);
};
}

View File

@@ -0,0 +1,67 @@
import {
IProfitLossSheetQuery,
IProfitLossSheetMeta,
IProfitLossSheetNode,
} from './ProfitLossSheet.types';
import ProfitLossSheet from './ProfitLossSheet';
import { mergeQueryWithDefaults } from './utils';
import { ProfitLossSheetRepository } from './ProfitLossSheetRepository';
import { ProfitLossSheetMeta } from './ProfitLossSheetMeta';
import { events } from '@/common/events/events';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Injectable } from '@nestjs/common';
import { I18nService } from 'nestjs-i18n';
@Injectable()
export class ProfitLossSheetService {
constructor(
private readonly profitLossSheetMeta: ProfitLossSheetMeta,
private readonly eventPublisher: EventEmitter2,
private readonly i18nService: I18nService,
private readonly profitLossRepository: ProfitLossSheetRepository,
) {}
/**
* Retrieve profit/loss sheet statement.
* @param {IProfitLossSheetQuery} query
* @return { }
*/
public profitLossSheet = async (
query: IProfitLossSheetQuery,
): Promise<{
data: IProfitLossSheetNode[];
query: IProfitLossSheetQuery;
meta: IProfitLossSheetMeta;
}> => {
// Merges the given query with default filter query.
const filter = mergeQueryWithDefaults(query);
// Loads the profit/loss sheet data.
this.profitLossRepository.setFilter(filter);
await this.profitLossRepository.asyncInitialize();
// Profit/Loss report instance.
const profitLossInstance = new ProfitLossSheet(
this.profitLossRepository,
filter,
this.i18nService,
);
// Profit/loss report data and columns.
const data = profitLossInstance.reportData();
// Retrieve the profit/loss sheet meta.
const meta = await this.profitLossSheetMeta.meta(filter);
// Triggers `onProfitLossSheetViewed` event.
await this.eventPublisher.emitAsync(
events.reports.onProfitLossSheetViewed,
{ query },
);
return {
query: filter,
data,
meta,
};
};
}

View File

@@ -0,0 +1,238 @@
// @ts-nocheck
import * as R from 'ramda';
import {
IProfitLossSheetQuery,
IProfitLossSheetAccountsNode,
ProfitLossNodeType,
ProfitLossSheetRowType,
IProfitLossSheetNode,
IProfitLossSheetEquationNode,
IProfitLossSheetAccountNode,
} from './ProfitLossSheet.types';
import {
ITableColumn,
ITableColumnAccessor,
ITableRow,
} from '../../types/Table.types';
import { ProfitLossSheetBase } from './ProfitLossSheetBase';
import { ProfitLossSheetTablePercentage } from './ProfitLossSheetTablePercentage';
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
import { ProfitLossTablePreviousPeriod } from './ProfitLossTablePreviousPeriod';
import { ProfitLossTablePreviousYear } from './ProfitLossTablePreviousYear';
import { ProfitLossSheetTableDatePeriods } from './ProfitLossSheetTableDatePeriods';
import { I18nService } from 'nestjs-i18n';
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
import { FinancialTable } from '../../common/FinancialTable';
import { tableRowMapper } from '../../utils/Table.utils';
export class ProfitLossSheetTable extends R.pipe(
ProfitLossTablePreviousPeriod,
ProfitLossTablePreviousYear,
ProfitLossSheetTablePercentage,
ProfitLossSheetTableDatePeriods,
ProfitLossSheetBase,
FinancialSheetStructure,
FinancialTable,
)(class {}) {
readonly query: ProfitLossSheetQuery;
readonly i18n: I18nService;
/**
* Constructor method.
* @param {} date
* @param {IProfitLossSheetQuery} query
*/
constructor(data: any, query: IProfitLossSheetQuery, i18n: I18nService) {
super();
this.query = new ProfitLossSheetQuery(query);
this.reportData = data;
this.i18n = i18n;
}
// ----------------------------------
// # Rows
// ----------------------------------
/**
* Retrieve the total column accessor.
* @return {ITableColumnAccessor[]}
*/
private totalColumnAccessor = (): ITableColumnAccessor[] => {
return R.pipe(
R.when(
this.query.isPreviousPeriodActive,
R.concat(this.previousPeriodColumnAccessor()),
),
R.when(
this.query.isPreviousYearActive,
R.concat(this.previousYearColumnAccessor()),
),
R.concat(this.percentageColumnsAccessor()),
R.concat([{ key: 'total', accessor: 'total.formattedAmount' }]),
)([]);
};
/**
* Common columns accessors.
* @returns {ITableColumnAccessor}
*/
private commonColumnsAccessors = (): ITableColumnAccessor[] => {
return R.compose(
R.concat([{ key: 'name', accessor: 'name' }]),
R.ifElse(
this.query.isDatePeriodsColumnsType,
R.concat(this.datePeriodsColumnsAccessors()),
R.concat(this.totalColumnAccessor()),
),
)([]);
};
/**
*
* @param {IProfitLossSheetAccountNode} node
* @returns {ITableRow}
*/
private accountNodeToTableRow = (
node: IProfitLossSheetAccountNode,
): ITableRow => {
const columns = this.commonColumnsAccessors();
const meta = {
rowTypes: [ProfitLossSheetRowType.ACCOUNT],
id: node.id,
};
return tableRowMapper(node, columns, meta);
};
/**
*
* @param {IProfitLossSheetAccountsNode} node
* @returns {ITableRow}
*/
private accountsNodeToTableRow = (
node: IProfitLossSheetAccountsNode,
): ITableRow => {
const columns = this.commonColumnsAccessors();
const meta = {
rowTypes: [ProfitLossSheetRowType.ACCOUNTS],
id: node.id,
};
return tableRowMapper(node, columns, meta);
};
/**
*
* @param {IProfitLossSheetEquationNode} node
* @returns {ITableRow}
*/
private equationNodeToTableRow = (
node: IProfitLossSheetEquationNode,
): ITableRow => {
const columns = this.commonColumnsAccessors();
const meta = {
rowTypes: [ProfitLossSheetRowType.TOTAL],
id: node.id,
};
return tableRowMapper(node, columns, meta);
};
/**
*
* @param {IProfitLossSheetNode} node
* @returns {ITableRow}
*/
private nodeToTableRowCompose = (node: IProfitLossSheetNode): ITableRow => {
return R.cond([
[
this.isNodeType(ProfitLossNodeType.ACCOUNTS),
this.accountsNodeToTableRow,
],
[
this.isNodeType(ProfitLossNodeType.EQUATION),
this.equationNodeToTableRow,
],
[this.isNodeType(ProfitLossNodeType.ACCOUNT), this.accountNodeToTableRow],
])(node);
};
/**
*
* @param {IProfitLossSheetNode[]} nodes
* @returns {ITableRow}
*/
private nodesToTableRowsCompose = (
nodes: IProfitLossSheetNode[],
): ITableRow[] => {
return this.mapNodesDeep(nodes, this.nodeToTableRowCompose);
};
/**
* Retrieves the table rows.
* @returns {ITableRow[]}
*/
public tableRows = (): ITableRow[] => {
return R.compose(
this.addTotalRows,
this.nodesToTableRowsCompose,
)(this.reportData);
};
// ----------------------------------
// # Columns.
// ----------------------------------
/**
* Retrieve total column children columns.
* @returns {ITableColumn[]}
*/
private tableColumnChildren = (): ITableColumn[] => {
return R.compose(
R.unless(
R.isEmpty,
R.concat([
{ key: 'total', label: this.i18n.t('profit_loss_sheet.total') },
]),
),
R.concat(this.percentageColumns()),
R.when(
this.query.isPreviousYearActive,
R.concat(this.getPreviousYearColumns()),
),
R.when(
this.query.isPreviousPeriodActive,
R.concat(this.getPreviousPeriodColumns()),
),
)([]);
};
/**
* Retrieves the total column.
* @returns {ITableColumn[]}
*/
private totalColumn = (): ITableColumn[] => {
return [
{
key: 'total',
label: this.i18n.t('profit_loss_sheet.total'),
children: this.tableColumnChildren(),
},
];
};
/**
* Retrieves the table columns.
* @returns {ITableColumn[]}
*/
public tableColumns = (): ITableColumn[] => {
return R.compose(
this.tableColumnsCellIndexing,
R.concat([
{ key: 'name', label: this.i18n.t('profit_loss_sheet.account_name') },
]),
R.ifElse(
this.query.isDatePeriodsColumnsType,
R.concat(this.datePeriodsColumns()),
R.concat(this.totalColumn()),
),
)([]);
};
}

View File

@@ -0,0 +1,158 @@
// @ts-nocheck
import * as R from 'ramda';
import moment from 'moment';
import { ITableColumn, ITableColumnAccessor } from '../../types/Table.types';
import { ProfitLossSheetTablePercentage } from './ProfitLossSheetTablePercentage';
import { ProfitLossTablePreviousPeriod } from './ProfitLossTablePreviousPeriod';
import { FinancialDatePeriods } from '../../common/FinancialDatePeriods';
import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from '../../common/FinancialSheet';
import { IDateRange } from '../../types/Report.types';
export const ProfitLossSheetTableDatePeriods = <
T extends GConstructor<FinancialSheet>,
>(
Base: T,
) =>
class extends R.pipe(
ProfitLossSheetTablePercentage,
ProfitLossTablePreviousPeriod,
FinancialDatePeriods,
)(Base) {
/**
* Retrieves the date periods based on the report query.
* @returns {IDateRange[]}
*/
get datePeriods() {
return this.getDateRanges(
this.query.query.fromDate,
this.query.query.toDate,
this.query.query.displayColumnsBy,
);
}
// --------------------------------
// # Accessors
// --------------------------------
/**
* Date period columns accessor.
* @param {IDateRange} dateRange -
* @param {number} index -
*/
private datePeriodColumnsAccessor = R.curry(
(dateRange: IDateRange, index: number) => {
return R.pipe(
R.when(
this.query.isPreviousPeriodActive,
R.concat(this.previousPeriodHorizontalColumnAccessors(index)),
),
R.when(
this.query.isPreviousYearActive,
R.concat(this.previousYearHorizontalColumnAccessors(index)),
),
R.concat(this.percetangeHorizontalColumnsAccessor(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
// --------------------------------
/**
* Retrieve the formatted column label from the given date range.
* @param {ICashFlowDateRange} dateRange -
* @return {string}
*/
private formatColumnLabel = (dateRange) => {
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);
};
/**
*
* @param {number} index
* @param {IDateRange} dateRange
* @returns {}
*/
private datePeriodChildrenColumns = (
index: number,
dateRange: IDateRange,
) => {
return R.compose(
R.unless(
R.isEmpty,
R.concat([
{ key: `total`, label: this.i18n.t('profit_loss_sheet.total') },
]),
),
R.concat(this.percentageColumns()),
R.when(
this.query.isPreviousYearActive,
R.concat(this.getPreviousYearDatePeriodColumnPlugin(dateRange)),
),
R.when(
this.query.isPreviousPeriodActive,
R.concat(this.getPreviousPeriodDatePeriodsPlugin(dateRange)),
),
)([]);
};
/**
*
* @param {IDateRange} dateRange
* @param {number} index
* @returns {ITableColumn}
*/
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

@@ -0,0 +1,39 @@
import { Injectable } from '@nestjs/common';
import { ProfitLossSheetService } from './ProfitLossSheetService';
import { ProfitLossSheetTable } from './ProfitLossSheetTable';
import {
IProfitLossSheetQuery,
IProfitLossSheetTable,
} from './ProfitLossSheet.types';
import { I18nService } from 'nestjs-i18n';
@Injectable()
export class ProfitLossSheetTableInjectable {
constructor(
private readonly i18n: I18nService,
private readonly profitLossSheet: ProfitLossSheetService,
) {}
/**
* Retrieves the profit/loss sheet in table format.
* @param {IProfitLossSheetQuery} filter - Profit/loss sheet query.
* @returns {Promise<IProfitLossSheetTable>}
*/
public async table(
filter: IProfitLossSheetQuery,
): Promise<IProfitLossSheetTable> {
const { data, query, meta } =
await this.profitLossSheet.profitLossSheet(filter);
const table = new ProfitLossSheetTable(data, query, this.i18n);
return {
table: {
rows: table.tableRows(),
columns: table.tableColumns(),
},
query,
meta,
};
}
}

View File

@@ -0,0 +1,141 @@
// @ts-nocheck
import * as R from 'ramda';
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
import { I18nService } from 'nestjs-i18n';
import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from '../../common/FinancialSheet';
import { ITableColumn, ITableColumnAccessor } from '../../types/Table.types';
export const ProfitLossSheetTablePercentage = <
T extends GConstructor<FinancialSheet>,
>(
Base: T,
) =>
class extends Base {
i18n: I18nService;
/**
* @param {ProfitLossSheetQuery}
*/
readonly query: ProfitLossSheetQuery;
// ----------------------------------
// # Columns.
// ----------------------------------
/**
* Retrieve percentage of column/row columns.
* @returns {ITableColumn[]}
*/
protected percentageColumns = (): ITableColumn[] => {
return R.pipe(
R.when(
this.query.isIncomePercentage,
R.append({
key: 'percentage_income',
label: this.i18n.t('profit_loss_sheet.percentage_of_income'),
}),
),
R.when(
this.query.isExpensesPercentage,
R.append({
key: 'percentage_expenses',
label: this.i18n.t('profit_loss_sheet.percentage_of_expenses'),
}),
),
R.when(
this.query.isColumnPercentage,
R.append({
key: 'percentage_column',
label: this.i18n.t('profit_loss_sheet.percentage_of_column'),
}),
),
R.when(
this.query.isRowPercentage,
R.append({
key: 'percentage_row',
label: this.i18n.t('profit_loss_sheet.percentage_of_row'),
}),
),
)([]);
};
// ----------------------------------
// # Accessors.
// ----------------------------------
/**
* Retrieves percentage of column/row accessors.
* @returns {ITableColumnAccessor[]}
*/
protected percentageColumnsAccessor = (): ITableColumnAccessor[] => {
return R.pipe(
R.when(
this.query.isIncomePercentage,
R.append({
key: 'percentage_income',
accessor: 'percentageIncome.formattedAmount',
}),
),
R.when(
this.query.isExpensesPercentage,
R.append({
key: 'percentage_expense',
accessor: 'percentageExpense.formattedAmount',
}),
),
R.when(
this.query.isColumnPercentage,
R.append({
key: 'percentage_column',
accessor: 'percentageColumn.formattedAmount',
}),
),
R.when(
this.query.isRowPercentage,
R.append({
key: 'percentage_row',
accessor: 'percentageRow.formattedAmount',
}),
),
)([]);
};
/**
* Retrieves percentage horizontal columns accessors.
* @param {number} index
* @returns {ITableColumn[]}
*/
protected percetangeHorizontalColumnsAccessor = (
index: number,
): ITableColumnAccessor[] => {
return R.pipe(
R.when(
this.query.isIncomePercentage,
R.append({
key: `percentage_income-${index}`,
accessor: `horizontalTotals[${index}].percentageIncome.formattedAmount`,
}),
),
R.when(
this.query.isExpensesPercentage,
R.append({
key: `percentage_expense-${index}`,
accessor: `horizontalTotals[${index}].percentageExpense.formattedAmount`,
}),
),
R.when(
this.query.isColumnPercentage,
R.append({
key: `percentage_of_column-${index}`,
accessor: `horizontalTotals[${index}].percentageColumn.formattedAmount`,
}),
),
R.when(
this.query.isRowPercentage,
R.append({
key: `percentage_of_row-${index}`,
accessor: `horizontalTotals[${index}].percentageRow.formattedAmount`,
}),
),
)([]);
};
};

View File

@@ -0,0 +1,29 @@
import { TableSheetPdf } from '../../common/TableSheetPdf';
import { IProfitLossSheetQuery } from './ProfitLossSheet.types';
import { ProfitLossSheetTableInjectable } from './ProfitLossSheetTableInjectable';
import { HtmlTableCustomCss } from './constants';
import { Injectable } from '@nestjs/common';
@Injectable()
export class ProfitLossTablePdfInjectable {
constructor(
private readonly profitLossTable: ProfitLossSheetTableInjectable,
private readonly tableSheetPdf: TableSheetPdf,
) {}
/**
* Retrieves the profit/loss sheet in pdf format.
* @param {number} query
* @returns {Promise<IBalanceSheetTable>}
*/
public async pdf(query: IProfitLossSheetQuery): Promise<Buffer> {
const table = await this.profitLossTable.table(query);
return this.tableSheetPdf.convertToPdf(
table.table,
table.meta.sheetName,
table.meta.formattedDateRange,
HtmlTableCustomCss,
);
}
}

View File

@@ -0,0 +1,101 @@
// @ts-nocheck
import * as R from 'ramda';
import { ITableColumn, ITableColumnAccessor } from '../../types/Table.types';
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
import { GConstructor } from '@/common/types/Constructor';
import { FinancialTablePreviousPeriod } from '../../common/FinancialTablePreviousPeriod';
import { FinancialSheet } from '../../common/FinancialSheet';
import { IDateRange } from '../../types/Report.types';
export const ProfitLossTablePreviousPeriod = <
T extends GConstructor<FinancialSheet>,
>(
Base: T,
) =>
class extends R.pipe(FinancialTablePreviousPeriod)(Base) {
query: ProfitLossSheetQuery;
// ----------------------------
// # Columns
// ----------------------------
/**
* Retrieves pervious period comparison columns.
* @returns {ITableColumn[]}
*/
protected getPreviousPeriodColumns = (
dateRange?: IDateRange,
): ITableColumn[] => {
return R.pipe(
// Previous period columns.
R.append(this.getPreviousPeriodTotalColumn(dateRange)),
R.when(
this.query.isPreviousPeriodChangeActive,
R.append(this.getPreviousPeriodChangeColumn()),
),
R.when(
this.query.isPreviousPeriodPercentageActive,
R.append(this.getPreviousPeriodPercentageColumn()),
),
)([]);
};
/**
* Compose the previous period for date periods columns.
* @params {IDateRange}
* @returns {ITableColumn[]}
*/
protected getPreviousPeriodDatePeriodsPlugin = (
dateRange: IDateRange,
): ITableColumn[] => {
const PPDateRange = this.getPPDatePeriodDateRange(
dateRange.fromDate,
dateRange.toDate,
this.query.displayColumnsBy,
);
return this.getPreviousPeriodColumns(PPDateRange);
};
// ----------------------------
// # Accessors
// ----------------------------
/**
* Retrieves previous period columns accessors.
* @returns {ITableColumn[]}
*/
protected previousPeriodColumnAccessor = (): ITableColumnAccessor[] => {
return R.pipe(
// Previous period columns.
R.append(this.getPreviousPeriodTotalAccessor()),
R.when(
this.query.isPreviousPeriodChangeActive,
R.append(this.getPreviousPeriodChangeAccessor()),
),
R.when(
this.query.isPreviousPeriodPercentageActive,
R.append(this.getPreviousPeriodPercentageAccessor()),
),
)([]);
};
/**
* Previous period period column accessor.
* @param {number} index
* @returns {ITableColumn[]}
*/
protected previousPeriodHorizontalColumnAccessors = (
index: number,
): ITableColumnAccessor[] => {
return R.pipe(
// Previous period columns.
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

@@ -0,0 +1,112 @@
// @ts-nocheck
import * as R from 'ramda';
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
import { GConstructor } from '@/common/types/Constructor';
import { FinancialSheet } from '../../common/FinancialSheet';
import { ITableColumn, ITableColumnAccessor } from '../../types/Table.types';
import { IDateRange } from '../CashFlowStatement/Cashflow.types';
import { FinancialTablePreviousYear } from '../../common/FinancialTablePreviousYear';
import { FinancialDateRanges } from '../../common/FinancialDateRanges';
export const ProfitLossTablePreviousYear = <
T extends GConstructor<FinancialSheet>,
>(
Base: T,
) =>
class extends R.pipe(FinancialTablePreviousYear, FinancialDateRanges)(Base) {
query: ProfitLossSheetQuery;
// ------------------------------------
// # Columns.
// ------------------------------------
/**
* Retrieves pervious year comparison columns.
* @returns {ITableColumn[]}
*/
protected getPreviousYearColumns = (
dateRange?: IDateRange,
): ITableColumn[] => {
return R.pipe(
// Previous year columns.
R.append(this.getPreviousYearTotalColumn(dateRange)),
R.when(
this.query.isPreviousYearChangeActive,
R.append(this.getPreviousYearChangeColumn()),
),
R.when(
this.query.isPreviousYearPercentageActive,
R.append(this.getPreviousYearPercentageColumn()),
),
)([]);
};
/**
* Compose the previous year for date periods columns.
* @param {IDateRange} dateRange
* @returns {ITableColumn[]}
*/
private previousYearDatePeriodColumnCompose = (
dateRange: IDateRange,
): ITableColumn[] => {
const PYDateRange = this.getPreviousYearDateRange(
dateRange.fromDate,
dateRange.toDate,
);
return this.getPreviousYearColumns(PYDateRange);
};
/**
* Retrieves previous year date periods columns.
* @param {IDateRange} dateRange
* @returns {ITableColumn[]}
*/
protected getPreviousYearDatePeriodColumnPlugin = (
dateRange: IDateRange,
): ITableColumn[] => {
return this.previousYearDatePeriodColumnCompose(dateRange);
};
// ---------------------------------------------------
// # Accessors.
// ---------------------------------------------------
/**
* Retrieves previous year columns accessors.
* @returns {ITableColumnAccessor[]}
*/
protected previousYearColumnAccessor = (): ITableColumnAccessor[] => {
return R.pipe(
// Previous year columns.
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[]}
*/
protected previousYearHorizontalColumnAccessors = (
index: number,
): ITableColumnAccessor[] => {
return R.pipe(
// Previous year columns.
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

@@ -0,0 +1,69 @@
import { ProfitLossNodeType } from './ProfitLossSheet.types';
export const MAP_CONFIG = { childrenPath: 'children', pathFormat: 'array' };
export const DISPLAY_COLUMNS_BY = {
DATE_PERIODS: 'date_periods',
TOTAL: 'total',
};
export enum IROW_TYPE {
AGGREGATE = 'AGGREGATE',
ACCOUNTS = 'ACCOUNTS',
ACCOUNT = 'ACCOUNT',
TOTAL = 'TOTAL',
}
export const TOTAL_NODE_TYPES = [
ProfitLossNodeType.ACCOUNTS,
ProfitLossNodeType.AGGREGATE,
ProfitLossNodeType.EQUATION,
];
export const HtmlTableCustomCss = `
table tr.row-type--total td {
font-weight: 600;
border-top: 1px solid #bbb;
color: #000;
}
table tr.row-id--net-income 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

@@ -0,0 +1,54 @@
import * as moment from 'moment';
import { merge } from 'lodash';
import { IProfitLossSheetQuery } from './ProfitLossSheet.types';
/**
* Default sheet filter query.
* @return {IBalanceSheetQuery}
*/
export const getDefaultPLQuery = (): IProfitLossSheetQuery => ({
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'),
numberFormat: {
divideOn1000: false,
negativeFormat: 'mines',
showZero: false,
formatMoney: 'total',
precision: 2,
},
basis: 'accrual',
noneZero: false,
noneTransactions: false,
displayColumnsType: 'total',
displayColumnsBy: 'month',
accountsIds: [],
percentageColumn: false,
percentageRow: false,
percentageIncome: false,
percentageExpense: false,
previousPeriod: false,
previousPeriodAmountChange: false,
previousPeriodPercentageChange: false,
previousYear: false,
previousYearAmountChange: false,
previousYearPercentageChange: false,
});
/**
*
* @param query
* @returns
*/
export const mergeQueryWithDefaults = (
query: IProfitLossSheetQuery,
): IProfitLossSheetQuery => {
return merge(getDefaultPLQuery(), query);
};