add server to monorepo.

This commit is contained in:
a.bouhuolia
2023-02-03 11:57:50 +02:00
parent 28e309981b
commit 80b97b5fdc
1303 changed files with 137049 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
import * as R from 'ramda';
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
import {
ProfitLossAggregateNodeId,
ProfitLossNodeType,
IProfitLossSchemaNode,
} from '@/interfaces';
import { FinancialSchema } from '../FinancialSchema';
export const ProfitLossShema = (Base) =>
class extends R.compose(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,324 @@
import * as R from 'ramda';
import { IProfitLossSheetQuery } from '@/interfaces/ProfitLossSheet';
import FinancialSheet from '../FinancialSheet';
import { FinancialSheetStructure } from '../FinancialSheetStructure';
import {
ProfitLossNodeType,
IProfitLossSheetEquationNode,
IProfitLossEquationSchemaNode,
IProfitLossSheetAccountsNode,
IProfitLossAccountsSchemaNode,
IProfitLossSchemaNode,
IProfitLossSheetNode,
IAccount,
IProfitLossSheetAccountNode,
} from '@/interfaces';
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 { FinancialEvaluateEquation } from '../FinancialEvaluateEquation';
import { ProfitLossSheetPreviousYear } from './ProfitLossSheetPreviousYear';
import { ProfitLossSheetPreviousPeriod } from './ProfitLossSheetPreviousPeriod';
import { FinancialDateRanges } from '../FinancialDateRanges';
import { ProfitLossSheetFilter } from './ProfitLossSheetFilter';
export default class ProfitLossSheet extends R.compose(
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;
/**
* Constructor method.
* @param {IProfitLossSheetQuery} query -
* @param {IAccount[]} accounts -
* @param {IJournalPoster} transactionsJournal -
*/
constructor(
repository: ProfitLossSheetRepository,
query: IProfitLossSheetQuery,
baseCurrency: string,
i18n: any
) {
super();
this.query = new ProfitLossSheetQuery(query);
this.repository = repository;
this.numberFormat = this.query.query.numberFormat;
this.baseCurrency = baseCurrency;
this.i18n = i18n;
}
/**
* Retrieve the sheet account node from the given account.
* @param {IAccount} account
* @returns {IProfitLossSheetAccountNode}
*/
private accountNodeMapper = (
account: IAccount
): IProfitLossSheetAccountNode => {
const total = this.repository.totalAccountsLedger
.whereAccountId(account.id)
.getClosingBalance();
return {
id: account.id,
name: account.name,
nodeType: ProfitLossNodeType.ACCOUNT,
total: this.getAmountMeta(total),
};
};
/**
* Compose account node.
* @param {IAccount} node
* @returns {IProfitLossSheetAccountNode}
*/
private accountNodeCompose = (
account: IAccount
): 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);
};
/**
* Retrieve report accounts nodes by the given accounts types.
* @param {string[]} types
* @returns {IBalanceSheetAccountNode}
*/
private getAccountsNodesByTypes = (
types: string[]
): IProfitLossSheetAccountNode[] => {
return R.compose(
R.map(this.accountNodeCompose),
R.flatten,
R.map(this.repository.getAccountsByType)
)(types);
};
/**
* 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.__(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.__(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 = (): IProfitLossSheetNode => {
const schema = this.getSchema();
return R.compose(
this.reportFilterPlugin,
this.reportRowsPercentageCompose,
this.reportColumnsPerentageCompose,
this.reportSchemaEquationNodesCompose,
this.reportSchemaAccountsNodesCompose
)(schema);
};
}

View File

@@ -0,0 +1,30 @@
import * as R from 'ramda';
import { TOTAL_NODE_TYPES } from './constants';
export const ProfitLossSheetBase = (Base) =>
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);
});
isNodeTotal = (node) => {
return this.isNodeTypeIn(TOTAL_NODE_TYPES, node);
}
};

View File

@@ -0,0 +1,236 @@
import * as R from 'ramda';
import { sumBy } from 'lodash';
import { FinancialDatePeriods } from '../FinancialDatePeriods';
import {
IDateRange,
IProfitLossHorizontalDatePeriodNode,
IProfitLossSheetAccountNode,
IProfitLossSheetAccountsNode,
IProfitLossSheetCommonNode,
IProfitLossSheetNode,
} from '@/interfaces';
export const ProfitLossSheetDatePeriods = (Base) =>
class extends R.compose(FinancialDatePeriods)(Base) {
/**
* 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,170 @@
import * as R from 'ramda';
import { get } from 'lodash';
import { IProfitLossSheetNode, ProfitLossNodeType } from '@/interfaces';
import { FinancialFilter } from '../FinancialFilter';
import { ProfitLossSheetBase } from './ProfitLossSheetBase';
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
export const ProfitLossSheetFilter = (Base) =>
class extends R.compose(FinancialFilter, ProfitLossSheetBase)(Base) {
query: ProfitLossSheetQuery;
// ----------------
// # 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);
};
/**
* Detarmines 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.
// ----------------
/**
* Detearmines aggregate none-children filtering.
* @param {IProfitLossSheetNode} node
* @returns {boolean}
*/
private aggregateNoneChildrenFilterDetarminer = (
node: IProfitLossSheetNode
): boolean => {
const schemaNode = this.getSchemaNodeById(node.id);
// Detarmines whether the given node is aggregate node.
const isAggregateNode = this.isNodeType(
ProfitLossNodeType.ACCOUNTS,
node
);
// Detarmines 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,301 @@
import * as R from 'ramda';
import {
IProfitLossSheetNode,
IProfitLossSheetTotal,
ProfitLossAggregateNodeId,
} from '@/interfaces';
import { FinancialHorizTotals } from '../FinancialHorizTotals';
export const ProfitLossSheetPercentage = (Base) =>
class extends R.compose(FinancialHorizTotals)(Base) {
/**
* 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,395 @@
import * as R from 'ramda';
import { sumBy } from 'lodash';
import {
IProfitLossHorizontalDatePeriodNode,
IProfitLossSchemaNode,
IProfitLossSheetAccountNode,
IProfitLossSheetAccountsNode,
IProfitLossSheetEquationNode,
IProfitLossSheetNode,
} from '@/interfaces';
import { FinancialPreviousPeriod } from '../FinancialPreviousPeriod';
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
export const ProfitLossSheetPreviousPeriod = (Base) =>
class extends R.compose(FinancialPreviousPeriod)(Base) {
query: ProfitLossSheetQuery;
// ---------------------------
// # 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,367 @@
import * as R from 'ramda';
import { sumBy } from 'lodash';
import { compose } from 'lodash/fp';
import {
IProfitLossSheetEquationNode,
IProfitLossSheetAccountNode,
IProfitLossSchemaNode,
IProfitLossSheetNode,
IProfitLossSheetTotal,
} from '@/interfaces';
import { ProfitLossSheetRepository } from './ProfitLossSheetRepository';
import { FinancialPreviousYear } from '../FinancialPreviousYear';
export const ProfitLossSheetPreviousYear = (Base) =>
class extends compose(FinancialPreviousYear)(Base) {
repository: ProfitLossSheetRepository;
// ---------------------------
// # 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,209 @@
import { merge } from 'lodash';
import * as R from 'ramda';
import { IProfitLossSheetQuery, IFinancialDatePeriodsUnit } from '@/interfaces';
import { DISPLAY_COLUMNS_BY } from './constants';
import { FinancialDateRanges } from '../FinancialDateRanges';
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);
}
/**
* Detarmines the given display columns type.
* @param {string} displayColumnsBy
* @returns {boolean}
*/
public isDisplayColumnsBy = (displayColumnsBy: string): boolean => {
return this.query.displayColumnsBy === displayColumnsBy;
};
/**
* Detarmines the given display columns by type.
* @param {string} displayColumnsBy
* @returns {boolean}
*/
public isDisplayColumnsType = (displayColumnsType: string): boolean => {
return this.query.displayColumnsType === displayColumnsType;
};
/**
* Detarmines whether the columns type is date periods.
* @returns {boolean}
*/
public isDatePeriodsColumnsType = (): boolean => {
return this.isDisplayColumnsType(DISPLAY_COLUMNS_BY.DATE_PERIODS);
};
/**
* Detarmines whether the columns type is total.
* @returns {boolean}
*/
public isTotalColumnType = (): boolean => {
return this.isDisplayColumnsType(DISPLAY_COLUMNS_BY.TOTAL);
};
// --------------------------------------
// # Previous Year (PY)
// --------------------------------------
/**
* Detarmines the report query has previous year enabled.
* @returns {boolean}
*/
public isPreviousYearActive = (): boolean => {
return this.query.previousYear;
};
/**
* Detarmines the report query has previous year percentage change active.
* @returns {boolean}
*/
public isPreviousYearPercentageActive = (): boolean => {
return this.query.previousYearPercentageChange;
};
/**
* Detarmines the report query has previous year change active.
* @returns {boolean}
*/
public isPreviousYearChangeActive = (): boolean => {
return this.query.previousYearAmountChange;
};
/**
* Retrieves PY date based on the current query.
* @returns {Date}
*/
public getTotalPreviousYear = (): Date => {
return this.PYFromDate;
};
// --------------------------------------
// # Previous Period (PP)
// --------------------------------------
/**
* Detarmines the report query has previous period enabled.
* @returns {boolean}
*/
public isPreviousPeriodActive = (): boolean => {
return this.query.previousPeriod;
};
/**
* Detarmines the report query has previous period percentage change active.
* @returns {boolean}
*/
public isPreviousPeriodPercentageActive = (): boolean => {
return this.query.previousPeriodPercentageChange;
};
/**
* Detarmines 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.
// --------------------------------------
/**
* Detarmines whether percentage of expenses is active.
* @returns {boolean}
*/
public isExpensesPercentage = (): boolean => {
return this.query.percentageExpense;
};
/**
* Detarmines whether percentage of income is active.
* @returns {boolean}
*/
public isIncomePercentage = (): boolean => {
return this.query.percentageIncome;
};
/**
* Detarmines whether percentage of column is active.
* @returns {boolean}
*/
public isColumnPercentage = (): boolean => {
return this.query.percentageColumn;
};
/**
* Detarmines whether percentage of row is active.
* @returns {boolean}
*/
public isRowPercentage = (): boolean => {
return this.query.percentageRow;
};
}

View File

@@ -0,0 +1,343 @@
import { defaultTo } from 'lodash';
import * as R from 'ramda';
import { Knex } from 'knex';
import { isEmpty } from 'lodash';
import { transformToMapBy } from 'utils';
import {
IProfitLossSheetQuery,
IAccount,
IAccountTransactionsGroupBy,
} from '@/interfaces';
import Ledger from '@/services/Accounting/Ledger';
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
import { FinancialDatePeriods } from '../FinancialDatePeriods';
export class ProfitLossSheetRepository extends R.compose(FinancialDatePeriods)(
class {}
) {
/**
*
*/
public models: any;
/**
*
*/
public accountsByType: any;
/**
* @param {}
*/
public accounts: IAccount[];
/**
* 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;
/**
* Constructor method.
* @param {number} tenantId
* @param {IBalanceSheetQuery} query
*/
constructor(models: any, query: IProfitLossSheetQuery) {
super();
this.models = models;
this.query = new ProfitLossSheetQuery(query);
this.transactionsGroupType = this.getGroupByFromDisplayColumnsBy(
this.query.displayColumnsBy
);
}
/**
* Async report repository.
*/
public asyncInitialize = async () => {
await this.initAccounts();
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();
}
};
// ----------------------------
// # 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');
};
// ----------------------------
// # 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.fromDate,
this.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) => {
const { AccountTransaction } = this.models;
return AccountTransaction.query().onBuild((query) => {
query.sum('credit as credit');
query.sum('debit as debit');
query.groupBy('accountId');
query.select(['accountId']);
query.modify('filterDateRange', fromDate, toDate);
query.withGraphFetched('account');
this.commonFilterBranchesQuery(query);
});
};
/**
* Closing accounts date periods.
* @param openingDate
* @param datePeriodsType
* @returns
*/
public accountsDatePeriods = async (
fromDate: Date,
toDate: Date,
datePeriodsType
) => {
const { AccountTransaction } = this.models;
return AccountTransaction.query().onBuild((query) => {
query.sum('credit as credit');
query.sum('debit as debit');
query.groupBy('accountId');
query.select(['accountId']);
query.modify('groupByDateFormat', datePeriodsType);
query.modify('filterDateRange', fromDate, toDate);
query.withGraphFetched('account');
this.commonFilterBranchesQuery(query);
});
};
/**
* Common branches filter query.
* @param {Knex.QueryBuilder} query
*/
private commonFilterBranchesQuery = (query: Knex.QueryBuilder) => {
if (!isEmpty(this.query.branchesIds)) {
query.modify('filterByBranches', this.query.branchesIds);
}
};
/**
* Retrieve accounts of the report.
* @return {Promise<IAccount[]>}
*/
private getAccounts = () => {
const { Account } = this.models;
return Account.query();
};
public getAccountsByType = (type: string) => {
return defaultTo(this.accountsByType.get(type), []);
};
}

