feat: Inventory item details report.

feat: Cash flow statement report.
This commit is contained in:
a.bouhuolia
2021-05-31 13:17:02 +02:00
parent 256d915f06
commit d47633b8ea
80 changed files with 5474 additions and 376 deletions

View File

@@ -16,13 +16,13 @@ import FinancialSheet from '../FinancialSheet';
export default class BalanceSheetStatement extends FinancialSheet {
readonly query: IBalanceSheetQuery;
readonly numberFormat: INumberFormatQuery;
readonly tenantId: number;
readonly accounts: IAccount & { type: IAccountType }[];
readonly journalFinancial: IJournalPoster;
readonly comparatorDateType: string;
readonly dateRangeSet: string[];
readonly baseCurrency: string;
readonly numberFormat: INumberFormatQuery;
/**
* Constructor method.
@@ -46,7 +46,6 @@ export default class BalanceSheetStatement extends FinancialSheet {
this.accounts = accounts;
this.journalFinancial = journalFinancial;
this.baseCurrency = baseCurrency;
this.comparatorDateType =
query.displayColumnsType === 'total' ? 'day' : query.displayColumnsBy;

View File

@@ -53,7 +53,7 @@ export default class BalanceSheetStatementService
* @param {number} tenantId -
* @returns {IBalanceSheetMeta}
*/
reportMetadata(tenantId: number): IBalanceSheetMeta {
private reportMetadata(tenantId: number): IBalanceSheetMeta {
const settings = this.tenancy.settings(tenantId);
const isCostComputeRunning = this.inventoryService
@@ -113,7 +113,7 @@ export default class BalanceSheetStatementService
// Retrieve all journal transactions based on the given query.
const transactions = await transactionsRepository.journal({
fromDate: query.fromDate,
toDate: query.toDate,
toDate: query.toDate,
});
// Transform transactions to journal collection.
const transactionsJournal = Journal.fromTransactions(

View File

@@ -0,0 +1,761 @@
import * as R from 'ramda';
import { defaultTo, map, set, sumBy, isEmpty, mapValues, get } from 'lodash';
import * as mathjs from 'mathjs';
import moment from 'moment';
import {
IAccount,
ILedger,
INumberFormatQuery,
ICashFlowSchemaSection,
ICashFlowStatementQuery,
ICashFlowStatementNetIncomeSection,
ICashFlowStatementAccountSection,
ICashFlowSchemaSectionAccounts,
ICashFlowStatementAccountMeta,
ICashFlowSchemaAccountRelation,
ICashFlowStatementSectionType,
ICashFlowStatementData,
ICashFlowDatePeriod,
ICashFlowStatement,
ICashFlowSchemaTotalSection,
ICashFlowStatementTotalSection,
ICashFlowStatementSection,
} from 'interfaces';
import CASH_FLOW_SCHEMA from './schema';
import FinancialSheet from '../FinancialSheet';
import {
transformToMapBy,
accumSum,
dateRangeFromToCollection,
applyMixins,
} from 'utils';
import {
reduceDeep,
iteratee,
mapValuesDeep,
filterDeep,
} from 'utils/deepdash';
import { ACCOUNT_ROOT_TYPE } from 'data/AccountTypes';
import CashFlowDatePeriods from './CashFlowDatePeriods';
const MAP_CONFIG = { childrenPath: 'children', pathFormat: 'array' };
const DISPLAY_COLUMNS_BY = {
DATE_PERIODS: 'date_periods',
TOTAL: 'total',
};
class CashFlowStatement extends FinancialSheet implements ICashFlowStatement {
readonly baseCurrency: string;
readonly sectionsByIds = {};
readonly cashFlowSchemaMap: Map<string, ICashFlowSchemaSection>;
readonly cashFlowSchemaSeq: Array<string>;
readonly accountByTypeMap: Map<string, IAccount[]>;
readonly accountsByRootType: Map<string, IAccount[]>;
readonly ledger: ILedger;
readonly cashLedger: ILedger;
readonly netIncomeLedger: ILedger;
readonly schemaSectionParserIteratee: any;
readonly query: ICashFlowStatementQuery;
readonly numberFormat: INumberFormatQuery;
readonly comparatorDateType: string;
readonly dateRangeSet: { fromDate: Date; toDate: Date }[];
/**
* Constructor method.
* @constructor
*/
constructor(
accounts: IAccount[],
ledger: ILedger,
cashLedger: ILedger,
netIncomeLedger: ILedger,
query: ICashFlowStatementQuery,
baseCurrency: string
) {
super();
this.baseCurrency = baseCurrency;
this.ledger = ledger;
this.cashLedger = cashLedger;
this.netIncomeLedger = netIncomeLedger;
this.accountByTypeMap = transformToMapBy(accounts, 'accountType');
this.accountsByRootType = transformToMapBy(accounts, 'accountRootType');
this.schemaSectionParserIteratee = iteratee(this.schemaSectionParser);
this.query = query;
this.numberFormat = this.query.numberFormat;
this.dateRangeSet = [];
this.comparatorDateType =
query.displayColumnsType === 'total' ? 'day' : query.displayColumnsBy;
this.initDateRangeCollection();
}
// --------------------------------------------
// # GENERAL UTILITIES
// --------------------------------------------
/**
* Retrieve the expense accounts ids.
* @return {number[]}
*/
private getAccountsIdsByType(accountType: string): number[] {
const expenseAccounts = this.accountsByRootType.get(accountType);
const expenseAccountsIds = map(expenseAccounts, 'id');
return expenseAccountsIds;
}
/**
* Detarmines the given display columns by type.
* @param {string} displayColumnsBy
* @returns {boolean}
*/
private isDisplayColumnsBy(displayColumnsBy: string): boolean {
return this.query.displayColumnsType === displayColumnsBy;
}
/**
* Adjustments the given amount.
* @param {string} direction
* @param {number} amount -
* @return {number}
*/
private amountAdjustment(direction: 'mines' | 'plus', amount): number {
return R.when(
R.always(R.equals(direction, 'mines')),
R.multiply(-1)
)(amount);
}
// --------------------------------------------
// # NET INCOME NODE
// --------------------------------------------
/**
* Retrieve the accounts net income.
* @returns {number} - Amount of net income.
*/
private getAccountsNetIncome(): number {
// Mapping income/expense accounts ids.
const incomeAccountsIds = this.getAccountsIdsByType(
ACCOUNT_ROOT_TYPE.INCOME
);
const expenseAccountsIds = this.getAccountsIdsByType(
ACCOUNT_ROOT_TYPE.EXPENSE
);
// Income closing balance.
const incomeClosingBalance = accumSum(incomeAccountsIds, (id) =>
this.netIncomeLedger.whereAccountId(id).getClosingBalance()
);
// Expense closing balance.
const expenseClosingBalance = accumSum(expenseAccountsIds, (id) =>
this.netIncomeLedger.whereAccountId(id).getClosingBalance()
);
// Net income = income - expenses.
const netIncome = incomeClosingBalance - expenseClosingBalance;
return netIncome;
}
/**
* Parses the net income section from the given section schema.
* @param {ICashFlowSchemaSection} sectionSchema - Report section schema.
* @returns {ICashFlowStatementNetIncomeSection}
*/
private netIncomeSectionMapper(
sectionSchema: ICashFlowSchemaSection
): ICashFlowStatementNetIncomeSection {
const netIncome = this.getAccountsNetIncome();
return R.compose(
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocPeriodsToNetIncomeNode.bind(this)
)
)({
id: sectionSchema.id,
label: sectionSchema.label,
total: this.getAmountMeta(netIncome),
sectionType: ICashFlowStatementSectionType.NET_INCOME,
});
}
// --------------------------------------------
// # ACCOUNT NODE
// --------------------------------------------
/**
* Retrieve account meta.
* @param {ICashFlowSchemaAccountRelation} relation - Account relation.
* @param {IAccount} account -
* @returns {ICashFlowStatementAccountMeta}
*/
private accountMetaMapper(
relation: ICashFlowSchemaAccountRelation,
account: IAccount
): ICashFlowStatementAccountMeta {
// Retrieve the closing balance of the given account.
const getClosingBalance = (id) =>
this.ledger.whereAccountId(id).getClosingBalance();
const closingBalance = R.compose(
// Multiplies the amount by -1 in case the relation in mines.
R.curry(this.amountAdjustment)(relation.direction)
)(getClosingBalance(account.id));
return R.compose(
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocPeriodsToAccountNode.bind(this)
)
)({
id: account.id,
code: account.code,
label: account.name,
accountType: account.accountType,
adjusmentType: relation.direction,
total: this.getAmountMeta(closingBalance),
sectionType: ICashFlowStatementSectionType.ACCOUNT,
});
}
/**
* 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.bind(this))(
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.bind(this))(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 total = this.getAccountsMetaTotal(accounts);
return R.compose(
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocPeriodsToAggregateNode.bind(this)
)
)({
sectionType: ICashFlowStatementSectionType.ACCOUNTS,
id: sectionSchema.id,
label: sectionSchema.label,
footerLabel: sectionSchema.footerLabel,
children: accounts,
total: this.getTotalAmountMeta(total),
});
}
/**
* Detarmines the schema section type.
* @param {string} type
* @param {ICashFlowSchemaSection} section
* @returns {boolean}
*/
private isSchemaSectionType(
type: string,
section: ICashFlowSchemaSection
): boolean {
return type === section.sectionType;
}
// --------------------------------------------
// # AGGREGATE NODE
// --------------------------------------------
/**
*
* @param {ICashFlowSchemaSection} schemaSection
* @returns
*/
private regularSectionParser(
schemaSection: ICashFlowSchemaSection
): ICashFlowStatementSection {
return {
id: schemaSection.id,
label: schemaSection.label,
footerLabel: schemaSection.footerLabel,
sectionType: ICashFlowStatementSectionType.REGULAR,
};
}
private transformSectionsToMap(sections: ICashFlowSchemaSection[]) {
return reduceDeep(
sections,
(acc, section) => {
if (section.id) {
acc[`${section.id}`] = section;
}
return acc;
},
{},
MAP_CONFIG
);
}
// --------------------------------------------
// # 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[]} accumlatedSections
*/
private totalEquationSectionParser(
accumlatedSections: ICashFlowSchemaSection[],
sectionSchema: ICashFlowSchemaTotalSection
): ICashFlowStatementTotalSection {
const mappedSectionsById = this.transformSectionsToMap(accumlatedSections);
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.bind(this))(
mappedSectionsById,
sectionSchema.equation
)
)
)({
sectionType: ICashFlowStatementSectionType.TOTAL,
id: sectionSchema.id,
label: sectionSchema.label,
total: this.getTotalAmountMeta(total),
});
}
/**
* Retrieve the beginning cash from date.
* @param {Date|string} fromDate -
* @return {Date}
*/
private beginningCashFrom(fromDate: string | Date): Date {
return moment(fromDate).subtract(1, 'days').toDate();
}
/**
* Retrieve account meta.
* @param {ICashFlowSchemaAccountRelation} relation
* @param {IAccount} account
* @returns {ICashFlowStatementAccountMeta}
*/
private cashAccountMetaMapper(
relation: ICashFlowSchemaAccountRelation,
account: IAccount
): ICashFlowStatementAccountMeta {
const cashToDate = this.beginningCashFrom(this.query.fromDate);
const closingBalance = this.cashLedger
.whereToDate(cashToDate)
.whereAccountId(account.id)
.getClosingBalance();
return R.compose(
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocCashAtBeginningAccountDatePeriods.bind(this)
)
)({
id: account.id,
code: account.code,
label: account.name,
accountType: account.accountType,
adjusmentType: relation.direction,
total: this.getAmountMeta(closingBalance),
sectionType: ICashFlowStatementSectionType.ACCOUNT,
});
}
/**
* 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.bind(this))(
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.bind(this))(relations)
);
}
/**
* Parses the cash at beginning section.
* @param {ICashFlowSchemaTotalSection} sectionSchema -
* @return {}
*/
private cashAtBeginningSectionParser(sectionSchema) {
const { accountsRelations } = sectionSchema;
const children = this.getCashAccountsBySchemaRelations(accountsRelations);
const total = this.getAccountsMetaTotal(children);
return R.compose(
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocCashAtBeginningDatePeriods.bind(this)
)
)({
sectionType: ICashFlowStatementSectionType.CASH_AT_BEGINNING,
id: sectionSchema.id,
label: sectionSchema.label,
children,
total: this.getTotalAmountMeta(total),
});
}
/**
* Parses the schema section.
* @param {ICashFlowSchemaSection} section
* @returns {ICashFlowSchemaSection}
*/
private schemaSectionParser(
section: ICashFlowSchemaSection,
key: number,
parentValue: ICashFlowSchemaSection[],
context,
accumlatedSections: ICashFlowSchemaSection[]
): ICashFlowSchemaSection {
const isSchemaSectionType = R.curry(this.isSchemaSectionType);
return R.compose(
// Accounts node.
R.when(
isSchemaSectionType(ICashFlowStatementSectionType.ACCOUNTS),
this.accountsSectionParser.bind(this)
),
// Net income node.
R.when(
isSchemaSectionType(ICashFlowStatementSectionType.NET_INCOME),
this.netIncomeSectionMapper.bind(this)
),
// Cash at beginning node.
R.when(
isSchemaSectionType(ICashFlowStatementSectionType.CASH_AT_BEGINNING),
R.curry(this.cashAtBeginningSectionParser.bind(this))
),
// Aggregate node. (that has no section type).
R.when(
isSchemaSectionType(ICashFlowStatementSectionType.AGGREGATE),
this.regularSectionParser.bind(this)
)
)(section);
}
/**
* Parses the schema section.
* @param {ICashFlowSchemaSection} section
* @returns {ICashFlowSchemaSection}
*/
private schemaSectionTotalParser(
section: ICashFlowSchemaSection | ICashFlowStatementSection,
key: number,
parentValue: ICashFlowSchemaSection[],
context,
accumlatedSections: ICashFlowSchemaSection | ICashFlowStatementSection[]
): ICashFlowSchemaSection {
const isSchemaSectionType = R.curry(this.isSchemaSectionType);
return R.compose(
// Total equation section.
R.when(
isSchemaSectionType(ICashFlowStatementSectionType.TOTAL),
R.curry(this.totalEquationSectionParser.bind(this))(accumlatedSections)
)
)(section);
}
/**
*
* @param acc
* @param value
* @param key
* @param parentValue
* @param context
* @returns
*/
private schemaSectionsReducer(acc, value, key, parentValue, context) {
set(
acc,
context.path,
this.schemaSectionParserIteratee(value, key, parentValue, context, acc)
);
return acc;
}
/**
* Schema sections parser.
* @param {ICashFlowSchemaSection[]}schema
* @returns
*/
private schemaSectionsParser(schema: ICashFlowSchemaSection[]) {
return reduceDeep(
schema,
this.schemaSectionsReducer.bind(this),
[],
MAP_CONFIG
);
}
/**
* Writes the `total` property to the aggregate node.
* @return {ICashFlowStatementSection}
*/
private assocRegularSectionTotal(section: ICashFlowStatementSection) {
const total = this.getAccountsMetaTotal(section.children);
return R.assoc('total', this.getTotalAmountMeta(total), section);
}
/**
* Parses the given node on stage 2.
* @param {ICashFlowStatementSection} node
* @return {ICashFlowStatementSection}
*/
private sectionMapperAfterParsing(section: ICashFlowStatementSection) {
const isSchemaSectionType = R.curry(this.isSchemaSectionType);
return R.compose(
R.when(
isSchemaSectionType(ICashFlowStatementSectionType.REGULAR),
this.assocRegularSectionTotal.bind(this)
),
R.when(
isSchemaSectionType(ICashFlowStatementSectionType.REGULAR),
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocPeriodsToAggregateNode.bind(this)
)
)
)(section);
}
private regularSectionsTotal(
sections: ICashFlowSchemaSection[]
): ICashFlowSchemaSection[] {
return mapValuesDeep(
sections,
this.sectionMapperAfterParsing.bind(this),
MAP_CONFIG
);
}
private totalSectionsParser(
sections: ICashFlowSchemaSection | ICashFlowStatementSection[]
) {
return reduceDeep(
sections,
(acc, value, key, parentValue, context) => {
set(
acc,
context.path,
this.schemaSectionTotalParser(value, key, parentValue, context, acc)
);
return acc;
},
[],
MAP_CONFIG
);
}
// --------------------------------------------
// 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 {
const isSchemaSectionType = R.curry(this.isSchemaSectionType);
return R.ifElse(
isSchemaSectionType(ICashFlowStatementSectionType.ACCOUNTS),
this.isSectionHasChildren.bind(this),
R.always(true)
)(section);
}
/**
* Detarmines the account section has no zero otherwise returns true.
* @param {ICashFlowStatementSection} section
* @returns {boolean}
*/
private isAccountLeafNoneZero(section: ICashFlowStatementSection[]): boolean {
const isSchemaSectionType = R.curry(this.isSchemaSectionType);
return R.ifElse(
isSchemaSectionType(ICashFlowStatementSectionType.ACCOUNT),
this.isSectionNoneZero.bind(this),
R.always(true)
)(section);
}
/**
* Deep filters the non-zero accounts leafs of the report sections.
* @param {ICashFlowStatementSection[]} sections
* @returns {ICashFlowStatementSection[]}
*/
private filterNoneZeroAccountsLeafs(sections: ICashFlowStatementSection[]) {
return filterDeep(
sections,
this.isAccountLeafNoneZero.bind(this),
MAP_CONFIG
);
}
/**
* Deep filter the non-children sections of the report sections.
* @param {ICashFlowStatementSection[]} sections
* @returns {ICashFlowStatementSection[]}
*/
private filterNoneChildrenSections(sections: ICashFlowStatementSection[]) {
return filterDeep(
sections,
this.isAccountsSectionHasChildren.bind(this),
MAP_CONFIG
);
}
/**
* Filters the report data.
* @param {ICashFlowStatementSection[]} sections
* @returns {ICashFlowStatementSection[]}
*/
private filterReportData(sections: ICashFlowStatementSection[]) {
return R.compose(
this.filterNoneChildrenSections.bind(this),
this.filterNoneZeroAccountsLeafs.bind(this)
)(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.bind(this)
),
this.totalSectionsParser.bind(this),
this.regularSectionsTotal.bind(this),
this.schemaSectionsParser.bind(this)
)(schema);
}
/**
* Retrieve the cashflow statement data.
* @return {ICashFlowStatementData}
*/
public reportData(): ICashFlowStatementData {
return this.schemaParser(R.clone(CASH_FLOW_SCHEMA));
}
}
applyMixins(CashFlowStatement, [CashFlowDatePeriods]);
export default CashFlowStatement;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,365 @@
import * as R from 'ramda';
import { isEmpty } from 'lodash';
import moment from 'moment';
import {
ICashFlowStatementSection,
ICashFlowStatementSectionType,
ICashFlowStatement,
ITableRow,
ITableColumn,
ICashFlowStatementQuery,
IDateRange
} from 'interfaces';
import { dateRangeFromToCollection, tableRowMapper } from 'utils';
import { mapValuesDeep } from 'utils/deepdash';
enum IROW_TYPE {
AGGREGATE = 'AGGREGATE',
NET_INCOME = 'NET_INCOME',
ACCOUNTS = 'ACCOUNTS',
ACCOUNT = 'ACCOUNT',
TOTAL = 'TOTAL',
}
const DEEP_CONFIG = { childrenPath: 'children', pathFormat: 'array' };
const DISPLAY_COLUMNS_BY = {
DATE_PERIODS: 'date_periods',
TOTAL: 'total',
};
export default class CashFlowTable implements ICashFlowTable {
private report: {
data: ICashFlowStatement;
query: ICashFlowStatementQuery;
};
private dateRangeSet: IDateRange[];
/**
* Constructor method.
* @param {ICashFlowStatement} reportStatement
*/
constructor(reportStatement: {
data: ICashFlowStatement;
query: ICashFlowStatementQuery;
}) {
this.report = reportStatement;
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: 'label', 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.REGULAR),
this.regularSectionMapper.bind(this)
),
R.when(
isSectionHasType(ICashFlowStatementSectionType.CASH_AT_BEGINNING),
this.regularSectionMapper.bind(this)
),
R.when(
isSectionHasType(ICashFlowStatementSectionType.NET_INCOME),
this.netIncomeSectionMapper.bind(this)
),
R.when(
isSectionHasType(ICashFlowStatementSectionType.ACCOUNTS),
this.accountsSectionMapper.bind(this)
),
R.when(
isSectionHasType(ICashFlowStatementSectionType.ACCOUNT),
this.accountSectionMapper.bind(this)
),
R.when(
isSectionHasType(ICashFlowStatementSectionType.TOTAL),
this.totalSectionMapper.bind(this)
)
)(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
: `Total ${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.bind(this),
this.mapSectionsToTableRows.bind(this)
)(sections);
}
/**
* Retrieve the total columns.
* @returns {ITableColumn}
*/
private totalColumns(): ITableColumn[] {
return [{ key: 'total', label: 'Total' }];
}
/**
* Retrieve the formatted column label from the given date range.
* @param {ICashFlowDateRange} dateRange -
* @return {string}
*/
private formatColumnLabel(dateRange: ICashFlowDateRange) {
const monthFormat = (range) => moment(range.toDate).format('YYYY-MM');
const yearFormat = (range) => moment(range.toDate).format('YYYY');
const dayFormat = (range) => moment(range.toDate).format('YYYY-MM-DD');
const conditions = [
['month', monthFormat],
['year', yearFormat],
['day', dayFormat],
['quarter', monthFormat],
['week', dayFormat],
];
const conditionsPairs = R.map(([type, formatFn]) => ([
R.always(this.isDisplayColumnsType(type)), formatFn,
]), conditions);
return R.compose(R.cond(conditionsPairs))(dateRange);
}
/**
* Date periods columns.
* @returns {ITableColumn[]}
*/
private datePeriodsColumns(): ITableColumn[] {
return this.dateRangeSet.map((dateRange, index) => ({
key: `date-range-${index}`,
label: this.formatColumnLabel(dateRange),
}));
}
/**
* Detarmines the given column type is the current.
* @reutrns {boolean}
*/
private isDisplayColumnsBy(displayColumnsType: string): Boolean {
return this.report.query.displayColumnsType === displayColumnsType;
}
/**
* Detarmines whether the given display columns type is the current.
* @param {string} displayColumnsBy
* @returns {boolean}
*/
private isDisplayColumnsType(displayColumnsBy: string): Boolean {
return this.report.query.displayColumnsBy === displayColumnsBy;
}
/**
* Retrieve the table columns.
* @return {ITableColumn[]}
*/
public tableColumns(): ITableColumn[] {
return R.compose(
R.concat([{ key: 'name', label: '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,75 @@
import { ICashFlowSchemaSection, CASH_FLOW_SECTION_ID, ICashFlowStatementSectionType } from 'interfaces';
import { ACCOUNT_TYPE } from 'data/AccountTypes';
export default [
{
id: CASH_FLOW_SECTION_ID.OPERATING,
label: 'OPERATING ACTIVITIES',
sectionType: ICashFlowStatementSectionType.AGGREGATE,
children: [
{
id: CASH_FLOW_SECTION_ID.NET_INCOME,
label: 'Net income',
sectionType: ICashFlowStatementSectionType.NET_INCOME,
},
{
id: CASH_FLOW_SECTION_ID.OPERATING_ACCOUNTS,
label: 'Adjustments net income by operating activities.',
sectionType: ICashFlowStatementSectionType.ACCOUNTS,
accountsRelations: [
{ type: ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE, direction: 'mines' },
{ type: ACCOUNT_TYPE.INVENTORY, direction: 'mines' },
{ type: ACCOUNT_TYPE.NON_CURRENT_ASSET, direction: 'mines' },
{ type: ACCOUNT_TYPE.ACCOUNTS_PAYABLE, direction: 'plus' },
{ type: ACCOUNT_TYPE.CREDIT_CARD, direction: 'plus' },
{ type: ACCOUNT_TYPE.TAX_PAYABLE, direction: 'plus' },
{ type: ACCOUNT_TYPE.OTHER_CURRENT_ASSET, direction: 'mines' },
{ type: ACCOUNT_TYPE.OTHER_CURRENT_LIABILITY, direction: 'plus' },
{ type: ACCOUNT_TYPE.NON_CURRENT_LIABILITY, direction: 'plus' },
],
showAlways: true,
},
],
footerLabel: 'Net cash provided by operating activities',
},
{
id: CASH_FLOW_SECTION_ID.INVESTMENT,
sectionType: ICashFlowStatementSectionType.ACCOUNTS,
label: 'INVESTMENT ACTIVITIES',
accountsRelations: [
{ type: ACCOUNT_TYPE.FIXED_ASSET, direction: 'mines' }
],
footerLabel: 'Net cash provided by investing activities',
},
{
id: CASH_FLOW_SECTION_ID.FINANCIAL,
label: 'FINANCIAL ACTIVITIES',
sectionType: ICashFlowStatementSectionType.ACCOUNTS,
accountsRelations: [
{ type: ACCOUNT_TYPE.LOGN_TERM_LIABILITY, direction: 'plus' },
{ type: ACCOUNT_TYPE.EQUITY, direction: 'plus' },
],
footerLabel: 'Net cash provided by financing activities',
},
{
id: CASH_FLOW_SECTION_ID.CASH_BEGINNING_PERIOD,
sectionType: ICashFlowStatementSectionType.CASH_AT_BEGINNING,
label: 'Cash at beginning of period',
accountsRelations: [
{ type: ACCOUNT_TYPE.CASH, direction: 'plus' },
{ type: ACCOUNT_TYPE.BANK, direction: 'plus' },
],
},
{
id: CASH_FLOW_SECTION_ID.NET_CASH_INCREASE,
sectionType: ICashFlowStatementSectionType.TOTAL,
equation: 'OPERATING + INVESTMENT + FINANCIAL',
label: 'NET CASH INCREASE FOR PERIOD',
},
{
id: CASH_FLOW_SECTION_ID.CASH_END_PERIOD,
label: 'CASH AT END OF PERIOD',
sectionType: ICashFlowStatementSectionType.TOTAL,
equation: 'NET_CASH_INCREASE + CASH_BEGINNING_PERIOD',
},
] as ICashFlowSchemaSection[];

View File

@@ -0,0 +1,69 @@
import { Inject, Service } from 'typedi';
import { map, isEmpty } from 'lodash';
import { ICustomer, IAccount } from 'interfaces';
import HasTenancyService from 'services/Tenancy/TenancyService';
import { ACCOUNT_TYPE } from 'data/AccountTypes';
@Service()
export default class CustomerBalanceSummaryRepository {
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve the report customers.
* @param {number} tenantId
* @param {number[]} customersIds
* @returns {ICustomer[]}
*/
public getCustomers(tenantId: number, customersIds: number[]): ICustomer[] {
const { Customer } = this.tenancy.models(tenantId);
return Customer.query()
.orderBy('displayName')
.onBuild((query) => {
if (!isEmpty(customersIds)) {
query.whereIn('id', customersIds);
}
});
}
/**
* Retrieve the A/R accounts.
* @param {number} tenantId
* @returns {Promise<IAccount[]>}
*/
public getReceivableAccounts(tenantId: number): Promise<IAccount> {
const { Account } = this.tenancy.models(tenantId);
return Account.query().where(
'accountType',
ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE
);
}
/**
* Retrieve the customers credit/debit totals
* @param {number} tenantId
* @returns
*/
public async getCustomersTransactions(tenantId: number, asDate: any) {
const { AccountTransaction } = this.tenancy.models(tenantId);
// Retrieve the receivable accounts A/R.
const receivableAccounts = await this.getReceivableAccounts(tenantId);
const receivableAccountsIds = map(receivableAccounts, 'id');
// Retrieve the customers transactions of A/R accounts.
const customersTranasctions = await AccountTransaction.query().onBuild(
(query) => {
query.whereIn('accountId', receivableAccountsIds);
query.modify('filterDateRange', null, asDate);
query.groupBy('contactId');
query.sum('credit as credit');
query.sum('debit as debit');
query.select('contactId');
}
);
return customersTranasctions;
}
}

View File

@@ -7,20 +7,26 @@ import {
ICustomerBalanceSummaryService,
ICustomerBalanceSummaryQuery,
ICustomerBalanceSummaryStatement,
ICustomer
ICustomer,
ILedgerEntry,
} from 'interfaces';
import { CustomerBalanceSummaryReport } from './CustomerBalanceSummary';
import { ACCOUNT_TYPE } from 'data/AccountTypes';
import Ledger from 'services/Accounting/Ledger';
import CustomerBalanceSummaryRepository from './CustomerBalanceSummaryRepository';
export default class CustomerBalanceSummaryService
implements ICustomerBalanceSummaryService {
implements ICustomerBalanceSummaryService
{
@Inject()
tenancy: TenancyService;
@Inject('logger')
logger: any;
@Inject()
reportRepository: CustomerBalanceSummaryRepository;
/**
* Defaults balance sheet filter query.
* @return {ICustomerBalanceSummaryQuery}
@@ -43,64 +49,24 @@ export default class CustomerBalanceSummaryService
};
}
/**
* Retrieve the A/R accounts.
* @param tenantId
* @returns
*/
private getReceivableAccounts(tenantId: number) {
const { Account } = this.tenancy.models(tenantId);
return Account.query().where(
'accountType',
ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE
);
}
/**
* Retrieve the customers credit/debit totals
* Retrieve the customers ledger entries mapped from accounts transactions.
* @param {number} tenantId
* @returns
* @param {Date|string} asDate
* @returns {Promise<ILedgerEntry[]>}
*/
private async getReportCustomersTransactions(tenantId: number, asDate: any) {
const { AccountTransaction } = this.tenancy.models(tenantId);
// Retrieve the receivable accounts A/R.
const receivableAccounts = await this.getReceivableAccounts(tenantId);
const receivableAccountsIds = map(receivableAccounts, 'id');
// Retrieve the customers transactions of A/R accounts.
const customersTranasctions = await AccountTransaction.query().onBuild(
(query) => {
query.whereIn('accountId', receivableAccountsIds);
query.modify('filterDateRange', null, asDate);
query.groupBy('contactId');
query.sum('credit as credit');
query.sum('debit as debit');
query.select('contactId');
}
private async getReportCustomersEntries(
tenantId: number,
asDate: Date | string
): Promise<ILedgerEntry[]> {
const transactions = await this.reportRepository.getCustomersTransactions(
tenantId,
asDate
);
const commonProps = { accountNormal: 'debit', date: asDate };
return R.map(R.merge(commonProps))(customersTranasctions);
}
/**
* Retrieve the report customers.
* @param {number} tenantId
* @param {number[]} customersIds
* @returns {ICustomer[]}
*/
private getReportCustomers(tenantId: number, customersIds: number[]): ICustomer[] {
const { Customer } = this.tenancy.models(tenantId);
return Customer.query()
.orderBy('displayName')
.onBuild((query) => {
if (!isEmpty(customersIds)) {
query.whereIn('id', customersIds);
}
});
return R.map(R.merge(commonProps))(transactions);
}
/**
@@ -130,17 +96,17 @@ export default class CustomerBalanceSummaryService
}
);
// Retrieve the customers list ordered by the display name.
const customers = await this.getReportCustomers(
const customers = await this.reportRepository.getCustomers(
tenantId,
query.customersIds
);
// Retrieve the customers debit/credit totals.
const customersTransactions = await this.getReportCustomersTransactions(
const customersEntries = await this.getReportCustomersEntries(
tenantId,
filter.asDate
);
// Ledger query.
const ledger = Ledger.fromTransactions(customersTransactions);
const ledger = new Ledger(customersEntries);
// Report instance.
const report = new CustomerBalanceSummaryReport(
@@ -153,7 +119,7 @@ export default class CustomerBalanceSummaryService
return {
data: report.reportData(),
columns: report.reportColumns(),
query: filter
query: filter,
};
}
}

View File

@@ -1,4 +1,9 @@
import { IFormatNumberSettings, INumberFormatQuery } from 'interfaces';
import moment from 'moment';
import {
ICashFlowStatementTotal,
IFormatNumberSettings,
INumberFormatQuery,
} from 'interfaces';
import { formatNumber } from 'utils';
export default class FinancialSheet {
@@ -37,7 +42,7 @@ export default class FinancialSheet {
};
return formatNumber(number, settings);
}
/**
* Formatting full amount with different format settings.
* @param {number} amount -
@@ -52,24 +57,68 @@ export default class FinancialSheet {
return this.formatNumber(amount, {
money: numberFormat.formatMoney === 'none' ? false : true,
excerptZero: false,
...settings
...settings,
});
}
/**
* Formates the amount to the percentage string.
* @param {number} amount
* @param {number} amount
* @returns {string}
*/
protected formatPercentage(
amount
): string {
protected formatPercentage(amount): string {
const percentage = amount * 100;
return formatNumber(percentage, {
symbol: '%',
excerptZero: true,
money: false,
})
});
}
/**
* Retrieve the amount meta object.
* @param {number} amount
* @returns {ICashFlowStatementTotal}
*/
protected getAmountMeta(
amount: number,
overrideSettings?: IFormatNumberSettings
): ICashFlowStatementTotal {
return {
amount,
formattedAmount: this.formatNumber(amount, overrideSettings),
currencyCode: this.baseCurrency,
};
}
/**
* Retrieve the total amount meta object.
* @param {number} amount
* @returns {ICashFlowStatementTotal}
*/
protected getTotalAmountMeta(
amount: number,
title?: string
): ICashFlowStatementTotal {
return {
...(title ? { title } : {}),
amount,
formattedAmount: this.formatTotalNumber(amount),
currencyCode: this.baseCurrency,
};
}
/**
* Retrieve the date meta.
* @param {Date} date
* @param {string} format
* @returns
*/
protected getDateMeta(date: Date, format = 'YYYY-MM-DD') {
return {
formattedDate: moment(date).format(format),
date: moment(date).toDate(),
};
}
}

View File

@@ -0,0 +1,407 @@
import * as R from 'ramda';
import { defaultTo, sumBy, get } from 'lodash';
import {
IInventoryDetailsQuery,
IItem,
IInventoryTransaction,
TInventoryTransactionDirection,
IInventoryDetailsNumber,
IInventoryDetailsDate,
IInventoryDetailsData,
IInventoryDetailsItem,
IInventoryDetailsClosing,
INumberFormatQuery,
IInventoryDetailsOpening,
IInventoryDetailsItemTransaction,
IFormatNumberSettings,
} from 'interfaces';
import FinancialSheet from '../FinancialSheet';
import { transformToMapBy, transformToMapKeyValue } from 'utils';
import { filterDeep } from 'utils/deepdash';
import moment from 'moment';
const MAP_CONFIG = { childrenPath: 'children', pathFormat: 'array' };
enum INodeTypes {
ITEM = 'item',
TRANSACTION = 'transaction',
OPENING_ENTRY = 'OPENING_ENTRY',
CLOSING_ENTRY = 'CLOSING_ENTRY',
}
export default class InventoryDetails extends FinancialSheet {
readonly inventoryTransactionsByItemId: Map<number, IInventoryTransaction[]>;
readonly openingBalanceTransactions: Map<number, IInventoryTransaction>;
readonly query: IInventoryDetailsQuery;
readonly numberFormat: INumberFormatQuery;
readonly baseCurrency: string;
readonly items: IItem[];
/**
* Constructor method.
* @param {IItem[]} items - Items.
* @param {IInventoryTransaction[]} inventoryTransactions - Inventory transactions.
* @param {IInventoryDetailsQuery} query - Report query.
* @param {string} baseCurrency - The base currency.
*/
constructor(
items: IItem[],
openingBalanceTransactions: IInventoryTransaction[],
inventoryTransactions: IInventoryTransaction[],
query: IInventoryDetailsQuery,
baseCurrency: string
) {
super();
this.inventoryTransactionsByItemId = transformToMapBy(
inventoryTransactions,
'itemId'
);
this.openingBalanceTransactions = transformToMapKeyValue(
openingBalanceTransactions,
'itemId'
);
this.query = query;
this.numberFormat = this.query.numberFormat;
this.items = items;
this.baseCurrency = baseCurrency;
}
/**
* Retrieve the number meta.
* @param {number} number
* @returns
*/
private getNumberMeta(
number: number,
settings?: IFormatNumberSettings
): IInventoryDetailsNumber {
return {
formattedNumber: this.formatNumber(number, {
excerptZero: true,
money: false,
...settings,
}),
number: number,
};
}
/**
* Retrieve the total number meta.
* @param {number} number -
* @param {IFormatNumberSettings} settings -
* @retrun {IInventoryDetailsNumber}
*/
private getTotalNumberMeta(
number: number,
settings?: IFormatNumberSettings
): IInventoryDetailsNumber {
return this.getNumberMeta(number, { excerptZero: false, ...settings });
}
/**
* Retrieve the date meta.
* @param {Date|string} date
* @returns {IInventoryDetailsDate}
*/
private getDateMeta(date: Date | string): IInventoryDetailsDate {
return {
formattedDate: moment(date).format('YYYY-MM-DD'),
date: moment(date).toDate(),
};
}
/**
* Adjusts the movement amount.
* @param {number} amount
* @param {TInventoryTransactionDirection} direction
* @returns {number}
*/
private adjustAmountMovement(
direction: TInventoryTransactionDirection,
amount: number
): number {
return direction === 'OUT' ? amount * -1 : amount;
}
/**
* Accumlate and mapping running quantity on transactions.
* @param {IInventoryDetailsItemTransaction[]} transactions
* @returns {IInventoryDetailsItemTransaction[]}
*/
private mapAccumTransactionsRunningQuantity(
transactions: IInventoryDetailsItemTransaction[]
): IInventoryDetailsItemTransaction[] {
const initial = this.getNumberMeta(0);
const mapAccumAppender = (a, b) => {
const total = a.runningQuantity.number + b.quantityMovement.number;
const totalMeta = this.getNumberMeta(total, { excerptZero: false });
const accum = { ...b, runningQuantity: totalMeta };
return [accum, accum];
};
return R.mapAccum(
mapAccumAppender,
{ runningQuantity: initial },
transactions
)[1];
}
/**
* Accumlate and mapping running valuation on transactions.
* @param {IInventoryDetailsItemTransaction[]} transactions
* @returns {IInventoryDetailsItemTransaction}
*/
private mapAccumTransactionsRunningValuation(
transactions: IInventoryDetailsItemTransaction[]
): IInventoryDetailsItemTransaction[] {
const initial = this.getNumberMeta(0);
const mapAccumAppender = (a, b) => {
const total = a.runningValuation.number + b.valueMovement.number;
const totalMeta = this.getNumberMeta(total, { excerptZero: false });
const accum = { ...b, runningValuation: totalMeta };
return [accum, accum];
};
return R.mapAccum(
mapAccumAppender,
{ runningValuation: initial },
transactions
)[1];
}
/**
* Mappes the item transaction to inventory item transaction node.
* @param {IItem} item
* @param {IInvetoryTransaction} transaction
* @returns {IInventoryDetailsItemTransaction}
*/
private itemTransactionMapper(
item: IItem,
transaction: IInventoryTransaction
): IInventoryDetailsItemTransaction {
const value = transaction.quantity * transaction.rate;
const amountMovement = R.curry(this.adjustAmountMovement)(
transaction.direction
);
// Quantity movement.
const quantityMovement = amountMovement(transaction.quantity);
const valueMovement = amountMovement(value);
// Profit margin.
const profitMargin = Math.max(
value - transaction.costLotAggregated.cost,
0
);
return {
nodeType: INodeTypes.TRANSACTION,
date: this.getDateMeta(transaction.date),
transactionType: transaction.transcationTypeFormatted,
transactionNumber: transaction.meta.transactionNumber,
direction: transaction.direction,
quantityMovement: this.getNumberMeta(quantityMovement),
valueMovement: this.getNumberMeta(valueMovement),
quantity: this.getNumberMeta(transaction.quantity),
value: this.getNumberMeta(value),
rate: this.getNumberMeta(transaction.rate),
cost: this.getNumberMeta(transaction.costLotAggregated.cost),
profitMargin: this.getNumberMeta(profitMargin),
runningQuantity: this.getNumberMeta(0),
runningValuation: this.getNumberMeta(0),
};
}
/**
* Retrieve the inventory transcations by item id.
* @param {number} itemId
* @returns {IInventoryTransaction[]}
*/
private getInventoryTransactionsByItemId(
itemId: number
): IInventoryTransaction[] {
return defaultTo(this.inventoryTransactionsByItemId.get(itemId + ''), []);
}
/**
* Retrieve the item transaction node by the given item.
* @param {IItem} item
* @returns {IInventoryDetailsItemTransaction[]}
*/
private getItemTransactions(item: IItem): IInventoryDetailsItemTransaction[] {
const transactions = this.getInventoryTransactionsByItemId(item.id);
return R.compose(
this.mapAccumTransactionsRunningQuantity.bind(this),
this.mapAccumTransactionsRunningValuation.bind(this),
R.map(R.curry(this.itemTransactionMapper.bind(this))(item))
)(transactions);
}
/**
* Mappes the given item transactions.
* @param {IItem} item -
* @returns {(
* IInventoryDetailsItemTransaction
* | IInventoryDetailsOpening
* | IInventoryDetailsClosing
* )[]}
*/
private itemTransactionsMapper(
item: IItem
): (
| IInventoryDetailsItemTransaction
| IInventoryDetailsOpening
| IInventoryDetailsClosing
)[] {
const transactions = this.getItemTransactions(item);
const openingValuation = this.getItemOpeingValuation(item);
const closingValuation = this.getItemClosingValuation(item, transactions);
const hasTransactions = transactions.length > 0;
const isItemHasOpeningBalance = this.isItemHasOpeningBalance(item.id);
return R.pipe(
R.concat(transactions),
R.when(R.always(isItemHasOpeningBalance), R.prepend(openingValuation)),
R.when(R.always(hasTransactions), R.append(closingValuation))
)([]);
}
/**
* Detarmines the given item has opening balance transaction.
* @param {number} itemId - Item id.
* @return {boolean}
*/
private isItemHasOpeningBalance(itemId: number): boolean {
return !!this.openingBalanceTransactions.get(itemId);
}
/**
* Retrieve the given item opening valuation.
* @param {IItem} item -
* @returns {IInventoryDetailsOpening}
*/
private getItemOpeingValuation(item: IItem): IInventoryDetailsOpening {
const openingBalance = this.openingBalanceTransactions.get(item.id);
const quantity = defaultTo(get(openingBalance, 'quantity'), 0);
const value = defaultTo(get(openingBalance, 'value'), 0);
return {
nodeType: INodeTypes.OPENING_ENTRY,
date: this.getDateMeta(this.query.fromDate),
quantity: this.getTotalNumberMeta(quantity),
value: this.getTotalNumberMeta(value),
};
}
/**
* Retrieve the given item closing valuation.
* @param {IItem} item -
* @returns {IInventoryDetailsOpening}
*/
private getItemClosingValuation(
item: IItem,
transactions: IInventoryDetailsItemTransaction[]
): IInventoryDetailsOpening {
const value = sumBy(transactions, 'valueMovement.number');
const quantity = sumBy(transactions, 'quantityMovement.number');
const profitMargin = sumBy(transactions, 'profitMargin.number');
const cost = sumBy(transactions, 'cost.number');
return {
nodeType: INodeTypes.CLOSING_ENTRY,
date: this.getDateMeta(this.query.toDate),
quantity: this.getTotalNumberMeta(quantity),
value: this.getTotalNumberMeta(value),
cost: this.getTotalNumberMeta(cost),
profitMargin: this.getTotalNumberMeta(profitMargin),
};
}
/**
* Retrieve the item node of the report.
* @param {IItem} item
* @returns {IInventoryDetailsItem}
*/
private itemsNodeMapper(item: IItem): IInventoryDetailsItem {
return {
id: item.id,
name: item.name,
code: item.code,
nodeType: INodeTypes.ITEM,
children: this.itemTransactionsMapper(item),
};
}
/**
* Detarmines the given node equals the given type.
* @param {string} nodeType
* @param {IItem} node
* @returns {boolean}
*/
private isNodeTypeEquals(
nodeType: string,
node: IInventoryDetailsItem
): boolean {
return nodeType === node.nodeType;
}
/**
* Detarmines whether the given item node has transactions.
* @param {IInventoryDetailsItem} item
* @returns {boolean}
*/
private isItemNodeHasTransactions(item: IInventoryDetailsItem) {
return !!this.inventoryTransactionsByItemId.get(item.id);
}
/**
* Detarmines the filter
* @param {IInventoryDetailsItem} item
* @return {boolean}
*/
private isFilterNode(item: IInventoryDetailsItem): boolean {
return R.ifElse(
R.curry(this.isNodeTypeEquals)(INodeTypes.ITEM),
this.isItemNodeHasTransactions.bind(this),
R.always(true)
)(item);
}
/**
* Filters items nodes.
* @param {IInventoryDetailsItem[]} items -
* @returns {IInventoryDetailsItem[]}
*/
private filterItemsNodes(items: IInventoryDetailsItem[]) {
return filterDeep(items, this.isFilterNode.bind(this), MAP_CONFIG);
}
/**
* Retrieve the items nodes of the report.
* @param {IItem} items
* @returns {IInventoryDetailsItem[]}
*/
private itemsNodes(items: IItem[]): IInventoryDetailsItem[] {
return R.compose(
this.filterItemsNodes.bind(this),
R.map(this.itemsNodeMapper.bind(this))
)(items);
}
/**
* Retrieve the inventory item details report data.
* @returns {IInventoryDetailsData}
*/
public reportData(): IInventoryDetailsData {
return this.itemsNodes(this.items);
}
}

View File

@@ -0,0 +1,87 @@
import { Inject } from 'typedi';
import { raw } from 'objection';
import moment from 'moment';
import { IItem, IInventoryDetailsQuery, IInventoryTransaction } from 'interfaces';
import HasTenancyService from 'services/Tenancy/TenancyService';
export default class InventoryDetailsRepository {
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve inventory items.
* @param {number} tenantId -
* @returns {Promise<IItem>}
*/
public getInventoryItems(tenantId: number): Promise<IItem[]> {
const { Item } = this.tenancy.models(tenantId);
return Item.query().where('type', 'inventory');
}
/**
* Retrieve the items opening balance transactions.
* @param {number} tenantId -
* @param {IInventoryDetailsQuery}
* @return {Promise<IInventoryTransaction>}
*/
public async openingBalanceTransactions(
tenantId: number,
filter: IInventoryDetailsQuery
): Promise<IInventoryTransaction[]> {
const { InventoryTransaction } = this.tenancy.models(tenantId);
const openingBalanceDate = moment(filter.fromDate)
.subtract(1, 'days')
.toDate();
// Opening inventory transactions.
const openingTransactions = InventoryTransaction.query()
.select('*')
.select(raw("IF(`DIRECTION` = 'IN', `QUANTITY`, 0) as 'QUANTITY_IN'"))
.select(raw("IF(`DIRECTION` = 'OUT', `QUANTITY`, 0) as 'QUANTITY_OUT'"))
.select(
raw("IF(`DIRECTION` = 'IN', `QUANTITY` * `RATE`, 0) as 'VALUE_IN'")
)
.select(
raw("IF(`DIRECTION` = 'OUT', `QUANTITY` * `RATE`, 0) as 'VALUE_OUT'")
)
.modify('filterDateRange', null, openingBalanceDate)
.as('inventory_transactions');
const openingBalanceTransactions = await InventoryTransaction.query()
.from(openingTransactions)
.select('itemId')
.select(raw('SUM(`QUANTITY_IN` - `QUANTITY_OUT`) AS `QUANTITY`'))
.select(raw('SUM(`VALUE_IN` - `VALUE_OUT`) AS `VALUE`'))
.groupBy('itemId')
.sum('rate as rate')
.sum('quantityIn as quantityIn')
.sum('quantityOut as quantityOut')
.sum('valueIn as valueIn')
.sum('valueOut as valueOut')
.withGraphFetched('itemCostAggregated');
return openingBalanceTransactions;
}
/**
* Retrieve the items inventory tranasactions.
* @param {number} tenantId -
* @param {IInventoryDetailsQuery}
* @return {Promise<IInventoryTransaction>}
*/
public async itemInventoryTransactions(
tenantId: number,
filter: IInventoryDetailsQuery
): Promise<IInventoryTransaction[]> {
const { InventoryTransaction } = this.tenancy.models(tenantId);
const inventoryTransactions = InventoryTransaction.query()
.modify('filterDateRange', filter.fromDate, filter.toDate)
.withGraphFetched('meta')
.withGraphFetched('costLotAggregated');
return inventoryTransactions;
}
}

View File

@@ -0,0 +1,81 @@
import moment from 'moment';
import { Service, Inject } from 'typedi';
import { raw } from 'objection';
import { IInventoryDetailsQuery, IInventoryTransaction } from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService';
import InventoryDetails from './InventoryDetails';
import FinancialSheet from '../FinancialSheet';
import InventoryDetailsRepository from './InventoryDetailsRepository';
@Service()
export default class InventoryDetailsService extends FinancialSheet {
@Inject()
tenancy: TenancyService;
@Inject()
reportRepo: InventoryDetailsRepository;
/**
* Defaults balance sheet filter query.
* @return {IBalanceSheetQuery}
*/
get defaultQuery(): IInventoryDetailsQuery {
return {
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
toDate: moment().endOf('year').format('YYYY-MM-DD'),
numberFormat: {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'total',
negativeFormat: 'mines',
},
noneTransactions: false,
};
}
/**
* Retrieve the inventory details report data.
* @param {number} tenantId -
* @param {IInventoryDetailsQuery} query -
*/
public async inventoryDetails(
tenantId: number,
query: IInventoryDetailsQuery
): Promise<any> {
// Settings tenant service.
const settings = this.tenancy.settings(tenantId);
const baseCurrency = settings.get({
group: 'organization',
key: 'base_currency',
});
const filter = {
...this.defaultQuery,
...query,
};
// Retrieves the items.
const items = await this.reportRepo.getInventoryItems(tenantId);
// Opening balance transactions.
const openingBalanceTransactions =
await this.reportRepo.openingBalanceTransactions(tenantId, filter);
// Retrieves the inventory transaction.
const inventoryTransactions =
await this.reportRepo.itemInventoryTransactions(tenantId, filter);
// Inventory details report mapper.
const inventoryDetailsInstance = new InventoryDetails(
items,
openingBalanceTransactions,
inventoryTransactions,
filter,
baseCurrency
);
return {
data: inventoryDetailsInstance.reportData(),
};
}
}

View File

@@ -0,0 +1,183 @@
import * as R from 'ramda';
import {
IInventoryDetailsItem,
IInventoryDetailsItemTransaction,
IInventoryDetailsClosing,
ITableColumn,
ITableRow,
IInventoryDetailsNode,
IInventoryDetailsOpening,
} from 'interfaces';
import { mapValuesDeep } from 'utils/deepdash';
import { tableRowMapper } from 'utils';
enum IROW_TYPE {
ITEM = 'ITEM',
TRANSACTION = 'TRANSACTION',
CLOSING_ENTRY = 'CLOSING_ENTRY',
OPENING_ENTRY = 'OPENING_ENTRY',
}
const MAP_CONFIG = { childrenPath: 'children', pathFormat: 'array' };
export default class InventoryDetailsTable {
/**
* Constructor methiod.
* @param {ICashFlowStatement} reportStatement
*/
constructor(reportStatement) {
this.report = reportStatement;
}
/**
* Mappes the item node to table rows.
* @param {IInventoryDetailsItem} item
* @returns {ITableRow}
*/
private itemNodeMapper(item: IInventoryDetailsItem) {
const columns = [{ key: 'item_name', accessor: 'name' }];
return tableRowMapper(item, columns, {
rowTypes: [IROW_TYPE.ITEM],
});
}
/**
* Mappes the item inventory transaction to table row.
* @param {IInventoryDetailsItemTransaction} transaction
* @returns {ITableRow}
*/
private itemTransactionNodeMapper(
transaction: IInventoryDetailsItemTransaction
) {
const columns = [
{ key: 'date', accessor: 'date.formattedDate' },
{ key: 'transaction_type', accessor: 'transactionType' },
{ key: 'transaction_id', accessor: 'transactionNumber' },
{
key: 'quantity_movement',
accessor: 'quantityMovement.formattedNumber',
},
{ key: 'rate', accessor: 'rate.formattedNumber' },
{ key: 'value_movement', accessor: 'valueMovement.formattedNumber' },
{ key: 'cost', accessor: 'cost.formattedNumber' },
{ key: 'profit_margin', accessor: 'profitMargin.formattedNumber' },
{ key: 'running_quantity', accessor: 'runningQuantity.formattedNumber' },
{ key: 'running_valuation', accessor: 'runningValuation.formattedNumber' },
];
return tableRowMapper(transaction, columns, {
rowTypes: [IROW_TYPE.TRANSACTION],
});
}
/**
* Opening balance transaction mapper to table row.
* @param {IInventoryDetailsOpening} transaction
* @returns {ITableRow}
*/
private openingNodeMapper(transaction: IInventoryDetailsOpening): ITableRow {
const columns = [
{ key: 'date', accessor: 'date.formattedDate' },
{ key: 'closing', value: 'Opening Balance' },
{ key: 'empty' },
{ key: 'quantity', accessor: 'quantity.formattedNumber' },
{ key: 'empty' },
{ key: 'value', accessor: 'value.formattedNumber' },
];
return tableRowMapper(transaction, columns, {
rowTypes: [IROW_TYPE.OPENING_ENTRY],
});
}
/**
* Closing balance transaction mapper to table raw.
* @param {IInventoryDetailsClosing} transaction
* @returns {ITableRow}
*/
private closingNodeMapper(transaction: IInventoryDetailsClosing): ITableRow {
const columns = [
{ key: 'date', accessor: 'date.formattedDate' },
{ key: 'closing', value: 'Closing Balance' },
{ key: 'empty' },
{ key: 'quantity', accessor: 'quantity.formattedNumber' },
{ key: 'empty' },
{ key: 'value', accessor: 'value.formattedNumber' },
{ key: 'cost', accessor: 'cost.formattedNumber' },
{ key: 'profitMargin', accessor: 'profitMargin.formattedNumber' },
];
return tableRowMapper(transaction, columns, {
rowTypes: [IROW_TYPE.CLOSING_ENTRY],
});
}
/**
* Detarmines the ginve inventory details node type.
* @param {string} type
* @param {IInventoryDetailsNode} node
* @returns {boolean}
*/
private isNodeTypeEquals(type: string, node: IInventoryDetailsNode): boolean {
return node.nodeType === type;
}
/**
* Mappes the given item or transactions node to table rows.
* @param {IInventoryDetailsNode} node -
* @return {ITableRow}
*/
private itemMapper(node: IInventoryDetailsNode): ITableRow {
return R.compose(
R.when(
R.curry(this.isNodeTypeEquals)('OPENING_ENTRY'),
this.openingNodeMapper
),
R.when(
R.curry(this.isNodeTypeEquals)('CLOSING_ENTRY'),
this.closingNodeMapper
),
R.when(R.curry(this.isNodeTypeEquals)('item'), this.itemNodeMapper),
R.when(
R.curry(this.isNodeTypeEquals)('transaction'),
this.itemTransactionNodeMapper.bind(this)
)
)(node);
}
/**
* Mappes the items nodes to table rows.
* @param {IInventoryDetailsItem[]} items
* @returns {ITableRow[]}
*/
private itemsMapper(items: IInventoryDetailsItem[]): ITableRow[] {
return mapValuesDeep(items, this.itemMapper.bind(this), MAP_CONFIG);
}
/**
* Retrieve the table rows of the inventory item details.
* @returns {ITableRow[]}
*/
public tableData(): ITableRow[] {
return this.itemsMapper(this.report.data);
}
/**
* Retrieve the table columns of inventory details report.
* @returns {ITableColumn[]}
*/
public tableColumns(): ITableColumn[] {
return [
{ key: 'date', label: 'Date' },
{ key: 'transaction_type', label: 'Transaction type' },
{ key: 'transaction_id', label: 'Transaction #' },
{ key: 'quantity_movement', label: 'Quantity' },
{ key: 'rate', label: 'Rate' },
{ key: 'value_movement', label: 'Value' },
{ key: 'cost', label: 'Cost' },
{ key: 'profit_margin', label: 'Profit Margin' },
{ key: 'quantity_on_hand', label: 'Running quantity' },
{ key: 'value', label: 'Running Value' },
];
}
}

View File

@@ -0,0 +1,92 @@
import { map } from 'lodash';
import { IAccount, IAccountTransaction } from 'interfaces';
import { ACCOUNT_TYPE } from 'data/AccountTypes';
import HasTenancyService from 'services/Tenancy/TenancyService';
import { Inject } from 'typedi';
export default class TransactionsByCustomersRepository {
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve the report customers.
* @param {number} tenantId
* @returns {Promise<ICustomer[]>}
*/
public async getCustomers(tenantId: number) {
const { Customer } = this.tenancy.models(tenantId);
return Customer.query().orderBy('displayName');
}
/**
* Retrieve the accounts receivable.
* @param {number} tenantId
* @returns {Promise<IAccount[]>}
*/
public async getReceivableAccounts(tenantId: number): Promise<IAccount[]> {
const { Account } = this.tenancy.models(tenantId);
const accounts = await Account.query().where(
'accountType',
ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE
);
return accounts;
}
/**
* Retrieve the customers opening balance transactions.
* @param {number} tenantId - Tenant id.
* @param {number} openingDate - Opening date.
* @param {number} customersIds - Customers ids.
* @returns {Promise<IAccountTransaction[]>}
*/
public async getCustomersOpeningBalanceTransactions(
tenantId: number,
openingDate: Date,
customersIds?: number[]
): Promise<IAccountTransaction[]> {
const { AccountTransaction } = this.tenancy.models(tenantId);
const receivableAccounts = await this.getReceivableAccounts(tenantId);
const receivableAccountsIds = map(receivableAccounts, 'id');
const openingTransactions = await AccountTransaction.query().modify(
'contactsOpeningBalance',
openingDate,
receivableAccountsIds,
customersIds
);
return openingTransactions;
}
/**
* Retrieve the customers periods transactions.
* @param {number} tenantId - Tenant id.
* @param {Date|string} openingDate - Opening date.
* @param {number[]} customersIds - Customers ids.
* @return {Promise<IAccountTransaction[]>}
*/
public async getCustomersPeriodTransactions(
tenantId: number,
fromDate: Date,
toDate: Date
): Promise<IAccountTransaction[]> {
const { AccountTransaction } = this.tenancy.models(tenantId);
const receivableAccounts = await this.getReceivableAccounts(tenantId);
const receivableAccountsIds = map(receivableAccounts, 'id');
const transactions = await AccountTransaction.query().onBuild((query) => {
// Filter by date.
query.modify('filterDateRange', fromDate, toDate);
// Filter by customers.
query.whereNot('contactId', null);
// Filter by accounts.
query.whereIn('accountId', receivableAccountsIds);
});
return transactions;
}
}

View File

@@ -1,25 +1,29 @@
import { Inject } from 'typedi';
import * as R from 'ramda';
import moment from 'moment';
import { map } from 'lodash';
import TenancyService from 'services/Tenancy/TenancyService';
import {
ITransactionsByCustomersService,
ITransactionsByCustomersFilter,
ITransactionsByCustomersStatement,
ILedgerEntry,
} from 'interfaces';
import TransactionsByCustomers from './TransactionsByCustomers';
import Ledger from 'services/Accounting/Ledger';
import { ACCOUNT_TYPE } from 'data/AccountTypes';
import TransactionsByCustomersRepository from './TransactionsByCustomersRepository';
export default class TransactionsByCustomersService
implements ITransactionsByCustomersService {
implements ITransactionsByCustomersService
{
@Inject()
tenancy: TenancyService;
@Inject('logger')
logger: any;
@Inject()
reportRepository: TransactionsByCustomersRepository;
/**
* Defaults balance sheet filter query.
* @return {ICustomerBalanceSummaryQuery}
@@ -44,43 +48,24 @@ export default class TransactionsByCustomersService
}
/**
* Retrieve the accounts receivable.
* Retrieve the customers opening balance ledger entries.
* @param {number} tenantId
* @returns
* @param {Date} openingDate
* @param {number[]} customersIds
* @returns {Promise<ILedgerEntry[]>}
*/
async getReceivableAccounts(tenantId: number) {
const { Account } = this.tenancy.models(tenantId);
const accounts = await Account.query().where(
'accountType',
ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE
);
return accounts;
}
/**
* Retrieve the customers opening balance transactions.
* @param {number} tenantId
* @param {number} openingDate
* @param {number} customersIds
* @returns {}
*/
async getCustomersOpeningBalance(
private async getCustomersOpeningBalanceEntries(
tenantId: number,
openingDate: Date,
customersIds?: number[]
): Promise<ILedgerEntry[]> {
const { AccountTransaction } = this.tenancy.models(tenantId);
const openingTransactions =
await this.reportRepository.getCustomersOpeningBalanceTransactions(
tenantId,
openingDate,
customersIds
);
const receivableAccounts = await this.getReceivableAccounts(tenantId);
const receivableAccountsIds = map(receivableAccounts, 'id');
const openingTransactions = await AccountTransaction.query().modify(
'contactsOpeningBalance',
openingDate,
receivableAccountsIds,
customersIds
);
return R.compose(
R.map(R.assoc('date', openingDate)),
R.map(R.assoc('accountNormal', 'debit'))
@@ -88,38 +73,29 @@ export default class TransactionsByCustomersService
}
/**
*
* Retrieve the customers periods ledger entries.
* @param {number} tenantId
* @param {Date|string} openingDate
* @param {number[]} customersIds
* @param {Date} fromDate
* @param {Date} toDate
* @returns {Promise<ILedgerEntry[]>}
*/
async getCustomersPeriodTransactions(
private async getCustomersPeriodsEntries(
tenantId: number,
fromDate: Date,
toDate: Date
fromDate: Date|string,
toDate: Date|string,
): Promise<ILedgerEntry[]> {
const { AccountTransaction } = this.tenancy.models(tenantId);
const receivableAccounts = await this.getReceivableAccounts(tenantId);
const receivableAccountsIds = map(receivableAccounts, 'id');
const transactions = await AccountTransaction.query().onBuild((query) => {
// Filter by date.
query.modify('filterDateRange', fromDate, toDate);
// Filter by customers.
query.whereNot('contactId', null);
// Filter by accounts.
query.whereIn('accountId', receivableAccountsIds);
});
const transactions =
await this.reportRepository.getCustomersPeriodTransactions(
tenantId,
fromDate,
toDate
);
return R.compose(
R.map(R.assoc('accountNormal', 'debit')),
R.map((trans) => ({
...trans,
referenceTypeFormatted: trans.referenceTypeFormatted,
})),
}))
)(transactions);
}
@@ -133,7 +109,6 @@ export default class TransactionsByCustomersService
tenantId: number,
query: ITransactionsByCustomersFilter
): Promise<ITransactionsByCustomersStatement> {
const { Customer } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
// Settings tenant service.
@@ -148,29 +123,31 @@ export default class TransactionsByCustomersService
...query,
};
const accountsGraph = await accountRepository.getDependencyGraph();
const customers = await Customer.query().orderBy('displayName');
// Retrieve the report customers.
const customers = await this.reportRepository.getCustomers(tenantId);
const openingBalanceDate = moment(filter.fromDate)
.subtract(1, 'days')
.toDate();
// Retrieve all ledger transactions of the opening balance of.
const openingBalanceTransactions = await this.getCustomersOpeningBalance(
const openingBalanceEntries = await this.getCustomersOpeningBalanceEntries(
tenantId,
openingBalanceDate
);
// Retrieve all ledger transactions between opeing and closing period.
const customersTransactions = await this.getCustomersPeriodTransactions(
const customersTransactions = await this.getCustomersPeriodsEntries(
tenantId,
query.fromDate,
query.toDate
);
// Concats the opening balance and period customer ledger transactions.
const journalTransactions = [
...openingBalanceTransactions,
...openingBalanceEntries,
...customersTransactions,
];
const journal = Ledger.fromTransactions(journalTransactions);
const journal = new Ledger(journalTransactions);
// Transactions by customers data mapper.
const reportInstance = new TransactionsByCustomers(

View File

@@ -0,0 +1,92 @@
import { Inject, Service } from 'typedi';
import { map } from 'lodash';
import { IVendor, IAccount, IAccountTransaction } from 'interfaces';
import HasTenancyService from 'services/Tenancy/TenancyService';
import { ACCOUNT_TYPE } from 'data/AccountTypes';
@Service()
export default class TransactionsByVendorRepository {
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve the report vendors.
* @param {number} tenantId
* @returns {Promise<IVendor[]>}
*/
public getVendors(tenantId: number): Promise<IVendor[]> {
const { Vendor } = this.tenancy.models(tenantId);
return Vendor.query().orderBy('displayName');
}
/**
* Retrieve the accounts receivable.
* @param {number} tenantId
* @returns {Promise<IAccount[]>}
*/
private async getPayableAccounts(tenantId: number): Promise<IAccount[]> {
const { Account } = this.tenancy.models(tenantId);
const accounts = await Account.query().where(
'accountType',
ACCOUNT_TYPE.ACCOUNTS_PAYABLE
);
return accounts;
}
/**
* Retrieve the customers opening balance transactions.
* @param {number} tenantId
* @param {number} openingDate
* @param {number} customersIds
* @returns {}
*/
public async getVendorsOpeningBalance(
tenantId: number,
openingDate: Date,
customersIds?: number[]
): Promise<IAccountTransaction[]> {
const { AccountTransaction } = this.tenancy.models(tenantId);
const payableAccounts = await this.getPayableAccounts(tenantId);
const payableAccountsIds = map(payableAccounts, 'id');
const openingTransactions = await AccountTransaction.query().modify(
'contactsOpeningBalance',
openingDate,
payableAccountsIds,
customersIds
);
return openingTransactions;
}
/**
* Retrieve vendors periods transactions.
* @param {number} tenantId
* @param {Date|string} openingDate
* @param {number[]} customersIds
*/
public async getVendorsPeriodTransactions(
tenantId: number,
fromDate: Date,
toDate: Date
): Promise<IAccountTransaction[]> {
const { AccountTransaction } = this.tenancy.models(tenantId);
const receivableAccounts = await this.getPayableAccounts(tenantId);
const receivableAccountsIds = map(receivableAccounts, 'id');
const transactions = await AccountTransaction.query().onBuild((query) => {
// Filter by date.
query.modify('filterDateRange', fromDate, toDate);
// Filter by customers.
query.whereNot('contactId', null);
// Filter by accounts.
query.whereIn('accountId', receivableAccountsIds);
});
return transactions;
}
}

View File

@@ -4,23 +4,27 @@ import * as R from 'ramda';
import { map } from 'lodash';
import TenancyService from 'services/Tenancy/TenancyService';
import {
IVendor,
ITransactionsByVendorsService,
ITransactionsByVendorsFilter,
ITransactionsByVendorsStatement,
ILedgerEntry,
} from 'interfaces';
import TransactionsByVendor from './TransactionsByVendor';
import { ACCOUNT_TYPE } from 'data/AccountTypes';
import Ledger from 'services/Accounting/Ledger';
import TransactionsByVendorRepository from './TransactionsByVendorRepository';
export default class TransactionsByVendorsService
implements ITransactionsByVendorsService {
implements ITransactionsByVendorsService
{
@Inject()
tenancy: TenancyService;
@Inject('logger')
logger: any;
@Inject()
reportRepository: TransactionsByVendorRepository;
/**
* Defaults balance sheet filter query.
* @return {IVendorBalanceSummaryQuery}
@@ -44,55 +48,24 @@ export default class TransactionsByVendorsService
};
}
/**
* Retrieve the report vendors.
* @param tenantId
* @returns
*/
private getReportVendors(tenantId: number): Promise<IVendor[]> {
const { Vendor } = this.tenancy.models(tenantId);
return Vendor.query().orderBy('displayName');
}
/**
* Retrieve the accounts receivable.
* @param {number} tenantId
* @returns
*/
private async getPayableAccounts(tenantId: number) {
const { Account } = this.tenancy.models(tenantId);
const accounts = await Account.query().where(
'accountType',
ACCOUNT_TYPE.ACCOUNTS_PAYABLE
);
return accounts;
}
/**
* Retrieve the customers opening balance transactions.
* @param {number} tenantId
* @param {number} openingDate
* @param {number} customersIds
* @returns {}
* @returns {Promise<ILedgerEntry[]>}
*/
private async getVendorsOpeningBalance(
private async getVendorsOpeningBalanceEntries(
tenantId: number,
openingDate: Date,
customersIds?: number[]
): Promise<ILedgerEntry[]> {
const { AccountTransaction } = this.tenancy.models(tenantId);
const payableAccounts = await this.getPayableAccounts(tenantId);
const payableAccountsIds = map(payableAccounts, 'id');
const openingTransactions = await AccountTransaction.query().modify(
'contactsOpeningBalance',
openingDate,
payableAccountsIds,
customersIds
);
const openingTransactions =
await this.reportRepository.getVendorsOpeningBalance(
tenantId,
openingDate,
customersIds
);
return R.compose(
R.map(R.assoc('date', openingDate)),
R.map(R.assoc('accountNormal', 'credit'))
@@ -105,42 +78,46 @@ export default class TransactionsByVendorsService
* @param {Date|string} openingDate
* @param {number[]} customersIds
*/
async getVendorsPeriodTransactions(
private async getVendorsPeriodEntries(
tenantId: number,
fromDate: Date,
toDate: Date
): Promise<ILedgerEntry[]> {
const { AccountTransaction } = this.tenancy.models(tenantId);
const receivableAccounts = await this.getPayableAccounts(tenantId);
const receivableAccountsIds = map(receivableAccounts, 'id');
const transactions = await AccountTransaction.query().onBuild((query) => {
// Filter by date.
query.modify('filterDateRange', fromDate, toDate);
// Filter by customers.
query.whereNot('contactId', null);
// Filter by accounts.
query.whereIn('accountId', receivableAccountsIds);
});
const transactions =
await this.reportRepository.getVendorsPeriodTransactions(
tenantId,
fromDate,
toDate
);
return R.compose(
R.map(R.assoc('accountNormal', 'credit')),
R.map((trans) => ({
...trans,
referenceTypeFormatted: trans.referenceTypeFormatted,
})),
}))
)(transactions);
}
async getReportTransactions(tenantId: number, fromDate: Date, toDate: Date) {
/**
* Retrieve the report ledger entries from repository.
* @param {number} tenantId
* @param {Date} fromDate
* @param {Date} toDate
* @returns {Promise<ILedgerEntry[]>}
*/
private async getReportEntries(
tenantId: number,
fromDate: Date,
toDate: Date
): Promise<ILedgerEntry[]> {
const openingBalanceDate = moment(fromDate).subtract(1, 'days').toDate();
return [
...(await this.getVendorsOpeningBalance(tenantId, openingBalanceDate)),
...(await this.getVendorsPeriodTransactions(tenantId, fromDate, toDate)),
...(await this.getVendorsOpeningBalanceEntries(
tenantId,
openingBalanceDate
)),
...(await this.getVendorsPeriodEntries(tenantId, fromDate, toDate)),
];
}
@@ -155,7 +132,7 @@ export default class TransactionsByVendorsService
query: ITransactionsByVendorsFilter
): Promise<ITransactionsByVendorsStatement> {
const { accountRepository } = this.tenancy.repositories(tenantId);
// Settings tenant service.
const settings = this.tenancy.settings(tenantId);
const baseCurrency = settings.get({
@@ -166,19 +143,19 @@ export default class TransactionsByVendorsService
const filter = { ...this.defaultQuery, ...query };
// Retrieve the report vendors.
const vendors = await this.getReportVendors(tenantId);
const vendors = await this.reportRepository.getVendors(tenantId);
// Retrieve the accounts graph.
const accountsGraph = await accountRepository.getDependencyGraph();
// Journal transactions.
const journalTransactions = await this.getReportTransactions(
const reportEntries = await this.getReportEntries(
tenantId,
filter.fromDate,
filter.toDate
);
// Ledger collection.
const journal = Ledger.fromTransactions(journalTransactions);
const journal = new Ledger(reportEntries);
// Transactions by customers data mapper.
const reportInstance = new TransactionsByVendor(

View File

@@ -0,0 +1,69 @@
import { Inject, Service } from 'typedi';
import { isEmpty, map } from 'lodash';
import { IVendor, IAccount } from 'interfaces';
import HasTenancyService from 'services/Tenancy/TenancyService';
import { ACCOUNT_TYPE } from 'data/AccountTypes';
@Service()
export default class VendorBalanceSummaryRepository {
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve the report vendors.
* @param {number} tenantId
* @param {number[]} vendorsIds - Vendors ids.
* @returns {IVendor[]}
*/
public getVendors(
tenantId: number,
vendorsIds?: number[]
): Promise<IVendor[]> {
const { Vendor } = this.tenancy.models(tenantId);
const vendorQuery = Vendor.query().orderBy('displayName');
if (!isEmpty(vendorsIds)) {
vendorQuery.whereIn('id', vendorsIds);
}
return vendorQuery;
}
/**
* Retrieve the payable accounts.
* @param {number} tenantId
* @returns {Promise<IAccount[]>}
*/
public getPayableAccounts(tenantId: number): Promise<IAccount[]> {
const { Account } = this.tenancy.models(tenantId);
return Account.query().where('accountType', ACCOUNT_TYPE.ACCOUNTS_PAYABLE);
}
/**
* Retrieve the vendors transactions.
* @param {number} tenantId
* @param {Date} asDate
* @returns
*/
public async getVendorsTransactions(tenantId: number, asDate: Date | string) {
const { AccountTransaction } = this.tenancy.models(tenantId);
// Retrieve payable accounts .
const payableAccounts = await this.getPayableAccounts(tenantId);
const payableAccountsIds = map(payableAccounts, 'id');
// Retrieve the customers transactions of A/R accounts.
const customersTranasctions = await AccountTransaction.query().onBuild(
(query) => {
query.whereIn('accountId', payableAccountsIds);
query.modify('filterDateRange', null, asDate);
query.groupBy('contactId');
query.sum('credit as credit');
query.sum('debit as debit');
query.select('contactId');
}
);
return customersTranasctions;
}
}

View File

@@ -8,20 +8,24 @@ import {
IVendorBalanceSummaryService,
IVendorBalanceSummaryQuery,
IVendorBalanceSummaryStatement,
ILedgerEntry,
} from 'interfaces';
import { VendorBalanceSummaryReport } from './VendorBalanceSummary';
import { isEmpty } from 'lodash';
import { ACCOUNT_TYPE } from 'data/AccountTypes';
import Ledger from 'services/Accounting/Ledger';
import VendorBalanceSummaryRepository from './VendorBalanceSummaryRepository';
export default class VendorBalanceSummaryService
implements IVendorBalanceSummaryService {
implements IVendorBalanceSummaryService
{
@Inject()
tenancy: TenancyService;
@Inject('logger')
logger: any;
@Inject()
reportRepo: VendorBalanceSummaryRepository;
/**
* Defaults balance sheet filter query.
* @return {IVendorBalanceSummaryQuery}
@@ -45,59 +49,22 @@ export default class VendorBalanceSummaryService
}
/**
* Retrieve the report vendors.
* @param {number} tenantId
* @param {number[]} vendorsIds - Vendors ids.
* @returns {IVendor[]}
* Retrieve the vendors ledger entrjes.
* @param {number} tenantId -
* @param {Date|string} date -
* @returns {Promise<ILedgerEntry>}
*/
getReportVendors(
private async getReportVendorsEntries(
tenantId: number,
vendorsIds?: number[]
): Promise<IVendor[]> {
const { Vendor } = this.tenancy.models(tenantId);
return Vendor.query()
.orderBy('displayName')
.onBuild((query) => {
if (!isEmpty(vendorsIds)) {
query.whereIn('id', vendorsIds);
}
});
}
getPayableAccounts(tenantId: number) {
const { Account } = this.tenancy.models(tenantId);
return Account.query().where('accountType', ACCOUNT_TYPE.ACCOUNTS_PAYABLE);
}
/**
* Retrieve
* @param tenantId
* @param asDate
* @returns
*/
async getReportVendorsTransactions(tenantId: number, asDate: Date | string) {
const { AccountTransaction } = this.tenancy.models(tenantId);
// Retrieve payable accounts .
const payableAccounts = await this.getPayableAccounts(tenantId);
const payableAccountsIds = map(payableAccounts, 'id');
// Retrieve the customers transactions of A/R accounts.
const customersTranasctions = await AccountTransaction.query().onBuild(
(query) => {
query.whereIn('accountId', payableAccountsIds);
query.modify('filterDateRange', null, asDate);
query.groupBy('contactId');
query.sum('credit as credit');
query.sum('debit as debit');
query.select('contactId');
}
date: Date | string
): Promise<ILedgerEntry[]> {
const transactions = await this.reportRepo.getVendorsTransactions(
tenantId,
date
);
const commonProps = { accountNormal: 'credit', date: asDate };
const commonProps = { accountNormal: 'credit' };
return R.map(R.merge(commonProps))(customersTranasctions);
return R.map(R.merge(commonProps))(transactions);
}
/**
@@ -126,19 +93,21 @@ export default class VendorBalanceSummaryService
}
);
// Retrieve the vendors transactions.
const vendorsTransactions = await this.getReportVendorsTransactions(
const vendorsEntries = await this.getReportVendorsEntries(
tenantId,
query.asDate
);
// Retrieve the customers list ordered by the display name.
const vendors = await this.getReportVendors(tenantId, query.vendorsIds);
const vendors = await this.reportRepo.getVendors(
tenantId,
query.vendorsIds
);
// Ledger query.
const ledger = Ledger.fromTransactions(vendorsTransactions);
const vendorsLedger = new Ledger(vendorsEntries);
// Report instance.
const reportInstance = new VendorBalanceSummaryReport(
ledger,
vendorsLedger,
vendors,
filter,
baseCurrency