View File

@@ -0,0 +1,104 @@
import { Service, Inject } from 'typedi';
import {
IProfitLossSheetQuery,
IProfitLossSheetMeta,
IProfitLossSheetNode,
} from '@/interfaces';
import ProfitLossSheet from './ProfitLossSheet';
import TenancyService from '@/services/Tenancy/TenancyService';
import InventoryService from '@/services/Inventory/Inventory';
import { parseBoolean } from 'utils';
import { Tenant } from '@/system/models';
import { mergeQueryWithDefaults } from './utils';
import { ProfitLossSheetRepository } from './ProfitLossSheetRepository';
// Profit/Loss sheet service.
@Service()
export default class ProfitLossSheetService {
@Inject()
tenancy: TenancyService;
@Inject('logger')
logger: any;
@Inject()
inventoryService: InventoryService;
/**
* Retrieve the trial balance sheet meta.
* @param {number} tenantId - Tenant id.
* @returns {ITrialBalanceSheetMeta}
*/
reportMetadata(tenantId: number): IProfitLossSheetMeta {
const settings = this.tenancy.settings(tenantId);
const isCostComputeRunning =
this.inventoryService.isItemsCostComputeRunning(tenantId);
const organizationName = settings.get({
group: 'organization',
key: 'name',
});
const baseCurrency = settings.get({
group: 'organization',
key: 'base_currency',
});
return {
isCostComputeRunning: parseBoolean(isCostComputeRunning, false),
organizationName,
baseCurrency,
};
}
/**
* Retrieve profit/loss sheet statement.
* @param {number} tenantId
* @param {IProfitLossSheetQuery} query
* @return { }
*/
profitLossSheet = async (
tenantId: number,
query: IProfitLossSheetQuery
): Promise<{
data: IProfitLossSheetNode[];
query: IProfitLossSheetQuery;
meta: IProfitLossSheetMeta;
}> => {
const models = this.tenancy.models(tenantId);
const i18n = this.tenancy.i18n(tenantId);
// Merges the given query with default filter query.
const filter = mergeQueryWithDefaults(query);
// Get the given accounts or throw not found service error.
// if (filter.accountsIds.length > 0) {
// await this.accountsService.getAccountsOrThrowError(
// tenantId,
// filter.accountsIds
// );
// }
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
const profitLossRepo = new ProfitLossSheetRepository(models, filter);
await profitLossRepo.asyncInitialize();
// Profit/Loss report instance.
const profitLossInstance = new ProfitLossSheet(
profitLossRepo,
filter,
tenant.metadata.baseCurrency,
i18n
);
// Profit/loss report data and collumns.
const profitLossData = profitLossInstance.reportData();
return {
data: profitLossData,
query: filter,
meta: this.reportMetadata(tenantId),
};
};
}

View File

@@ -0,0 +1,233 @@
import * as R from 'ramda';
import {
IProfitLossSheetQuery,
ITableColumn,
IProfitLossSheetAccountsNode,
ITableColumnAccessor,
ITableRow,
ProfitLossNodeType,
ProfitLossSheetRowType,
IProfitLossSheetNode,
IProfitLossSheetEquationNode,
IProfitLossSheetAccountNode,
} from '@/interfaces';
import { tableRowMapper } from 'utils';
import { FinancialTable } from '../FinancialTable';
import { ProfitLossSheetBase } from './ProfitLossSheetBase';
import { ProfitLossSheetTablePercentage } from './ProfitLossSheetTablePercentage';
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
import { ProfitLossTablePreviousPeriod } from './ProfitLossTablePreviousPeriod';
import { ProfitLossTablePreviousYear } from './ProfitLossTablePreviousYear';
import { FinancialSheetStructure } from '../FinancialSheetStructure';
import { ProfitLossSheetTableDatePeriods } from './ProfitLossSheetTableDatePeriods';
export class ProfitLossSheetTable extends R.compose(
ProfitLossTablePreviousPeriod,
ProfitLossTablePreviousYear,
ProfitLossSheetTablePercentage,
ProfitLossSheetTableDatePeriods,
ProfitLossSheetBase,
FinancialSheetStructure,
FinancialTable
)(class {}) {
readonly query: ProfitLossSheetQuery;
/**
* Constructor method.
* @param {} date
* @param {IProfitLossSheetQuery} query
*/
constructor(data: any, query: IProfitLossSheetQuery, i18n: any) {
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.__('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.__('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.__('profit_loss_sheet.account_name') },
]),
R.ifElse(
this.query.isDatePeriodsColumnsType,
R.concat(this.datePeriodsColumns()),
R.concat(this.totalColumn())
)
)([]);
};
}

View File

@@ -0,0 +1,150 @@
import * as R from 'ramda';
import moment from 'moment';
import { ITableColumn, IDateRange, ITableColumnAccessor } from '@/interfaces';
import { FinancialDatePeriods } from '../FinancialDatePeriods';
import { ProfitLossSheetTablePercentage } from './ProfitLossSheetTablePercentage';
import { ProfitLossTablePreviousPeriod } from './ProfitLossTablePreviousPeriod';
export const ProfitLossSheetTableDatePeriods = (Base) =>
class extends R.compose(
ProfitLossSheetTablePercentage,
ProfitLossTablePreviousPeriod,
FinancialDatePeriods
)(Base) {
/**
* Retrieves the date periods based on the report query.
* @returns {IDateRange[]}
*/
get datePeriods() {
return this.getDateRanges(
this.query.fromDate,
this.query.toDate,
this.query.displayColumnsBy
);
}
// --------------------------------
// # 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.__('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,131 @@
import * as R from 'ramda';
import { ITableColumn, ITableColumnAccessor } from '@/interfaces';
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
export const ProfitLossSheetTablePercentage = (Base) =>
class extends Base {
/**
* @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.__('profit_loss_sheet.percentage_of_income'),
})
),
R.when(
this.query.isExpensesPercentage,
R.append({
key: 'percentage_expenses',
label: this.i18n.__('profit_loss_sheet.percentage_of_expenses'),
})
),
R.when(
this.query.isColumnPercentage,
R.append({
key: 'percentage_column',
label: this.i18n.__('profit_loss_sheet.percentage_of_column'),
})
),
R.when(
this.query.isRowPercentage,
R.append({
key: 'percentage_row',
label: this.i18n.__('profit_loss_sheet.percentage_of_row'),
})
)
)([]);
};
// ----------------------------------
// # Accessors.
// ----------------------------------
/**
* Retrieves percentage of column/row accessors.
* @returns {ITableColumn[]}
*/
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,93 @@
import * as R from 'ramda';
import { IDateRange, ITableColumn, ITableColumnAccessor } from '@/interfaces';
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
import { FinancialTablePreviousPeriod } from '../FinancialTablePreviousPeriod';
export const ProfitLossTablePreviousPeriod = (Base) =>
class extends R.compose(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,107 @@
import * as R from 'ramda';
import { IDateRange, ITableColumn, ITableColumnAccessor } from '@/interfaces';
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
import { FinancialTablePreviousYear } from '../FinancialTablePreviousYear';
import { FinancialDateRanges } from '../FinancialDateRanges';
export const ProfitLossTablePreviousYear = (Base) =>
class extends R.compose(
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())
)
)([]);
};
/**
*
* @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,21 @@
import { ProfitLossNodeType } from '@/interfaces';
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
];

View File

@@ -0,0 +1,54 @@
import moment from 'moment';
import { merge } from 'lodash';
import { IProfitLossSheetQuery } from '@/interfaces';
/**
* Default sheet filter query.
* @return {IBalanceSheetQuery}
*/
export const getDefaultPLQuery = (): IProfitLossSheetQuery => ({
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
toDate: moment().endOf('year').format('YYYY-MM-DD'),
numberFormat: {
divideOn1000: false,
negativeFormat: 'mines',
showZero: false,
formatMoney: 'total',
precision: 2,
},
basis: 'accural',
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);
};