mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
add server to monorepo.
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import moment from 'moment';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { IAPAgingSummaryQuery, IARAgingSummaryMeta } from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import APAgingSummarySheet from './APAgingSummarySheet';
|
||||
import { Tenant } from '@/system/models';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
@Service()
|
||||
export default class PayableAgingSummaryService {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Default report query.
|
||||
*/
|
||||
get defaultQuery(): IAPAgingSummaryQuery {
|
||||
return {
|
||||
asDate: moment().format('YYYY-MM-DD'),
|
||||
agingDaysBefore: 30,
|
||||
agingPeriods: 3,
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
divideOn1000: false,
|
||||
showZero: false,
|
||||
formatMoney: 'total',
|
||||
negativeFormat: 'mines',
|
||||
},
|
||||
vendorsIds: [],
|
||||
branchesIds: [],
|
||||
noneZero: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the balance sheet meta.
|
||||
* @param {number} tenantId -
|
||||
* @returns {IBalanceSheetMeta}
|
||||
*/
|
||||
reportMetadata(tenantId: number): IARAgingSummaryMeta {
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
|
||||
const organizationName = settings.get({
|
||||
group: 'organization',
|
||||
key: 'name',
|
||||
});
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
|
||||
return {
|
||||
organizationName,
|
||||
baseCurrency,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve A/P aging summary report.
|
||||
* @param {number} tenantId -
|
||||
* @param {IAPAgingSummaryQuery} query -
|
||||
*/
|
||||
async APAgingSummary(tenantId: number, query: IAPAgingSummaryQuery) {
|
||||
const { Bill } = this.tenancy.models(tenantId);
|
||||
const { vendorRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
// Settings tenant service.
|
||||
const tenant = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
// Retrieve all vendors from the storage.
|
||||
const vendors =
|
||||
filter.vendorsIds.length > 0
|
||||
? await vendorRepository.findWhereIn('id', filter.vendorsIds)
|
||||
: await vendorRepository.all();
|
||||
|
||||
// Common query.
|
||||
const commonQuery = (query) => {
|
||||
if (isEmpty(filter.branchesIds)) {
|
||||
query.modify('filterByBranches', filter.branchesIds);
|
||||
}
|
||||
};
|
||||
// Retrieve all overdue vendors bills.
|
||||
const overdueBills = await Bill.query()
|
||||
.modify('overdueBillsFromDate', filter.asDate)
|
||||
.onBuild(commonQuery);
|
||||
|
||||
// Retrieve all due vendors bills.
|
||||
const dueBills = await Bill.query()
|
||||
.modify('dueBillsFromDate', filter.asDate)
|
||||
.onBuild(commonQuery);
|
||||
|
||||
// A/P aging summary report instance.
|
||||
const APAgingSummaryReport = new APAgingSummarySheet(
|
||||
tenantId,
|
||||
filter,
|
||||
vendors,
|
||||
overdueBills,
|
||||
dueBills,
|
||||
tenant.metadata.baseCurrency
|
||||
);
|
||||
// A/P aging summary report data and columns.
|
||||
const data = APAgingSummaryReport.reportData();
|
||||
const columns = APAgingSummaryReport.reportColumns();
|
||||
|
||||
return {
|
||||
data,
|
||||
columns,
|
||||
query: filter,
|
||||
meta: this.reportMetadata(tenantId),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { groupBy, sum, isEmpty } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import AgingSummaryReport from './AgingSummary';
|
||||
import {
|
||||
IAPAgingSummaryQuery,
|
||||
IAgingPeriod,
|
||||
IBill,
|
||||
IVendor,
|
||||
IAPAgingSummaryData,
|
||||
IAPAgingSummaryVendor,
|
||||
IAPAgingSummaryColumns,
|
||||
IAPAgingSummaryTotal,
|
||||
} from '@/interfaces';
|
||||
import { Dictionary } from 'tsyringe/dist/typings/types';
|
||||
import { allPassedConditionsPass } from 'utils';
|
||||
|
||||
export default class APAgingSummarySheet extends AgingSummaryReport {
|
||||
readonly tenantId: number;
|
||||
readonly query: IAPAgingSummaryQuery;
|
||||
readonly contacts: IVendor[];
|
||||
readonly unpaidBills: IBill[];
|
||||
readonly baseCurrency: string;
|
||||
|
||||
readonly overdueInvoicesByContactId: Dictionary<IBill[]>;
|
||||
readonly currentInvoicesByContactId: Dictionary<IBill[]>;
|
||||
|
||||
readonly agingPeriods: IAgingPeriod[];
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {IAPAgingSummaryQuery} query - Report query.
|
||||
* @param {IVendor[]} vendors - Unpaid bills.
|
||||
* @param {string} baseCurrency - Base currency of the organization.
|
||||
*/
|
||||
constructor(
|
||||
tenantId: number,
|
||||
query: IAPAgingSummaryQuery,
|
||||
vendors: IVendor[],
|
||||
overdueBills: IBill[],
|
||||
unpaidBills: IBill[],
|
||||
baseCurrency: string
|
||||
) {
|
||||
super();
|
||||
|
||||
this.tenantId = tenantId;
|
||||
this.query = query;
|
||||
this.numberFormat = this.query.numberFormat;
|
||||
this.contacts = vendors;
|
||||
this.baseCurrency = baseCurrency;
|
||||
|
||||
this.overdueInvoicesByContactId = groupBy(overdueBills, 'vendorId');
|
||||
this.currentInvoicesByContactId = groupBy(unpaidBills, 'vendorId');
|
||||
|
||||
// Initializes the aging periods.
|
||||
this.agingPeriods = this.agingRangePeriods(
|
||||
this.query.asDate,
|
||||
this.query.agingDaysBefore,
|
||||
this.query.agingPeriods
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the vendors aging and current total.
|
||||
* @param {IAPAgingSummaryTotal} vendorsAgingPeriods
|
||||
* @return {IAPAgingSummaryTotal}
|
||||
*/
|
||||
private getVendorsTotal = (vendorsAgingPeriods): IAPAgingSummaryTotal => {
|
||||
const totalAgingPeriods = this.getTotalAgingPeriods(vendorsAgingPeriods);
|
||||
const totalCurrent = this.getTotalCurrent(vendorsAgingPeriods);
|
||||
const totalVendorsTotal = this.getTotalContactsTotals(vendorsAgingPeriods);
|
||||
|
||||
return {
|
||||
current: this.formatTotalAmount(totalCurrent),
|
||||
aging: totalAgingPeriods,
|
||||
total: this.formatTotalAmount(totalVendorsTotal),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the vendor section data.
|
||||
* @param {IVendor} vendor
|
||||
* @return {IAPAgingSummaryVendor}
|
||||
*/
|
||||
private vendorTransformer = (vendor: IVendor): IAPAgingSummaryVendor => {
|
||||
const agingPeriods = this.getContactAgingPeriods(vendor.id);
|
||||
const currentTotal = this.getContactCurrentTotal(vendor.id);
|
||||
const agingPeriodsTotal = this.getAgingPeriodsTotal(agingPeriods);
|
||||
|
||||
const amount = sum([agingPeriodsTotal, currentTotal]);
|
||||
|
||||
return {
|
||||
vendorName: vendor.displayName,
|
||||
current: this.formatTotalAmount(currentTotal),
|
||||
aging: agingPeriods,
|
||||
total: this.formatTotalAmount(amount),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Mappes the given vendor objects to vendor report node.
|
||||
* @param {IVendor[]} vendors
|
||||
* @returns {IAPAgingSummaryVendor[]}
|
||||
*/
|
||||
private vendorsMapper = (vendors: IVendor[]): IAPAgingSummaryVendor[] => {
|
||||
return vendors.map(this.vendorTransformer);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether the given vendor node is none zero.
|
||||
* @param {IAPAgingSummaryVendor} vendorNode
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private filterNoneZeroVendorNode = (
|
||||
vendorNode: IAPAgingSummaryVendor
|
||||
): boolean => {
|
||||
return vendorNode.total.amount !== 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters vendors report nodes based on the given report query.
|
||||
* @param {IAPAgingSummaryVendor} vendorNode
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private vendorNodeFilter = (vendorNode: IAPAgingSummaryVendor): boolean => {
|
||||
const { noneZero } = this.query;
|
||||
|
||||
const conditions = [[noneZero, this.filterNoneZeroVendorNode]];
|
||||
|
||||
return allPassedConditionsPass(conditions)(vendorNode);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filtesr the given report vendors nodes.
|
||||
* @param {IAPAgingSummaryVendor[]} vendorNodes
|
||||
* @returns {IAPAgingSummaryVendor[]}
|
||||
*/
|
||||
private vendorsFilter = (
|
||||
vendorNodes: IAPAgingSummaryVendor[]
|
||||
): IAPAgingSummaryVendor[] => {
|
||||
return vendorNodes.filter(this.vendorNodeFilter);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether vendors nodes filter enabled.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private isVendorNodesFilter = (): boolean => {
|
||||
return isEmpty(this.query.vendorsIds);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve vendors aging periods.
|
||||
* @return {IAPAgingSummaryVendor[]}
|
||||
*/
|
||||
private vendorsSection = (vendors: IVendor[]): IAPAgingSummaryVendor[] => {
|
||||
return R.compose(
|
||||
R.when(this.isVendorNodesFilter, this.vendorsFilter),
|
||||
this.vendorsMapper
|
||||
)(vendors);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the A/P aging summary report data.
|
||||
* @return {IAPAgingSummaryData}
|
||||
*/
|
||||
public reportData = (): IAPAgingSummaryData => {
|
||||
const vendorsAgingPeriods = this.vendorsSection(this.contacts);
|
||||
const vendorsTotal = this.getVendorsTotal(vendorsAgingPeriods);
|
||||
|
||||
return {
|
||||
vendors: vendorsAgingPeriods,
|
||||
total: vendorsTotal,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the A/P aging summary report columns.
|
||||
*/
|
||||
public reportColumns = (): IAPAgingSummaryColumns => {
|
||||
return this.agingPeriods;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import moment from 'moment';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { IARAgingSummaryQuery, IARAgingSummaryMeta } from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import ARAgingSummarySheet from './ARAgingSummarySheet';
|
||||
import { Tenant } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export default class ARAgingSummaryService {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Default report query.
|
||||
*/
|
||||
get defaultQuery(): IARAgingSummaryQuery {
|
||||
return {
|
||||
asDate: moment().format('YYYY-MM-DD'),
|
||||
agingDaysBefore: 30,
|
||||
agingPeriods: 3,
|
||||
numberFormat: {
|
||||
divideOn1000: false,
|
||||
negativeFormat: 'mines',
|
||||
showZero: false,
|
||||
formatMoney: 'total',
|
||||
precision: 2,
|
||||
},
|
||||
customersIds: [],
|
||||
branchesIds: [],
|
||||
noneZero: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the balance sheet meta.
|
||||
* @param {number} tenantId -
|
||||
* @returns {IBalanceSheetMeta}
|
||||
*/
|
||||
reportMetadata(tenantId: number): IARAgingSummaryMeta {
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
|
||||
const organizationName = settings.get({
|
||||
group: 'organization',
|
||||
key: 'name',
|
||||
});
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
|
||||
return {
|
||||
organizationName,
|
||||
baseCurrency,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve A/R aging summary report.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {IARAgingSummaryQuery} query -
|
||||
*/
|
||||
async ARAgingSummary(tenantId: number, query: IARAgingSummaryQuery) {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
const { customerRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
const tenant = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
// Retrieve all customers from the storage.
|
||||
const customers =
|
||||
filter.customersIds.length > 0
|
||||
? await customerRepository.findWhereIn('id', filter.customersIds)
|
||||
: await customerRepository.all();
|
||||
|
||||
// Common query.
|
||||
const commonQuery = (query) => {
|
||||
if (!isEmpty(filter.branchesIds)) {
|
||||
query.modify('filterByBranches', filter.branchesIds);
|
||||
}
|
||||
};
|
||||
// Retrieve all overdue sale invoices.
|
||||
const overdueSaleInvoices = await SaleInvoice.query()
|
||||
.modify('dueInvoicesFromDate', filter.asDate)
|
||||
.onBuild(commonQuery);
|
||||
|
||||
// Retrieve all due sale invoices.
|
||||
const currentInvoices = await SaleInvoice.query()
|
||||
.modify('overdueInvoicesFromDate', filter.asDate)
|
||||
.onBuild(commonQuery);
|
||||
|
||||
// AR aging summary report instance.
|
||||
const ARAgingSummaryReport = new ARAgingSummarySheet(
|
||||
tenantId,
|
||||
filter,
|
||||
customers,
|
||||
overdueSaleInvoices,
|
||||
currentInvoices,
|
||||
tenant.metadata.baseCurrency
|
||||
);
|
||||
// AR aging summary report data and columns.
|
||||
const data = ARAgingSummaryReport.reportData();
|
||||
const columns = ARAgingSummaryReport.reportColumns();
|
||||
|
||||
return {
|
||||
data,
|
||||
columns,
|
||||
query: filter,
|
||||
meta: this.reportMetadata(tenantId),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import { groupBy, isEmpty, sum } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
ICustomer,
|
||||
IARAgingSummaryQuery,
|
||||
IARAgingSummaryCustomer,
|
||||
IAgingPeriod,
|
||||
ISaleInvoice,
|
||||
IARAgingSummaryData,
|
||||
IARAgingSummaryColumns,
|
||||
IARAgingSummaryTotal,
|
||||
} from '@/interfaces';
|
||||
import AgingSummaryReport from './AgingSummary';
|
||||
import { allPassedConditionsPass } from '../../../utils';
|
||||
|
||||
export default class ARAgingSummarySheet extends AgingSummaryReport {
|
||||
readonly tenantId: number;
|
||||
readonly query: IARAgingSummaryQuery;
|
||||
readonly contacts: ICustomer[];
|
||||
readonly agingPeriods: IAgingPeriod[];
|
||||
readonly baseCurrency: string;
|
||||
|
||||
readonly overdueInvoicesByContactId: Dictionary<ISaleInvoice[]>;
|
||||
readonly currentInvoicesByContactId: Dictionary<ISaleInvoice[]>;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId
|
||||
* @param {IARAgingSummaryQuery} query
|
||||
* @param {ICustomer[]} customers
|
||||
* @param {IJournalPoster} journal
|
||||
*/
|
||||
constructor(
|
||||
tenantId: number,
|
||||
query: IARAgingSummaryQuery,
|
||||
customers: ICustomer[],
|
||||
overdueSaleInvoices: ISaleInvoice[],
|
||||
currentSaleInvoices: ISaleInvoice[],
|
||||
baseCurrency: string
|
||||
) {
|
||||
super();
|
||||
|
||||
this.tenantId = tenantId;
|
||||
this.contacts = customers;
|
||||
this.query = query;
|
||||
this.baseCurrency = baseCurrency;
|
||||
this.numberFormat = this.query.numberFormat;
|
||||
|
||||
this.overdueInvoicesByContactId = groupBy(
|
||||
overdueSaleInvoices,
|
||||
'customerId'
|
||||
);
|
||||
this.currentInvoicesByContactId = groupBy(
|
||||
currentSaleInvoices,
|
||||
'customerId'
|
||||
);
|
||||
|
||||
// Initializes the aging periods.
|
||||
this.agingPeriods = this.agingRangePeriods(
|
||||
this.query.asDate,
|
||||
this.query.agingDaysBefore,
|
||||
this.query.agingPeriods
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping aging customer.
|
||||
* @param {ICustomer} customer -
|
||||
* @return {IARAgingSummaryCustomer[]}
|
||||
*/
|
||||
private customerTransformer = (
|
||||
customer: ICustomer
|
||||
): IARAgingSummaryCustomer => {
|
||||
const agingPeriods = this.getContactAgingPeriods(customer.id);
|
||||
const currentTotal = this.getContactCurrentTotal(customer.id);
|
||||
const agingPeriodsTotal = this.getAgingPeriodsTotal(agingPeriods);
|
||||
const amount = sum([agingPeriodsTotal, currentTotal]);
|
||||
|
||||
return {
|
||||
customerName: customer.displayName,
|
||||
current: this.formatAmount(currentTotal),
|
||||
aging: agingPeriods,
|
||||
total: this.formatTotalAmount(amount),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Mappes the customers objects to report accounts nodes.
|
||||
* @param {ICustomer[]} customers
|
||||
* @returns {IARAgingSummaryCustomer[]}
|
||||
*/
|
||||
private customersMapper = (
|
||||
customers: ICustomer[]
|
||||
): IARAgingSummaryCustomer[] => {
|
||||
return customers.map(this.customerTransformer);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters the none-zero account report node.
|
||||
* @param {IARAgingSummaryCustomer} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private filterNoneZeroAccountNode = (
|
||||
node: IARAgingSummaryCustomer
|
||||
): boolean => {
|
||||
return node.total.amount !== 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters customer report node based on the given report query.
|
||||
* @param {IARAgingSummaryCustomer} customerNode
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private customerNodeFilter = (
|
||||
customerNode: IARAgingSummaryCustomer
|
||||
): boolean => {
|
||||
const { noneZero } = this.query;
|
||||
|
||||
const conditions = [[noneZero, this.filterNoneZeroAccountNode]];
|
||||
|
||||
return allPassedConditionsPass(conditions)(customerNode);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters customers report nodes.
|
||||
* @param {IARAgingSummaryCustomer[]} customers
|
||||
* @returns {IARAgingSummaryCustomer[]}
|
||||
*/
|
||||
private customersFilter = (
|
||||
customers: IARAgingSummaryCustomer[]
|
||||
): IARAgingSummaryCustomer[] => {
|
||||
return customers.filter(this.customerNodeFilter);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines the customers nodes filter is enabled.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private isCustomersFilterEnabled = (): boolean => {
|
||||
return isEmpty(this.query.customersIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve customers report.
|
||||
* @param {ICustomer[]} customers
|
||||
* @return {IARAgingSummaryCustomer[]}
|
||||
*/
|
||||
private customersWalker = (
|
||||
customers: ICustomer[]
|
||||
): IARAgingSummaryCustomer[] => {
|
||||
return R.compose(
|
||||
R.when(this.isCustomersFilterEnabled, this.customersFilter),
|
||||
this.customersMapper
|
||||
)(customers);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the customers aging and current total.
|
||||
* @param {IARAgingSummaryCustomer} customersAgingPeriods
|
||||
*/
|
||||
private getCustomersTotal = (
|
||||
customersAgingPeriods: IARAgingSummaryCustomer[]
|
||||
): IARAgingSummaryTotal => {
|
||||
const totalAgingPeriods = this.getTotalAgingPeriods(customersAgingPeriods);
|
||||
const totalCurrent = this.getTotalCurrent(customersAgingPeriods);
|
||||
const totalCustomersTotal = this.getTotalContactsTotals(
|
||||
customersAgingPeriods
|
||||
);
|
||||
|
||||
return {
|
||||
current: this.formatTotalAmount(totalCurrent),
|
||||
aging: totalAgingPeriods,
|
||||
total: this.formatTotalAmount(totalCustomersTotal),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve A/R aging summary report data.
|
||||
* @return {IARAgingSummaryData}
|
||||
*/
|
||||
public reportData = (): IARAgingSummaryData => {
|
||||
const customersAgingPeriods = this.customersWalker(this.contacts);
|
||||
const customersTotal = this.getCustomersTotal(customersAgingPeriods);
|
||||
|
||||
return {
|
||||
customers: customersAgingPeriods,
|
||||
total: customersTotal,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve AR aging summary report columns.
|
||||
* @return {IARAgingSummaryColumns}
|
||||
*/
|
||||
public reportColumns(): IARAgingSummaryColumns {
|
||||
return this.agingPeriods;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import moment from 'moment';
|
||||
import {
|
||||
IAgingPeriod,
|
||||
} from '@/interfaces';
|
||||
import FinancialSheet from "../FinancialSheet";
|
||||
|
||||
|
||||
export default abstract class AgingReport extends FinancialSheet{
|
||||
/**
|
||||
* Retrieve the aging periods range.
|
||||
* @param {string} asDay
|
||||
* @param {number} agingDaysBefore
|
||||
* @param {number} agingPeriodsFreq
|
||||
*/
|
||||
agingRangePeriods(
|
||||
asDay: Date|string,
|
||||
agingDaysBefore: number,
|
||||
agingPeriodsFreq: number
|
||||
): IAgingPeriod[] {
|
||||
const totalAgingDays = agingDaysBefore * agingPeriodsFreq;
|
||||
const startAging = moment(asDay).startOf('day');
|
||||
const endAging = startAging
|
||||
.clone()
|
||||
.subtract(totalAgingDays, 'days')
|
||||
.endOf('day');
|
||||
|
||||
const agingPeriods: IAgingPeriod[] = [];
|
||||
const startingAging = startAging.clone();
|
||||
|
||||
let beforeDays = 1;
|
||||
let toDays = 0;
|
||||
|
||||
while (startingAging > endAging) {
|
||||
const currentAging = startingAging.clone();
|
||||
startingAging.subtract(agingDaysBefore, 'days').endOf('day');
|
||||
toDays += agingDaysBefore;
|
||||
|
||||
agingPeriods.push({
|
||||
fromPeriod: moment(currentAging).format('YYYY-MM-DD'),
|
||||
toPeriod: moment(startingAging).format('YYYY-MM-DD'),
|
||||
beforeDays: beforeDays === 1 ? 0 : beforeDays,
|
||||
toDays: toDays,
|
||||
...(startingAging.valueOf() === endAging.valueOf()
|
||||
? {
|
||||
toPeriod: null,
|
||||
toDays: null,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
beforeDays += agingDaysBefore;
|
||||
}
|
||||
return agingPeriods;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import { defaultTo, sumBy, get } from 'lodash';
|
||||
import {
|
||||
IAgingPeriod,
|
||||
ISaleInvoice,
|
||||
IBill,
|
||||
IAgingPeriodTotal,
|
||||
IARAgingSummaryCustomer,
|
||||
IContact,
|
||||
IARAgingSummaryQuery,
|
||||
IFormatNumberSettings,
|
||||
IAgingAmount,
|
||||
IAgingSummaryContact,
|
||||
} from '@/interfaces';
|
||||
import AgingReport from './AgingReport';
|
||||
import { Dictionary } from 'tsyringe/dist/typings/types';
|
||||
|
||||
export default abstract class AgingSummaryReport extends AgingReport {
|
||||
protected readonly contacts: IContact[];
|
||||
protected readonly agingPeriods: IAgingPeriod[] = [];
|
||||
protected readonly baseCurrency: string;
|
||||
protected readonly query: IARAgingSummaryQuery;
|
||||
protected readonly overdueInvoicesByContactId: Dictionary<
|
||||
(ISaleInvoice | IBill)[]
|
||||
>;
|
||||
protected readonly currentInvoicesByContactId: Dictionary<
|
||||
(ISaleInvoice | IBill)[]
|
||||
>;
|
||||
|
||||
/**
|
||||
* Setes initial aging periods to the contact.
|
||||
*/
|
||||
protected getInitialAgingPeriodsTotal(): IAgingPeriodTotal[] {
|
||||
return this.agingPeriods.map((agingPeriod) => ({
|
||||
...agingPeriod,
|
||||
total: this.formatAmount(0),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the given contact aging periods.
|
||||
* @param {number} contactId - Contact id.
|
||||
* @return {IAgingPeriodTotal[]}
|
||||
*/
|
||||
protected getContactAgingPeriods(contactId: number): IAgingPeriodTotal[] {
|
||||
const unpaidInvoices = this.getUnpaidInvoicesByContactId(contactId);
|
||||
const initialAgingPeriods = this.getInitialAgingPeriodsTotal();
|
||||
|
||||
return unpaidInvoices.reduce(
|
||||
(agingPeriods: IAgingPeriodTotal[], unpaidInvoice) => {
|
||||
const newAgingPeriods = this.getContactAgingDueAmount(
|
||||
agingPeriods,
|
||||
unpaidInvoice.dueAmount,
|
||||
unpaidInvoice.overdueDays
|
||||
);
|
||||
return newAgingPeriods;
|
||||
},
|
||||
initialAgingPeriods
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the contact aging due amount to the table.
|
||||
* @param {IAgingPeriodTotal} agingPeriods - Aging periods.
|
||||
* @param {number} dueAmount - Due amount.
|
||||
* @param {number} overdueDays - Overdue days.
|
||||
* @return {IAgingPeriodTotal[]}
|
||||
*/
|
||||
protected getContactAgingDueAmount(
|
||||
agingPeriods: IAgingPeriodTotal[],
|
||||
dueAmount: number,
|
||||
overdueDays: number
|
||||
): IAgingPeriodTotal[] {
|
||||
const newAgingPeriods = agingPeriods.map((agingPeriod) => {
|
||||
const isInAgingPeriod =
|
||||
agingPeriod.beforeDays <= overdueDays &&
|
||||
(agingPeriod.toDays > overdueDays || !agingPeriod.toDays);
|
||||
|
||||
const total: number = isInAgingPeriod
|
||||
? agingPeriod.total.amount + dueAmount
|
||||
: agingPeriod.total.amount;
|
||||
|
||||
return {
|
||||
...agingPeriod,
|
||||
total: this.formatAmount(total),
|
||||
};
|
||||
});
|
||||
return newAgingPeriods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the aging period total object.
|
||||
* @param {number} amount
|
||||
* @param {IFormatNumberSettings} settings - Override the format number settings.
|
||||
* @return {IAgingAmount}
|
||||
*/
|
||||
protected formatAmount(
|
||||
amount: number,
|
||||
settings: IFormatNumberSettings = {}
|
||||
): IAgingAmount {
|
||||
return {
|
||||
amount,
|
||||
formattedAmount: this.formatNumber(amount, settings),
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the aging period total object.
|
||||
* @param {number} amount
|
||||
* @param {IFormatNumberSettings} settings - Override the format number settings.
|
||||
* @return {IAgingPeriodTotal}
|
||||
*/
|
||||
protected formatTotalAmount(
|
||||
amount: number,
|
||||
settings: IFormatNumberSettings = {}
|
||||
): IAgingAmount {
|
||||
return this.formatAmount(amount, {
|
||||
money: true,
|
||||
excerptZero: false,
|
||||
...settings,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the total of the aging period by the given index.
|
||||
* @param {number} index
|
||||
* @return {number}
|
||||
*/
|
||||
protected getTotalAgingPeriodByIndex(
|
||||
contactsAgingPeriods: any,
|
||||
index: number
|
||||
): number {
|
||||
return this.contacts.reduce((acc, contact) => {
|
||||
const totalPeriod = contactsAgingPeriods[index]
|
||||
? contactsAgingPeriods[index].total
|
||||
: 0;
|
||||
|
||||
return acc + totalPeriod;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the due invoices by the given contact id.
|
||||
* @param {number} contactId -
|
||||
* @return {(ISaleInvoice | IBill)[]}
|
||||
*/
|
||||
protected getUnpaidInvoicesByContactId(
|
||||
contactId: number
|
||||
): (ISaleInvoice | IBill)[] {
|
||||
return defaultTo(this.overdueInvoicesByContactId[contactId], []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve total aging periods of the report.
|
||||
* @return {(IAgingPeriodTotal & IAgingPeriod)[]}
|
||||
*/
|
||||
protected getTotalAgingPeriods(
|
||||
contactsAgingPeriods: IARAgingSummaryCustomer[]
|
||||
): IAgingPeriodTotal[] {
|
||||
return this.agingPeriods.map((agingPeriod, index) => {
|
||||
const total = sumBy(
|
||||
contactsAgingPeriods,
|
||||
(summary: IARAgingSummaryCustomer) => {
|
||||
const aging = summary.aging[index];
|
||||
|
||||
if (!aging) {
|
||||
return 0;
|
||||
}
|
||||
return aging.total.amount;
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
...agingPeriod,
|
||||
total: this.formatTotalAmount(total),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current invoices by the given contact id.
|
||||
* @param {number} contactId - Specific contact id.
|
||||
* @return {(ISaleInvoice | IBill)[]}
|
||||
*/
|
||||
protected getCurrentInvoicesByContactId(
|
||||
contactId: number
|
||||
): (ISaleInvoice | IBill)[] {
|
||||
return get(this.currentInvoicesByContactId, contactId, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the contact total due amount.
|
||||
* @param {number} contactId - Specific contact id.
|
||||
* @return {number}
|
||||
*/
|
||||
protected getContactCurrentTotal(contactId: number): number {
|
||||
const currentInvoices = this.getCurrentInvoicesByContactId(contactId);
|
||||
return sumBy(currentInvoices, (invoice) => invoice.dueAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve to total sumation of the given contacts summeries sections.
|
||||
* @param {IARAgingSummaryCustomer[]} contactsSections -
|
||||
* @return {number}
|
||||
*/
|
||||
protected getTotalCurrent(contactsSummaries: IAgingSummaryContact[]): number {
|
||||
return sumBy(contactsSummaries, (summary) => summary.current.amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the total of the given aging periods.
|
||||
* @param {IAgingPeriodTotal[]} agingPeriods
|
||||
* @return {number}
|
||||
*/
|
||||
protected getAgingPeriodsTotal(agingPeriods: IAgingPeriodTotal[]): number {
|
||||
return sumBy(agingPeriods, (period) => period.total.amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve total of contacts totals.
|
||||
* @param {IAgingSummaryContact[]} contactsSummaries
|
||||
*/
|
||||
protected getTotalContactsTotals(
|
||||
contactsSummaries: IAgingSummaryContact[]
|
||||
): number {
|
||||
return sumBy(contactsSummaries, (summary) => summary.total.amount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
import * as R from 'ramda';
|
||||
import { defaultTo, isEmpty, sumBy } from 'lodash';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
import {
|
||||
IBalanceSheetAggregateNode,
|
||||
IBalanceSheetAccountNode,
|
||||
BALANCE_SHEET_SCHEMA_NODE_TYPE,
|
||||
IBalanceSheetQuery,
|
||||
INumberFormatQuery,
|
||||
IAccount,
|
||||
IBalanceSheetSchemaNode,
|
||||
IBalanceSheetSchemaAggregateNode,
|
||||
IBalanceSheetDataNode,
|
||||
IBalanceSheetSchemaAccountNode,
|
||||
IBalanceSheetCommonNode,
|
||||
} from '../../../interfaces';
|
||||
import { BalanceSheetSchema } from './BalanceSheetSchema';
|
||||
import { BalanceSheetPercentage } from './BalanceSheetPercentage';
|
||||
import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod';
|
||||
import { BalanceSheetComparsionPreviousYear } from './BalanceSheetComparsionPreviousYear';
|
||||
import { BalanceSheetDatePeriods } from './BalanceSheetDatePeriods';
|
||||
import { BalanceSheetBase } from './BalanceSheetBase';
|
||||
import { FinancialSheetStructure } from '../FinancialSheetStructure';
|
||||
import BalanceSheetRepository from './BalanceSheetRepository';
|
||||
import { BalanceSheetQuery } from './BalanceSheetQuery';
|
||||
import { BalanceSheetFiltering } from './BalanceSheetFiltering';
|
||||
|
||||
export default class BalanceSheet extends R.compose(
|
||||
BalanceSheetFiltering,
|
||||
BalanceSheetDatePeriods,
|
||||
BalanceSheetComparsionPreviousPeriod,
|
||||
BalanceSheetComparsionPreviousYear,
|
||||
BalanceSheetPercentage,
|
||||
BalanceSheetSchema,
|
||||
BalanceSheetBase,
|
||||
FinancialSheetStructure
|
||||
)(FinancialSheet) {
|
||||
/**
|
||||
* Balance sheet query.
|
||||
* @param {BalanceSheetQuery}
|
||||
*/
|
||||
readonly query: BalanceSheetQuery;
|
||||
|
||||
/**
|
||||
* Balance sheet number format query.
|
||||
* @param {INumberFormatQuery}
|
||||
*/
|
||||
readonly numberFormat: INumberFormatQuery;
|
||||
|
||||
/**
|
||||
* Base currency of the organization.
|
||||
* @param {string}
|
||||
*/
|
||||
readonly baseCurrency: string;
|
||||
|
||||
readonly i18n: any;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {IBalanceSheetQuery} query -
|
||||
* @param {IAccount[]} accounts -
|
||||
* @param {string} baseCurrency -
|
||||
*/
|
||||
constructor(
|
||||
query: IBalanceSheetQuery,
|
||||
repository: BalanceSheetRepository,
|
||||
baseCurrency: string,
|
||||
i18n
|
||||
) {
|
||||
super();
|
||||
|
||||
this.query = new BalanceSheetQuery(query);
|
||||
this.repository = repository;
|
||||
this.baseCurrency = baseCurrency;
|
||||
this.numberFormat = this.query.query.numberFormat;
|
||||
this.i18n = i18n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the accounts node of accounts types.
|
||||
* @param {string} accountsTypes
|
||||
* @returns {IAccount[]}
|
||||
*/
|
||||
private getAccountsByAccountTypes = (accountsTypes: string[]): IAccount[] => {
|
||||
const mapAccountsByTypes = R.map((accountType) =>
|
||||
defaultTo(this.repository.accountsByType.get(accountType), [])
|
||||
);
|
||||
return R.compose(R.flatten, mapAccountsByTypes)(accountsTypes);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mappes the aggregate schema node type.
|
||||
* @param {IBalanceSheetSchemaAggregateNode} node - Schema node.
|
||||
* @return {IBalanceSheetAggregateNode}
|
||||
*/
|
||||
private reportSchemaAggregateNodeMapper = (
|
||||
node: IBalanceSheetSchemaAggregateNode
|
||||
): IBalanceSheetAggregateNode => {
|
||||
const total = this.getTotalOfNodes(node.children);
|
||||
|
||||
return {
|
||||
name: this.i18n.__(node.name),
|
||||
id: node.id,
|
||||
nodeType: BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE,
|
||||
total: this.getTotalAmountMeta(total),
|
||||
children: node.children,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Compose shema aggregate node of balance sheet schema.
|
||||
* @param {IBalanceSheetSchemaAggregateNode} node
|
||||
* @returns {IBalanceSheetSchemaAggregateNode}
|
||||
*/
|
||||
private schemaAggregateNodeCompose = (
|
||||
node: IBalanceSheetSchemaAggregateNode
|
||||
) => {
|
||||
return R.compose(
|
||||
this.aggregateNodeTotalMapper,
|
||||
this.reportSchemaAggregateNodeMapper
|
||||
)(node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mappes the account model to report account node.
|
||||
* @param {IAccount} account
|
||||
* @returns {IBalanceSheetAccountNode}
|
||||
*/
|
||||
private reportSchemaAccountNodeMapper = (
|
||||
account: IAccount
|
||||
): IBalanceSheetAccountNode => {
|
||||
const total = this.repository.totalAccountsLedger
|
||||
.whereAccountId(account.id)
|
||||
.getClosingBalance();
|
||||
|
||||
return {
|
||||
id: account.id,
|
||||
index: account.index,
|
||||
name: account.name,
|
||||
code: account.code,
|
||||
total: this.getAmountMeta(total),
|
||||
nodeType: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNT,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IAccount} account
|
||||
* @returns {IBalanceSheetAccountNode}
|
||||
*/
|
||||
private reportSchemaAccountNodeComposer = (
|
||||
account: IAccount
|
||||
): IBalanceSheetAccountNode => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.query.isPreviousYearActive,
|
||||
this.previousYearAccountNodeComposer
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodActive,
|
||||
this.previousPeriodAccountNodeComposer
|
||||
),
|
||||
R.when(
|
||||
this.query.isDatePeriodsColumnsType,
|
||||
this.assocAccountNodeDatePeriods
|
||||
),
|
||||
this.reportSchemaAccountNodeMapper
|
||||
)(account);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the total of the given nodes.
|
||||
* @param {IBalanceSheetCommonNode[]} nodes
|
||||
* @returns {number}
|
||||
*/
|
||||
private getTotalOfNodes = (nodes: IBalanceSheetCommonNode[]) => {
|
||||
return sumBy(nodes, 'total.amount');
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the report accounts node by the given accounts types.
|
||||
* @param {string[]} accountsTypes
|
||||
* @returns {}
|
||||
*/
|
||||
private getAccountsNodesByAccountTypes = (accountsTypes: string[]) => {
|
||||
const accounts = this.getAccountsByAccountTypes(accountsTypes);
|
||||
|
||||
return R.compose(R.map(this.reportSchemaAccountNodeComposer))(accounts);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mappes the accounts schema node type.
|
||||
* @param {IBalanceSheetSchemaNode} node - Schema node.
|
||||
* @returns {IBalanceSheetAccountNode}
|
||||
*/
|
||||
private reportSchemaAccountsNodeMapper = (
|
||||
node: IBalanceSheetSchemaAccountNode
|
||||
): IBalanceSheetAccountNode => {
|
||||
const accounts = this.getAccountsNodesByAccountTypes(node.accountsTypes);
|
||||
const total = this.getTotalOfNodes(accounts);
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
name: this.i18n.__(node.name),
|
||||
type: node.type,
|
||||
nodeType: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS,
|
||||
children: accounts,
|
||||
total: this.getTotalAmountMeta(total),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Compose account schema node to report node.
|
||||
* @param {IBalanceSheetSchemaAccountNode} node
|
||||
* @returns {IBalanceSheetAccountNode}
|
||||
*/
|
||||
private reportSchemaAccountsNodeComposer = (
|
||||
node: IBalanceSheetSchemaAccountNode
|
||||
): IBalanceSheetAccountNode => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.query.isPreviousYearActive,
|
||||
this.previousYearAggregateNodeComposer
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodActive,
|
||||
this.previousPeriodAggregateNodeComposer
|
||||
),
|
||||
R.when(
|
||||
this.query.isDatePeriodsColumnsType,
|
||||
this.assocAccountsNodeDatePeriods
|
||||
),
|
||||
this.reportSchemaAccountsNodeMapper
|
||||
)(node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mappes the given report schema node.
|
||||
* @param {IBalanceSheetSchemaNode} node - Schema node.
|
||||
* @return {IBalanceSheetDataNode}
|
||||
*/
|
||||
private reportSchemaNodeMapper = (
|
||||
schemaNode: IBalanceSheetSchemaNode
|
||||
): IBalanceSheetDataNode => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.isSchemaNodeType(BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE),
|
||||
this.schemaAggregateNodeCompose
|
||||
),
|
||||
R.when(
|
||||
this.isSchemaNodeType(BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS),
|
||||
this.reportSchemaAccountsNodeComposer
|
||||
)
|
||||
)(schemaNode);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mappes the report schema nodes.
|
||||
* @param {IBalanceSheetSchemaNode[]} nodes -
|
||||
* @return {IBalanceSheetStructureSection[]}
|
||||
*/
|
||||
private reportSchemaAccountNodesMapper = (
|
||||
schemaNodes: IBalanceSheetSchemaNode[]
|
||||
): IBalanceSheetDataNode[] => {
|
||||
return this.mapNodesDeepReverse(schemaNodes, this.reportSchemaNodeMapper);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets total amount that calculated from node children.
|
||||
* @param {IBalanceSheetSection} node
|
||||
* @returns {IBalanceSheetDataNode}
|
||||
*/
|
||||
private aggregateNodeTotalMapper = (
|
||||
node: IBalanceSheetDataNode
|
||||
): IBalanceSheetDataNode => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.query.isPreviousYearActive,
|
||||
this.previousYearAggregateNodeComposer
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodActive,
|
||||
this.previousPeriodAggregateNodeComposer
|
||||
),
|
||||
R.when(
|
||||
this.query.isDatePeriodsColumnsType,
|
||||
this.assocAggregateNodeDatePeriods
|
||||
)
|
||||
)(node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the report statement data.
|
||||
* @returns {IBalanceSheetDataNode[]}
|
||||
*/
|
||||
public reportData = () => {
|
||||
const balanceSheetSchema = this.getSchema();
|
||||
|
||||
return R.compose(
|
||||
this.reportFilterPlugin,
|
||||
this.reportPercentageCompose,
|
||||
this.reportSchemaAccountNodesMapper
|
||||
)(balanceSheetSchema);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as R from 'ramda';
|
||||
import { IBalanceSheetDataNode, IBalanceSheetSchemaNode } from '@/interfaces';
|
||||
|
||||
export const BalanceSheetBase = (Base) =>
|
||||
class extends Base {
|
||||
/**
|
||||
* Detarmines the node type of the given schema node.
|
||||
* @param {IBalanceSheetStructureSection} node -
|
||||
* @param {string} type -
|
||||
* @return {boolean}
|
||||
*/
|
||||
protected isSchemaNodeType = R.curry(
|
||||
(type: string, node: IBalanceSheetSchemaNode): boolean => {
|
||||
return node.type === type;
|
||||
}
|
||||
);
|
||||
|
||||
isNodeType = R.curry(
|
||||
(type: string, node: IBalanceSheetDataNode): boolean => {
|
||||
return node.nodeType === type;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Detarmines the given display columns by type.
|
||||
* @param {string} displayColumnsBy
|
||||
* @returns {boolean}
|
||||
*/
|
||||
protected isDisplayColumnsBy = (displayColumnsBy: string): boolean => {
|
||||
return this.query.displayColumnsType === displayColumnsBy;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,267 @@
|
||||
import * as R from 'ramda';
|
||||
import { sumBy } from 'lodash';
|
||||
import {
|
||||
IBalanceSheetAccountNode,
|
||||
IBalanceSheetDataNode,
|
||||
IBalanceSheetAggregateNode,
|
||||
IBalanceSheetTotal,
|
||||
IBalanceSheetCommonNode,
|
||||
} from '@/interfaces';
|
||||
import { FinancialPreviousPeriod } from '../FinancialPreviousPeriod';
|
||||
import { FinancialHorizTotals } from '../FinancialHorizTotals';
|
||||
|
||||
export const BalanceSheetComparsionPreviousPeriod = (Base: any) =>
|
||||
class
|
||||
extends R.compose(FinancialPreviousPeriod, FinancialHorizTotals)(Base)
|
||||
implements IBalanceSheetComparsions
|
||||
{
|
||||
// ------------------------------
|
||||
// # Account
|
||||
// ------------------------------
|
||||
/**
|
||||
* Associates the previous period to account node.
|
||||
* @param {IBalanceSheetDataNode} node
|
||||
* @returns {IBalanceSheetDataNode}
|
||||
*/
|
||||
protected assocPreviousPeriodAccountNode = (
|
||||
node: IBalanceSheetDataNode
|
||||
): IBalanceSheetDataNode => {
|
||||
const total = this.repository.PPTotalAccountsLedger.whereAccountId(
|
||||
node.id
|
||||
).getClosingBalance();
|
||||
|
||||
return R.assoc('previousPeriod', this.getAmountMeta(total), node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Previous period account node composer.
|
||||
* @param {IBalanceSheetAccountNode} node
|
||||
* @returns {IBalanceSheetAccountNode}
|
||||
*/
|
||||
protected previousPeriodAccountNodeComposer = (
|
||||
node: IBalanceSheetAccountNode
|
||||
): IBalanceSheetAccountNode => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.isNodeHasHorizTotals,
|
||||
this.assocPreivousPeriodAccountHorizNodeComposer
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodPercentageActive,
|
||||
this.assocPreviousPeriodPercentageNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodChangeActive,
|
||||
this.assocPreviousPeriodChangeNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodActive,
|
||||
this.assocPreviousPeriodAccountNode
|
||||
)
|
||||
)(node);
|
||||
};
|
||||
|
||||
// ------------------------------
|
||||
// # Aggregate
|
||||
// ------------------------------
|
||||
/**
|
||||
* Assoc previous period total to aggregate node.
|
||||
* @param {IBalanceSheetAggregateNode} node
|
||||
* @returns {IBalanceSheetAggregateNode}
|
||||
*/
|
||||
protected assocPreviousPeriodAggregateNode = (
|
||||
node: IBalanceSheetAggregateNode
|
||||
): IBalanceSheetAggregateNode => {
|
||||
const total = sumBy(node.children, 'previousYear.amount');
|
||||
|
||||
return R.assoc('previousPeriod', this.getTotalAmountMeta(total), node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Previous period aggregate node composer.
|
||||
* @param {IBalanceSheetAccountNode} node
|
||||
* @returns {IBalanceSheetAccountNode}
|
||||
*/
|
||||
protected previousPeriodAggregateNodeComposer = (
|
||||
node: IBalanceSheetAccountNode
|
||||
): IBalanceSheetAccountNode => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.isNodeHasHorizTotals,
|
||||
this.assocPreviousPeriodAggregateHorizNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodPercentageActive,
|
||||
this.assocPreviousPeriodTotalPercentageNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodChangeActive,
|
||||
this.assocPreviousPeriodTotalChangeNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodActive,
|
||||
this.assocPreviousPeriodAggregateNode
|
||||
)
|
||||
)(node);
|
||||
};
|
||||
|
||||
// ------------------------------
|
||||
// # Horizontal Nodes - Account.
|
||||
// ------------------------------
|
||||
/**
|
||||
* Retrieve the given account total in the given period.
|
||||
* @param {number} accountId - Account id.
|
||||
* @param {Date} fromDate - From date.
|
||||
* @param {Date} toDate - To date.
|
||||
* @returns {number}
|
||||
*/
|
||||
private getAccountPPDatePeriodTotal = R.curry(
|
||||
(accountId: number, fromDate: Date, toDate: Date): number => {
|
||||
const PPPeriodsTotal =
|
||||
this.repository.PPPeriodsAccountsLedger.whereAccountId(accountId)
|
||||
.whereToDate(toDate)
|
||||
.getClosingBalance();
|
||||
|
||||
const PPPeriodsOpeningTotal =
|
||||
this.repository.PPPeriodsOpeningAccountLedger.whereAccountId(
|
||||
accountId
|
||||
).getClosingBalance();
|
||||
|
||||
return PPPeriodsOpeningTotal + PPPeriodsTotal;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Assoc preivous period to account horizontal total node.
|
||||
* @param {IBalanceSheetAccountNode} node
|
||||
* @returns {}
|
||||
*/
|
||||
private assocPreviousPeriodAccountHorizTotal = R.curry(
|
||||
(node: IBalanceSheetAccountNode, totalNode) => {
|
||||
const total = this.getAccountPPDatePeriodTotal(
|
||||
node.id,
|
||||
totalNode.previousPeriodFromDate.date,
|
||||
totalNode.previousPeriodToDate.date
|
||||
);
|
||||
return R.assoc('previousPeriod', this.getAmountMeta(total), totalNode);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Previous year account horizontal node composer.
|
||||
* @param {IBalanceSheetAccountNode} node -
|
||||
* @param {IBalanceSheetTotal}
|
||||
* @returns {IBalanceSheetTotal}
|
||||
*/
|
||||
private previousPeriodAccountHorizNodeCompose = R.curry(
|
||||
(
|
||||
node: IBalanceSheetAccountNode,
|
||||
horizontalTotalNode: IBalanceSheetTotal
|
||||
): IBalanceSheetTotal => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.query.isPreviousPeriodPercentageActive,
|
||||
this.assocPreviousPeriodPercentageNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodChangeActive,
|
||||
this.assocPreviousPeriodChangeNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodActive,
|
||||
this.assocPreviousPeriodAccountHorizTotal(node)
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodActive,
|
||||
this.assocPreviousPeriodHorizNodeFromToDates(
|
||||
this.query.displayColumnsBy
|
||||
)
|
||||
)
|
||||
)(horizontalTotalNode);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IBalanceSheetAccountNode} node
|
||||
* @returns
|
||||
*/
|
||||
private assocPreivousPeriodAccountHorizNodeComposer = (
|
||||
node: IBalanceSheetAccountNode
|
||||
) => {
|
||||
const horizontalTotals = R.map(
|
||||
this.previousPeriodAccountHorizNodeCompose(node),
|
||||
node.horizontalTotals
|
||||
);
|
||||
return R.assoc('horizontalTotals', horizontalTotals, node);
|
||||
};
|
||||
|
||||
// ------------------------------
|
||||
// # Horizontal Nodes - Aggregate
|
||||
// ------------------------------
|
||||
/**
|
||||
* Assoc previous year total to horizontal node.
|
||||
* @param node
|
||||
* @returns
|
||||
*/
|
||||
private assocPreviousPeriodAggregateHorizTotalNode = R.curry(
|
||||
(node, index: number, totalNode) => {
|
||||
const total = this.getPPHorizNodesTotalSumation(index, node);
|
||||
|
||||
return R.assoc(
|
||||
'previousPeriod',
|
||||
this.getTotalAmountMeta(total),
|
||||
totalNode
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Compose previous period to aggregate horizontal nodes.
|
||||
* @param {IBalanceSheetTotal} node
|
||||
* @returns {IBalanceSheetTotal}
|
||||
*/
|
||||
private previousPeriodAggregateHorizNodeComposer = R.curry(
|
||||
(
|
||||
node: IBalanceSheetCommonNode,
|
||||
horiontalTotalNode: IBalanceSheetTotal,
|
||||
index: number
|
||||
): IBalanceSheetTotal => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.query.isPreviousPeriodPercentageActive,
|
||||
this.assocPreviousPeriodTotalPercentageNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodChangeActive,
|
||||
this.assocPreviousPeriodTotalChangeNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodActive,
|
||||
this.assocPreviousPeriodAggregateHorizTotalNode(node, index)
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodActive,
|
||||
this.assocPreviousPeriodHorizNodeFromToDates(
|
||||
this.query.displayColumnsBy
|
||||
)
|
||||
)
|
||||
)(horiontalTotalNode);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Assoc
|
||||
* @param {IBalanceSheetCommonNode} node
|
||||
* @returns {IBalanceSheetCommonNode}
|
||||
*/
|
||||
private assocPreviousPeriodAggregateHorizNode = (
|
||||
node: IBalanceSheetCommonNode
|
||||
) => {
|
||||
const horizontalTotals = R.addIndex(R.map)(
|
||||
this.previousPeriodAggregateHorizNodeComposer(node),
|
||||
node.horizontalTotals
|
||||
);
|
||||
return R.assoc('horizontalTotals', horizontalTotals, node);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,269 @@
|
||||
import * as R from 'ramda';
|
||||
import { sumBy, isEmpty } from 'lodash';
|
||||
import {
|
||||
IBalanceSheetAccountNode,
|
||||
IBalanceSheetCommonNode,
|
||||
IBalanceSheetDataNode,
|
||||
IBalanceSheetTotal,
|
||||
ITableColumn,
|
||||
} from '@/interfaces';
|
||||
import { FinancialPreviousYear } from '../FinancialPreviousYear';
|
||||
|
||||
export const BalanceSheetComparsionPreviousYear = (Base: any) =>
|
||||
class
|
||||
extends R.compose(FinancialPreviousYear)(Base)
|
||||
implements IBalanceSheetComparsions
|
||||
{
|
||||
// ------------------------------
|
||||
// # Account
|
||||
// ------------------------------
|
||||
/**
|
||||
* Associates the previous year to account node.
|
||||
* @param {IBalanceSheetDataNode} node
|
||||
* @returns {IBalanceSheetDataNode}
|
||||
*/
|
||||
protected assocPreviousYearAccountNode = (
|
||||
node: IBalanceSheetDataNode
|
||||
): IBalanceSheetDataNode => {
|
||||
const closingBalance =
|
||||
this.repository.PYTotalAccountsLedger.whereAccountId(
|
||||
node.id
|
||||
).getClosingBalance();
|
||||
|
||||
return R.assoc('previousYear', this.getAmountMeta(closingBalance), node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Assoc previous year attributes to account node.
|
||||
* @param {IBalanceSheetAccountNode} node
|
||||
* @returns {IBalanceSheetAccountNode}
|
||||
*/
|
||||
protected previousYearAccountNodeComposer = (
|
||||
node: IBalanceSheetAccountNode
|
||||
): IBalanceSheetAccountNode => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.isNodeHasHorizontalTotals,
|
||||
this.assocPreviousYearAccountHorizNodeComposer
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearPercentageActive,
|
||||
this.assocPreviousYearPercentageNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearChangeActive,
|
||||
this.assocPreviousYearChangetNode
|
||||
),
|
||||
this.assocPreviousYearAccountNode
|
||||
)(node);
|
||||
};
|
||||
|
||||
// ------------------------------
|
||||
// # Aggregate
|
||||
// ------------------------------
|
||||
/**
|
||||
* Assoc previous year on aggregate node.
|
||||
* @param {IBalanceSheetAccountNode} node
|
||||
* @returns {IBalanceSheetAccountNode}
|
||||
*/
|
||||
protected assocPreviousYearAggregateNode = (
|
||||
node: IBalanceSheetAccountNode
|
||||
): IBalanceSheetAccountNode => {
|
||||
const total = sumBy(node.children, 'previousYear.amount');
|
||||
|
||||
return R.assoc('previousYear', this.getTotalAmountMeta(total), node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Assoc previous year attributes to aggregate node.
|
||||
* @param {IBalanceSheetAccountNode} node
|
||||
* @returns {IBalanceSheetAccountNode}
|
||||
*/
|
||||
protected previousYearAggregateNodeComposer = (
|
||||
node: IBalanceSheetAccountNode
|
||||
): IBalanceSheetAccountNode => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.query.isPreviousYearPercentageActive,
|
||||
this.assocPreviousYearTotalPercentageNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearChangeActive,
|
||||
this.assocPreviousYearTotalChangeNode
|
||||
),
|
||||
R.when(
|
||||
this.isNodeHasHorizontalTotals,
|
||||
this.assocPreviousYearAggregateHorizNode
|
||||
),
|
||||
this.assocPreviousYearAggregateNode
|
||||
)(node);
|
||||
};
|
||||
|
||||
// ------------------------------
|
||||
// # Horizontal Nodes - Aggregate
|
||||
// ------------------------------
|
||||
/**
|
||||
* Assoc previous year total to horizontal node.
|
||||
* @param node
|
||||
* @returns
|
||||
*/
|
||||
private assocPreviousYearAggregateHorizTotalNode = R.curry(
|
||||
(node, index, totalNode) => {
|
||||
const total = this.getPYHorizNodesTotalSumation(index, node);
|
||||
|
||||
return R.assoc(
|
||||
'previousYear',
|
||||
this.getTotalAmountMeta(total),
|
||||
totalNode
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Compose previous year to aggregate horizontal nodes.
|
||||
* @param {IBalanceSheetTotal} node
|
||||
* @returns {IBalanceSheetTotal}
|
||||
*/
|
||||
private previousYearAggregateHorizNodeComposer = R.curry(
|
||||
(
|
||||
node: IBalanceSheetCommonNode,
|
||||
horiontalTotalNode: IBalanceSheetTotal,
|
||||
index: number
|
||||
): IBalanceSheetTotal => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.query.isPreviousYearPercentageActive,
|
||||
this.assocPreviousYearTotalPercentageNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearChangeActive,
|
||||
this.assocPreviousYearTotalChangeNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearActive,
|
||||
this.assocPreviousYearAggregateHorizTotalNode(node, index)
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearActive,
|
||||
this.assocPreviousYearHorizNodeFromToDates
|
||||
)
|
||||
)(horiontalTotalNode);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Assoc
|
||||
* @param {IBalanceSheetCommonNode} node
|
||||
* @returns {IBalanceSheetCommonNode}
|
||||
*/
|
||||
private assocPreviousYearAggregateHorizNode = (
|
||||
node: IBalanceSheetCommonNode
|
||||
) => {
|
||||
const horizontalTotals = R.addIndex(R.map)(
|
||||
this.previousYearAggregateHorizNodeComposer(node),
|
||||
node.horizontalTotals
|
||||
);
|
||||
return R.assoc('horizontalTotals', horizontalTotals, node);
|
||||
};
|
||||
|
||||
// ------------------------------
|
||||
// # Horizontal Nodes - Account.
|
||||
// ------------------------------
|
||||
/**
|
||||
* Retrieve the given account total in the given period.
|
||||
* @param {number} accountId - Account id.
|
||||
* @param {Date} fromDate - From date.
|
||||
* @param {Date} toDate - To date.
|
||||
* @returns {number}
|
||||
*/
|
||||
private getAccountPYDatePeriodTotal = R.curry(
|
||||
(accountId: number, fromDate: Date, toDate: Date): number => {
|
||||
const PYPeriodsTotal =
|
||||
this.repository.PYPeriodsAccountsLedger.whereAccountId(accountId)
|
||||
.whereToDate(toDate)
|
||||
.getClosingBalance();
|
||||
|
||||
const PYPeriodsOpeningTotal =
|
||||
this.repository.PYPeriodsOpeningAccountLedger.whereAccountId(
|
||||
accountId
|
||||
).getClosingBalance();
|
||||
|
||||
return PYPeriodsOpeningTotal + PYPeriodsTotal;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Assoc preivous year to account horizontal total node.
|
||||
* @param {IBalanceSheetAccountNode} node
|
||||
* @returns {}
|
||||
*/
|
||||
private assocPreviousYearAccountHorizTotal = R.curry(
|
||||
(node: IBalanceSheetAccountNode, totalNode) => {
|
||||
const total = this.getAccountPYDatePeriodTotal(
|
||||
node.id,
|
||||
totalNode.previousYearFromDate.date,
|
||||
totalNode.previousYearToDate.date
|
||||
);
|
||||
return R.assoc('previousYear', this.getAmountMeta(total), totalNode);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Previous year account horizontal node composer.
|
||||
* @param {IBalanceSheetAccountNode} node -
|
||||
* @param {IBalanceSheetTotal}
|
||||
* @returns {IBalanceSheetTotal}
|
||||
*/
|
||||
private previousYearAccountHorizNodeCompose = R.curry(
|
||||
(
|
||||
node: IBalanceSheetAccountNode,
|
||||
horizontalTotalNode: IBalanceSheetTotal
|
||||
): IBalanceSheetTotal => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.query.isPreviousYearPercentageActive,
|
||||
this.assocPreviousYearPercentageNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearChangeActive,
|
||||
this.assocPreviousYearChangetNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearActive,
|
||||
this.assocPreviousYearAccountHorizTotal(node)
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearActive,
|
||||
this.assocPreviousYearHorizNodeFromToDates
|
||||
)
|
||||
)(horizontalTotalNode);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Assoc previous year horizontal nodes to account node.
|
||||
* @param {IBalanceSheetAccountNode} node
|
||||
* @returns {IBalanceSheetAccountNode}
|
||||
*/
|
||||
private assocPreviousYearAccountHorizNodeComposer = (
|
||||
node: IBalanceSheetAccountNode
|
||||
) => {
|
||||
const horizontalTotals = R.map(
|
||||
this.previousYearAccountHorizNodeCompose(node),
|
||||
node.horizontalTotals
|
||||
);
|
||||
return R.assoc('horizontalTotals', horizontalTotals, node);
|
||||
};
|
||||
|
||||
// ------------------------------
|
||||
// # Horizontal Nodes - Aggregate.
|
||||
// ------------------------------
|
||||
|
||||
/**
|
||||
* Detarmines whether the given node has horizontal totals.
|
||||
* @param {IBalanceSheetCommonNode} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private isNodeHasHorizontalTotals = (node: IBalanceSheetCommonNode) =>
|
||||
!isEmpty(node.horizontalTotals);
|
||||
};
|
||||
@@ -0,0 +1,211 @@
|
||||
import * as R from 'ramda';
|
||||
import { sumBy } from 'lodash';
|
||||
import {
|
||||
IBalanceSheetQuery,
|
||||
IFormatNumberSettings,
|
||||
IBalanceSheetDatePeriods,
|
||||
IBalanceSheetAccountNode,
|
||||
IBalanceSheetTotalPeriod,
|
||||
IDateRange,
|
||||
IBalanceSheetCommonNode,
|
||||
} from '@/interfaces';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
import { FinancialDatePeriods } from '../FinancialDatePeriods';
|
||||
|
||||
/**
|
||||
* Balance sheet date periods.
|
||||
*/
|
||||
export const BalanceSheetDatePeriods = (Base: FinancialSheet) =>
|
||||
class
|
||||
extends R.compose(FinancialDatePeriods)(Base)
|
||||
implements IBalanceSheetDatePeriods
|
||||
{
|
||||
/**
|
||||
* @param {IBalanceSheetQuery}
|
||||
*/
|
||||
readonly query: IBalanceSheetQuery;
|
||||
|
||||
/**
|
||||
* Retrieves the date periods based on the report query.
|
||||
* @returns {IDateRange[]}
|
||||
*/
|
||||
get datePeriods(): IDateRange[] {
|
||||
return this.getDateRanges(
|
||||
this.query.fromDate,
|
||||
this.query.toDate,
|
||||
this.query.displayColumnsBy
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the date periods of the given node based on the report query.
|
||||
* @param {IBalanceSheetCommonNode} node
|
||||
* @param {Function} callback
|
||||
* @returns {}
|
||||
*/
|
||||
protected getReportNodeDatePeriods = (
|
||||
node: IBalanceSheetCommonNode,
|
||||
callback: (
|
||||
node: IBalanceSheetCommonNode,
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
index: number
|
||||
) => any
|
||||
) => {
|
||||
return this.getNodeDatePeriods(
|
||||
this.query.fromDate,
|
||||
this.query.toDate,
|
||||
this.query.displayColumnsBy,
|
||||
node,
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the date period meta.
|
||||
* @param {number} total - Total amount.
|
||||
* @param {Date} fromDate - From date.
|
||||
* @param {Date} toDate - To date.
|
||||
* @return {ICashFlowDatePeriod}
|
||||
*/
|
||||
private getDatePeriodTotalMeta = (
|
||||
total: number,
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
overrideSettings: IFormatNumberSettings = {}
|
||||
): IBalanceSheetTotalPeriod => {
|
||||
return this.getDatePeriodMeta(total, fromDate, toDate, {
|
||||
money: true,
|
||||
...overrideSettings,
|
||||
});
|
||||
};
|
||||
|
||||
// --------------------------------
|
||||
// # Account
|
||||
// --------------------------------
|
||||
/**
|
||||
* Retrieve the given account date period total.
|
||||
* @param {number} accountId
|
||||
* @param {Date} toDate
|
||||
* @returns {number}
|
||||
*/
|
||||
private getAccountDatePeriodTotal = (
|
||||
accountId: number,
|
||||
toDate: Date
|
||||
): number => {
|
||||
const periodTotalBetween = this.repository.periodsAccountsLedger
|
||||
.whereAccountId(accountId)
|
||||
.whereToDate(toDate)
|
||||
.getClosingBalance();
|
||||
|
||||
const periodOpening = this.repository.periodsOpeningAccountLedger
|
||||
.whereAccountId(accountId)
|
||||
.getClosingBalance();
|
||||
|
||||
return periodOpening + periodTotalBetween;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IBalanceSheetAccountNode} node
|
||||
* @param {Date} fromDate
|
||||
* @param {Date} toDate
|
||||
* @returns {IBalanceSheetAccountNode}
|
||||
*/
|
||||
private getAccountNodeDatePeriod = (
|
||||
node: IBalanceSheetAccountNode,
|
||||
fromDate: Date,
|
||||
toDate: Date
|
||||
): IBalanceSheetTotalPeriod => {
|
||||
const periodTotal = this.getAccountDatePeriodTotal(node.id, toDate);
|
||||
|
||||
return this.getDatePeriodTotalMeta(periodTotal, fromDate, toDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve total date periods of the given account node.
|
||||
* @param {IBalanceSheetAccountNode} node
|
||||
* @returns {IBalanceSheetAccountNode}
|
||||
*/
|
||||
private getAccountsNodeDatePeriods = (
|
||||
node: IBalanceSheetAccountNode
|
||||
): IBalanceSheetTotalPeriod[] => {
|
||||
return this.getReportNodeDatePeriods(node, this.getAccountNodeDatePeriod);
|
||||
};
|
||||
|
||||
/**
|
||||
* Assoc total date periods to account node.
|
||||
* @param {IBalanceSheetAccountNode} node
|
||||
* @returns {IBalanceSheetAccountNode}
|
||||
*/
|
||||
public assocAccountNodeDatePeriods = (
|
||||
node: IBalanceSheetAccountNode
|
||||
): IBalanceSheetAccountNode => {
|
||||
const datePeriods = this.getAccountsNodeDatePeriods(node);
|
||||
|
||||
return R.assoc('horizontalTotals', datePeriods, node);
|
||||
};
|
||||
|
||||
// --------------------------------
|
||||
// # Aggregate
|
||||
// --------------------------------
|
||||
/**
|
||||
*
|
||||
* @param {} node
|
||||
* @param {number} index
|
||||
* @returns {number}
|
||||
*/
|
||||
private getAggregateDatePeriodIndexTotal = (node, index) => {
|
||||
return sumBy(node.children, `horizontalTotals[${index}].total.amount`);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IBalanceSheetAccountNode} node
|
||||
* @param {Date} fromDate
|
||||
* @param {Date} toDate
|
||||
* @returns
|
||||
*/
|
||||
public getAggregateNodeDatePeriod = (
|
||||
node: IBalanceSheetAccountNode,
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
index: number
|
||||
) => {
|
||||
const periodTotal = this.getAggregateDatePeriodIndexTotal(node, index);
|
||||
|
||||
return this.getDatePeriodTotalMeta(periodTotal, fromDate, toDate);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param node
|
||||
* @returns
|
||||
*/
|
||||
public getAggregateNodeDatePeriods = (node) => {
|
||||
return this.getReportNodeDatePeriods(
|
||||
node,
|
||||
this.getAggregateNodeDatePeriod
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Assoc total date periods to aggregate node.
|
||||
* @param node
|
||||
* @returns {}
|
||||
*/
|
||||
public assocAggregateNodeDatePeriods = (node) => {
|
||||
const datePeriods = this.getAggregateNodeDatePeriods(node);
|
||||
|
||||
return R.assoc('horizontalTotals', datePeriods, node);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param node
|
||||
* @returns
|
||||
*/
|
||||
public assocAccountsNodeDatePeriods = (node) => {
|
||||
return this.assocAggregateNodeDatePeriods(node);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import * as R from 'ramda';
|
||||
import { get } from 'lodash';
|
||||
import {
|
||||
IBalanceSheetDataNode,
|
||||
BALANCE_SHEET_NODE_TYPE,
|
||||
} from '../../../interfaces';
|
||||
import { FinancialFilter } from '../FinancialFilter';
|
||||
|
||||
export const BalanceSheetFiltering = (Base) =>
|
||||
class extends R.compose(FinancialFilter)(Base) {
|
||||
// -----------------------
|
||||
// # Account
|
||||
// -----------------------
|
||||
/**
|
||||
* Filter report node detarmine.
|
||||
* @param {IBalanceSheetDataNode} node - Balance sheet node.
|
||||
* @return {boolean}
|
||||
*/
|
||||
private accountNoneZeroNodesFilterDetarminer = (
|
||||
node: IBalanceSheetDataNode
|
||||
): boolean => {
|
||||
return R.ifElse(
|
||||
this.isNodeType(BALANCE_SHEET_NODE_TYPE.ACCOUNT),
|
||||
this.isNodeNoneZero,
|
||||
R.always(true)
|
||||
)(node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines account none-transactions node.
|
||||
* @param {IBalanceSheetDataNode} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private accountNoneTransFilterDetarminer = (
|
||||
node: IBalanceSheetDataNode
|
||||
): boolean => {
|
||||
return R.ifElse(
|
||||
this.isNodeType(BALANCE_SHEET_NODE_TYPE.ACCOUNT),
|
||||
this.isNodeNoneZero,
|
||||
R.always(true)
|
||||
)(node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Report nodes filter.
|
||||
* @param {IBalanceSheetSection[]} nodes -
|
||||
* @return {IBalanceSheetSection[]}
|
||||
*/
|
||||
private accountsNoneZeroNodesFilter = (
|
||||
nodes: IBalanceSheetDataNode[]
|
||||
): IBalanceSheetDataNode[] => {
|
||||
return this.filterNodesDeep(
|
||||
nodes,
|
||||
this.accountNoneZeroNodesFilterDetarminer
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters the accounts none-transactions nodes.
|
||||
* @param {IBalanceSheetDataNode[]} nodes
|
||||
* @returns {IBalanceSheetDataNode[]}
|
||||
*/
|
||||
private accountsNoneTransactionsNodesFilter = (
|
||||
nodes: IBalanceSheetDataNode[]
|
||||
) => {
|
||||
return this.filterNodesDeep(nodes, this.accountNoneTransFilterDetarminer);
|
||||
};
|
||||
|
||||
// -----------------------
|
||||
// # Aggregate/Accounts.
|
||||
// -----------------------
|
||||
/**
|
||||
* Detearmines aggregate none-children filtering.
|
||||
* @param {IBalanceSheetDataNode} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private aggregateNoneChildrenFilterDetarminer = (
|
||||
node: IBalanceSheetDataNode
|
||||
): boolean => {
|
||||
// Detarmines whether the given node is aggregate or accounts node.
|
||||
const isAggregateOrAccounts =
|
||||
this.isNodeType(BALANCE_SHEET_NODE_TYPE.AGGREGATE, node) ||
|
||||
this.isNodeType(BALANCE_SHEET_NODE_TYPE.ACCOUNTS, node);
|
||||
|
||||
// Retrieve the schema node of the given id.
|
||||
const schemaNode = this.getSchemaNodeById(node.id);
|
||||
|
||||
// Detarmines if the schema node is always should show.
|
||||
const isSchemaAlwaysShow = get(schemaNode, 'alwaysShow', false);
|
||||
|
||||
return isAggregateOrAccounts && !isSchemaAlwaysShow
|
||||
? this.isNodeHasChildren(node)
|
||||
: true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters aggregate none-children nodes.
|
||||
* @param {IBalanceSheetDataNode[]} nodes
|
||||
* @returns {IBalanceSheetDataNode[]}
|
||||
*/
|
||||
private aggregateNoneChildrenFilter = (
|
||||
nodes: IBalanceSheetDataNode[]
|
||||
): IBalanceSheetDataNode[] => {
|
||||
return this.filterNodesDeep2(
|
||||
this.aggregateNoneChildrenFilterDetarminer,
|
||||
nodes
|
||||
);
|
||||
};
|
||||
|
||||
// -----------------------
|
||||
// # Composers.
|
||||
// -----------------------
|
||||
/**
|
||||
* Filters none-zero nodes.
|
||||
* @param {IBalanceSheetDataNode[]} nodes
|
||||
* @returns {IBalanceSheetDataNode[]}
|
||||
*/
|
||||
private filterNoneZeroNodesCompose = (
|
||||
nodes: IBalanceSheetDataNode[]
|
||||
): IBalanceSheetDataNode[] => {
|
||||
return R.compose(
|
||||
this.aggregateNoneChildrenFilter,
|
||||
this.accountsNoneZeroNodesFilter
|
||||
)(nodes);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters none-transactions nodes.
|
||||
* @param {IBalanceSheetDataNode[]} nodes
|
||||
* @returns {IBalanceSheetDataNode[]}
|
||||
*/
|
||||
private filterNoneTransNodesCompose = (
|
||||
nodes: IBalanceSheetDataNode[]
|
||||
): IBalanceSheetDataNode[] => {
|
||||
return R.compose(
|
||||
this.aggregateNoneChildrenFilter,
|
||||
this.accountsNoneTransactionsNodesFilter
|
||||
)(nodes);
|
||||
};
|
||||
|
||||
/**
|
||||
* Supress nodes when accounts transactions ledger is empty.
|
||||
* @param {IBalanceSheetDataNode[]} nodes
|
||||
* @returns {IBalanceSheetDataNode[]}
|
||||
*/
|
||||
private supressNodesWhenAccountsTransactionsEmpty = (
|
||||
nodes: IBalanceSheetDataNode[]
|
||||
): IBalanceSheetDataNode[] => {
|
||||
return this.repository.totalAccountsLedger.isEmpty() ? [] : nodes;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compose report nodes filtering.
|
||||
* @param {IBalanceSheetDataNode[]} nodes
|
||||
* @returns {IBalanceSheetDataNode[]}
|
||||
*/
|
||||
protected reportFilterPlugin = (nodes: IBalanceSheetDataNode[]) => {
|
||||
return R.compose(
|
||||
this.supressNodesWhenAccountsTransactionsEmpty,
|
||||
R.when(R.always(this.query.noneZero), this.filterNoneZeroNodesCompose),
|
||||
R.when(
|
||||
R.always(this.query.noneTransactions),
|
||||
this.filterNoneTransNodesCompose
|
||||
)
|
||||
)(nodes);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,225 @@
|
||||
import * as R from 'ramda';
|
||||
import { get } from 'lodash';
|
||||
import { IBalanceSheetDataNode } from '@/interfaces';
|
||||
import { BalanceSheetQuery } from './BalanceSheetQuery';
|
||||
|
||||
export const BalanceSheetPercentage = (Base: any) =>
|
||||
class extends Base {
|
||||
readonly query: BalanceSheetQuery;
|
||||
|
||||
/**
|
||||
* Assoc percentage of column to report node.
|
||||
* @param {IBalanceSheetDataNode} node
|
||||
* @returns {IBalanceSheetDataNode}
|
||||
*/
|
||||
protected assocReportNodeColumnPercentage = R.curry(
|
||||
(
|
||||
parentTotal: number,
|
||||
node: IBalanceSheetDataNode
|
||||
): IBalanceSheetDataNode => {
|
||||
const percentage = this.getPercentageBasis(
|
||||
parentTotal,
|
||||
node.total.amount
|
||||
);
|
||||
return R.assoc(
|
||||
'percentageColumn',
|
||||
this.getPercentageAmountMeta(percentage),
|
||||
node
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Assoc percentage of row to report node.
|
||||
* @param {IBalanceSheetDataNode} node
|
||||
* @returns {IBalanceSheetDataNode}
|
||||
*/
|
||||
protected assocReportNodeRowPercentage = R.curry(
|
||||
(
|
||||
parentTotal: number,
|
||||
node: IBalanceSheetDataNode
|
||||
): IBalanceSheetDataNode => {
|
||||
const percenatage = this.getPercentageBasis(
|
||||
parentTotal,
|
||||
node.total.amount
|
||||
);
|
||||
return R.assoc(
|
||||
'percentageRow',
|
||||
this.getPercentageAmountMeta(percenatage),
|
||||
node
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Assoc percentage of row to horizontal total.
|
||||
* @param {number} parentTotal -
|
||||
* @param {IBalanceSheetDataNode} node
|
||||
* @returns {IBalanceSheetDataNode}
|
||||
*/
|
||||
protected assocRowPercentageHorizTotals = R.curry(
|
||||
(
|
||||
parentTotal: number,
|
||||
node: IBalanceSheetDataNode
|
||||
): IBalanceSheetDataNode => {
|
||||
const assocRowPercen = this.assocReportNodeRowPercentage(parentTotal);
|
||||
const horTotals = R.map(assocRowPercen)(node.horizontalTotals);
|
||||
|
||||
return R.assoc('horizontalTotals', horTotals, node);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {} parentNode -
|
||||
* @param {} horTotalNode -
|
||||
* @param {number} index -
|
||||
*/
|
||||
private assocColumnPercentageHorizTotal = R.curry(
|
||||
(parentNode, horTotalNode, index) => {
|
||||
const parentTotal = get(
|
||||
parentNode,
|
||||
`horizontalTotals[${index}].total.amount`,
|
||||
0
|
||||
);
|
||||
return this.assocReportNodeColumnPercentage(parentTotal, horTotalNode);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Assoc column percentage to horizontal totals nodes.
|
||||
* @param {IBalanceSheetDataNode} node
|
||||
* @returns {IBalanceSheetDataNode}
|
||||
*/
|
||||
protected assocColumnPercentageHorizTotals = R.curry(
|
||||
(
|
||||
parentNode: IBalanceSheetDataNode,
|
||||
node: IBalanceSheetDataNode
|
||||
): IBalanceSheetDataNode => {
|
||||
// Horizontal totals.
|
||||
const assocColPerc = this.assocColumnPercentageHorizTotal(parentNode);
|
||||
const horTotals = R.addIndex(R.map)(assocColPerc)(
|
||||
node.horizontalTotals
|
||||
);
|
||||
return R.assoc('horizontalTotals', horTotals, node);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} parentTotal -
|
||||
* @param {} node
|
||||
* @returns
|
||||
*/
|
||||
protected reportNodeColumnPercentageComposer = R.curry(
|
||||
(parentNode, node) => {
|
||||
const parentTotal = parentNode.total.amount;
|
||||
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.isNodeHasHorizoTotals,
|
||||
this.assocColumnPercentageHorizTotals(parentNode)
|
||||
),
|
||||
this.assocReportNodeColumnPercentage(parentTotal)
|
||||
)(node);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param node
|
||||
* @returns
|
||||
*/
|
||||
private reportNodeRowPercentageComposer = (node) => {
|
||||
const total = node.total.amount;
|
||||
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.isNodeHasHorizoTotals,
|
||||
this.assocRowPercentageHorizTotals(total)
|
||||
),
|
||||
this.assocReportNodeRowPercentage(total)
|
||||
)(node);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private assocNodeColumnPercentageChildren = (node) => {
|
||||
const children = this.mapNodesDeep(
|
||||
node.children,
|
||||
this.reportNodeColumnPercentageComposer(node)
|
||||
);
|
||||
return R.assoc('children', children, node);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param node
|
||||
* @returns
|
||||
*/
|
||||
private reportNodeColumnPercentageDeepMap = (node) => {
|
||||
const parentTotal = node.total.amount;
|
||||
const parentNode = node;
|
||||
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.isNodeHasHorizoTotals,
|
||||
this.assocColumnPercentageHorizTotals(parentNode)
|
||||
),
|
||||
this.assocReportNodeColumnPercentage(parentTotal),
|
||||
this.assocNodeColumnPercentageChildren
|
||||
)(node);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IBalanceSheetDataNode[]} node
|
||||
* @returns {IBalanceSheetDataNode[]}
|
||||
*/
|
||||
private reportColumnsPercentageMapper = (
|
||||
nodes: IBalanceSheetDataNode[]
|
||||
): IBalanceSheetDataNode[] => {
|
||||
return R.map(this.reportNodeColumnPercentageDeepMap, nodes);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param nodes
|
||||
* @returns
|
||||
*/
|
||||
private reportRowsPercentageMapper = (nodes) => {
|
||||
return this.mapNodesDeep(nodes, this.reportNodeRowPercentageComposer);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param nodes
|
||||
* @returns
|
||||
*/
|
||||
protected reportPercentageCompose = (nodes) => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.query.isColumnsPercentageActive,
|
||||
this.reportColumnsPercentageMapper
|
||||
),
|
||||
R.when(
|
||||
this.query.isRowsPercentageActive,
|
||||
this.reportRowsPercentageMapper
|
||||
)
|
||||
)(nodes);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether the given node has horizontal total.
|
||||
* @param {IBalanceSheetDataNode} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
protected isNodeHasHorizoTotals = (
|
||||
node: IBalanceSheetDataNode
|
||||
): boolean => {
|
||||
return (
|
||||
!R.isEmpty(node.horizontalTotals) && !R.isNil(node.horizontalTotals)
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,177 @@
|
||||
import { merge } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import { IBalanceSheetQuery, IFinancialDatePeriodsUnit } from '@/interfaces';
|
||||
import { FinancialDateRanges } from '../FinancialDateRanges';
|
||||
import { DISPLAY_COLUMNS_BY } from './constants';
|
||||
|
||||
export class BalanceSheetQuery extends R.compose(FinancialDateRanges)(
|
||||
class {}
|
||||
) {
|
||||
/**
|
||||
* Balance sheet query.
|
||||
* @param {IBalanceSheetQuery}
|
||||
*/
|
||||
public readonly query: IBalanceSheetQuery;
|
||||
/**
|
||||
* Previous year to date.
|
||||
* @param {Date}
|
||||
*/
|
||||
public readonly PYToDate: Date;
|
||||
/**
|
||||
* Previous year from date.
|
||||
* @param {Date}
|
||||
*/
|
||||
public readonly PYFromDate: Date;
|
||||
/**
|
||||
* Previous period to date.
|
||||
* @param {Date}
|
||||
*/
|
||||
public readonly PPToDate: Date;
|
||||
/**
|
||||
* Previous period from date.
|
||||
* @param {Date}
|
||||
*/
|
||||
public readonly PPFromDate: Date;
|
||||
/**
|
||||
* Constructor method
|
||||
* @param {IBalanceSheetQuery} query
|
||||
*/
|
||||
constructor(query: IBalanceSheetQuery) {
|
||||
super();
|
||||
this.query = query;
|
||||
|
||||
// Pervious Year (PY) Dates.
|
||||
this.PYToDate = this.getPreviousYearDate(this.query.toDate);
|
||||
this.PYFromDate = this.getPreviousYearDate(this.query.fromDate);
|
||||
|
||||
// Previous Period (PP) Dates for Total column.
|
||||
if (this.isTotalColumnType()) {
|
||||
const { fromDate, toDate } = this.getPPTotalDateRange(
|
||||
this.query.fromDate,
|
||||
this.query.toDate
|
||||
);
|
||||
this.PPToDate = toDate;
|
||||
this.PPFromDate = fromDate;
|
||||
// Previous Period (PP) Dates for Date period columns type.
|
||||
} else if (this.isDatePeriodsColumnsType()) {
|
||||
const { fromDate, toDate } = this.getPPDatePeriodDateRange(
|
||||
this.query.fromDate,
|
||||
this.query.toDate,
|
||||
this.query.displayColumnsBy as IFinancialDatePeriodsUnit
|
||||
);
|
||||
this.PPToDate = toDate;
|
||||
this.PPFromDate = fromDate;
|
||||
}
|
||||
return merge(this, query);
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// # Columns Type/By.
|
||||
// ---------------------------
|
||||
/**
|
||||
* Detarmines the given display columns type.
|
||||
* @param {string} displayColumnsBy
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isDisplayColumnsBy = (displayColumnsBy: string): boolean => {
|
||||
return this.query.displayColumnsBy === displayColumnsBy;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines the given display columns by type.
|
||||
* @param {string} displayColumnsBy
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isDisplayColumnsType = (displayColumnsType: string): boolean => {
|
||||
return this.query.displayColumnsType === displayColumnsType;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether the columns type is date periods.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isDatePeriodsColumnsType = (): boolean => {
|
||||
return this.isDisplayColumnsType(DISPLAY_COLUMNS_BY.DATE_PERIODS);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether the columns type is total.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isTotalColumnType = (): boolean => {
|
||||
return this.isDisplayColumnsType(DISPLAY_COLUMNS_BY.TOTAL);
|
||||
};
|
||||
|
||||
// ---------------------------
|
||||
// # Percentage column/row.
|
||||
// ---------------------------
|
||||
/**
|
||||
* Detarmines whether the percentage of column active.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isColumnsPercentageActive = (): boolean => {
|
||||
return this.query.percentageOfColumn;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether the percentage of row active.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isRowsPercentageActive = (): boolean => {
|
||||
return this.query.percentageOfRow;
|
||||
};
|
||||
|
||||
// ---------------------------
|
||||
// # Previous Year (PY)
|
||||
// ---------------------------
|
||||
/**
|
||||
* Detarmines the report query has previous year enabled.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isPreviousYearActive = (): boolean => {
|
||||
return this.query.previousYear;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines the report query has previous year percentage change active.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isPreviousYearPercentageActive = (): boolean => {
|
||||
return this.query.previousYearPercentageChange;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines the report query has previous year change active.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isPreviousYearChangeActive = (): boolean => {
|
||||
return this.query.previousYearAmountChange;
|
||||
};
|
||||
|
||||
// ---------------------------
|
||||
// # Previous Period (PP).
|
||||
// ---------------------------
|
||||
/**
|
||||
* Detarmines the report query has previous period enabled.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isPreviousPeriodActive = (): boolean => {
|
||||
return this.query.previousPeriod;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines wether the preivous period percentage is active.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isPreviousPeriodPercentageActive = (): boolean => {
|
||||
return this.query.previousPeriodPercentageChange;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines wether the previous period change is active.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isPreviousPeriodChangeActive = (): boolean => {
|
||||
return this.query.previousPeriodAmountChange;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
import { Service } from 'typedi';
|
||||
import * as R from 'ramda';
|
||||
import { Knex } from 'knex';
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
IAccountTransactionsGroupBy,
|
||||
IBalanceSheetQuery,
|
||||
ILedger,
|
||||
} from '@/interfaces';
|
||||
import { transformToMapBy } from 'utils';
|
||||
import Ledger from '@/services/Accounting/Ledger';
|
||||
import { BalanceSheetQuery } from './BalanceSheetQuery';
|
||||
import { FinancialDatePeriods } from '../FinancialDatePeriods';
|
||||
|
||||
@Service()
|
||||
export default class BalanceSheetRepository extends R.compose(
|
||||
FinancialDatePeriods
|
||||
)(class {}) {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private readonly models;
|
||||
|
||||
/**
|
||||
* @param {number}
|
||||
*/
|
||||
public readonly tenantId: number;
|
||||
|
||||
/**
|
||||
* @param {BalanceSheetQuery}
|
||||
*/
|
||||
public readonly query: BalanceSheetQuery;
|
||||
|
||||
/**
|
||||
* @param {}
|
||||
*/
|
||||
public accounts: any;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public accountsByType: any;
|
||||
|
||||
/**
|
||||
* PY from date.
|
||||
* @param {Date}
|
||||
*/
|
||||
public readonly PYFromDate: Date;
|
||||
|
||||
/**
|
||||
* PY to date.
|
||||
* @param {Date}
|
||||
*/
|
||||
public readonly PYToDate: Date;
|
||||
|
||||
/**
|
||||
* PP to date.
|
||||
* @param {Date}
|
||||
*/
|
||||
public readonly PPToDate: Date;
|
||||
|
||||
/**
|
||||
* PP from date.
|
||||
* @param {Date}
|
||||
*/
|
||||
public readonly PPFromDate: Date;
|
||||
|
||||
public totalAccountsLedger: Ledger;
|
||||
|
||||
/**
|
||||
* Transactions group type.
|
||||
* @param {IAccountTransactionsGroupBy}
|
||||
*/
|
||||
public transactionsGroupType: IAccountTransactionsGroupBy =
|
||||
IAccountTransactionsGroupBy.Month;
|
||||
|
||||
// -----------------------
|
||||
// # Date Periods
|
||||
// -----------------------
|
||||
/**
|
||||
* @param {Ledger}
|
||||
*/
|
||||
public periodsAccountsLedger: Ledger;
|
||||
|
||||
/**
|
||||
* @param {Ledger}
|
||||
*/
|
||||
public periodsOpeningAccountLedger: Ledger;
|
||||
|
||||
// -----------------------
|
||||
// # Previous Year (PY).
|
||||
// -----------------------
|
||||
/**
|
||||
* @param {Ledger}
|
||||
*/
|
||||
public PYPeriodsOpeningAccountLedger: Ledger;
|
||||
|
||||
/**
|
||||
* @param {Ledger}
|
||||
*/
|
||||
public PYPeriodsAccountsLedger: Ledger;
|
||||
|
||||
/**
|
||||
* @param {Ledger}
|
||||
*/
|
||||
public PYTotalAccountsLedger: ILedger;
|
||||
|
||||
// -----------------------
|
||||
// # Previous Period (PP).
|
||||
// -----------------------
|
||||
/**
|
||||
* @param {Ledger}
|
||||
*/
|
||||
public PPTotalAccountsLedger: Ledger;
|
||||
|
||||
/**
|
||||
* @param {Ledger}
|
||||
*/
|
||||
public PPPeriodsAccountsLedger: ILedger;
|
||||
|
||||
/**
|
||||
* @param {Ledger}
|
||||
*/
|
||||
public PPPeriodsOpeningAccountLedger: ILedger;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId
|
||||
* @param {IBalanceSheetQuery} query
|
||||
*/
|
||||
constructor(models: any, query: IBalanceSheetQuery) {
|
||||
super();
|
||||
|
||||
this.query = new BalanceSheetQuery(query);
|
||||
this.models = models;
|
||||
|
||||
this.transactionsGroupType = this.getGroupByFromDisplayColumnsBy(
|
||||
this.query.displayColumnsBy
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Async initialize.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public asyncInitialize = async () => {
|
||||
await this.initAccounts();
|
||||
await this.initAccountsTotalLedger();
|
||||
|
||||
// Date periods.
|
||||
if (this.query.isDatePeriodsColumnsType()) {
|
||||
await this.initTotalDatePeriods();
|
||||
}
|
||||
// Previous Year (PY).
|
||||
if (this.query.isPreviousYearActive()) {
|
||||
await this.initTotalPreviousYear();
|
||||
}
|
||||
if (
|
||||
this.query.isPreviousYearActive() &&
|
||||
this.query.isDatePeriodsColumnsType()
|
||||
) {
|
||||
await this.initPeriodsPreviousYear();
|
||||
}
|
||||
// Previous Period (PP).
|
||||
if (this.query.isPreviousPeriodActive()) {
|
||||
await this.initTotalPreviousPeriod();
|
||||
}
|
||||
if (
|
||||
this.query.isPreviousPeriodActive() &&
|
||||
this.query.isDatePeriodsColumnsType()
|
||||
) {
|
||||
await this.initPeriodsPreviousPeriod();
|
||||
}
|
||||
};
|
||||
|
||||
// ----------------------------
|
||||
// # Accounts
|
||||
// ----------------------------
|
||||
public initAccounts = async () => {
|
||||
const accounts = await this.getAccounts();
|
||||
|
||||
this.accounts = accounts;
|
||||
this.accountsByType = transformToMapBy(accounts, 'accountType');
|
||||
};
|
||||
|
||||
// ----------------------------
|
||||
// # Closing Total
|
||||
// ----------------------------
|
||||
/**
|
||||
* Initialize accounts closing total based on the given query.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private initAccountsTotalLedger = async (): Promise<void> => {
|
||||
const totalByAccount = await this.closingAccountsTotal(this.query.toDate);
|
||||
|
||||
// Inject to the repository.
|
||||
this.totalAccountsLedger = Ledger.fromTransactions(totalByAccount);
|
||||
};
|
||||
|
||||
// ----------------------------
|
||||
// # Date periods.
|
||||
// ----------------------------
|
||||
/**
|
||||
* Initialize date periods total.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public initTotalDatePeriods = async (): Promise<void> => {
|
||||
// Retrieves grouped transactions by given date group.
|
||||
const periodsByAccount = await this.accountsDatePeriods(
|
||||
this.query.fromDate,
|
||||
this.query.toDate,
|
||||
this.transactionsGroupType
|
||||
);
|
||||
// Retrieves opening balance of grouped transactions.
|
||||
const periodsOpeningByAccount = await this.closingAccountsTotal(
|
||||
this.query.fromDate
|
||||
);
|
||||
// Inject to the repository.
|
||||
this.periodsAccountsLedger = Ledger.fromTransactions(periodsByAccount);
|
||||
this.periodsOpeningAccountLedger = Ledger.fromTransactions(
|
||||
periodsOpeningByAccount
|
||||
);
|
||||
};
|
||||
|
||||
// ----------------------------
|
||||
// # Previous Year (PY).
|
||||
// ----------------------------
|
||||
/**
|
||||
* Initialize total of previous year.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private initTotalPreviousYear = async (): Promise<void> => {
|
||||
const PYTotalsByAccounts = await this.closingAccountsTotal(
|
||||
this.query.PYToDate
|
||||
);
|
||||
// Inject to the repository.
|
||||
this.PYTotalAccountsLedger = Ledger.fromTransactions(PYTotalsByAccounts);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize date periods of previous year.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private initPeriodsPreviousYear = async (): Promise<void> => {
|
||||
const PYPeriodsBYAccounts = await this.accountsDatePeriods(
|
||||
this.query.PYFromDate,
|
||||
this.query.PYToDate,
|
||||
this.transactionsGroupType
|
||||
);
|
||||
// Retrieves opening balance of grouped transactions.
|
||||
const periodsOpeningByAccount = await this.closingAccountsTotal(
|
||||
this.query.PYFromDate
|
||||
);
|
||||
// Inject to the repository.
|
||||
this.PYPeriodsAccountsLedger = Ledger.fromTransactions(PYPeriodsBYAccounts);
|
||||
this.PYPeriodsOpeningAccountLedger = Ledger.fromTransactions(
|
||||
periodsOpeningByAccount
|
||||
);
|
||||
};
|
||||
|
||||
// ----------------------------
|
||||
// # Previous Year (PP).
|
||||
// ----------------------------
|
||||
/**
|
||||
* Initialize total of previous year.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private initTotalPreviousPeriod = async (): Promise<void> => {
|
||||
const PPTotalsByAccounts = await this.closingAccountsTotal(
|
||||
this.query.PPToDate
|
||||
);
|
||||
// Inject to the repository.
|
||||
this.PPTotalAccountsLedger = Ledger.fromTransactions(PPTotalsByAccounts);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize date periods of previous year.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private initPeriodsPreviousPeriod = async (): Promise<void> => {
|
||||
const PPPeriodsBYAccounts = await this.accountsDatePeriods(
|
||||
this.query.PPFromDate,
|
||||
this.query.PPToDate,
|
||||
this.transactionsGroupType
|
||||
);
|
||||
// Retrieves opening balance of grouped transactions.
|
||||
const periodsOpeningByAccount = await this.closingAccountsTotal(
|
||||
this.query.PPFromDate
|
||||
);
|
||||
// Inject to the repository.
|
||||
this.PPPeriodsAccountsLedger = Ledger.fromTransactions(PPPeriodsBYAccounts);
|
||||
this.PPPeriodsOpeningAccountLedger = Ledger.fromTransactions(
|
||||
periodsOpeningByAccount
|
||||
);
|
||||
};
|
||||
|
||||
// ----------------------------
|
||||
// # Utils
|
||||
// ----------------------------
|
||||
/**
|
||||
* Retrieve accounts of the report.
|
||||
* @return {Promise<IAccount[]>}
|
||||
*/
|
||||
private getAccounts = () => {
|
||||
const { Account } = this.models;
|
||||
|
||||
return Account.query();
|
||||
};
|
||||
|
||||
/**
|
||||
* Closing accounts date periods.
|
||||
* @param openingDate
|
||||
* @param datePeriodsType
|
||||
* @returns
|
||||
*/
|
||||
public accountsDatePeriods = async (
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
datePeriodsType
|
||||
) => {
|
||||
const { AccountTransaction } = this.models;
|
||||
|
||||
return AccountTransaction.query().onBuild((query) => {
|
||||
query.sum('credit as credit');
|
||||
query.sum('debit as debit');
|
||||
query.groupBy('accountId');
|
||||
query.select(['accountId']);
|
||||
|
||||
query.modify('groupByDateFormat', datePeriodsType);
|
||||
query.modify('filterDateRange', fromDate, toDate);
|
||||
query.withGraphFetched('account');
|
||||
|
||||
this.commonFilterBranchesQuery(query);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the opening balance transactions of the report.
|
||||
*/
|
||||
public closingAccountsTotal = async (openingDate: Date | string) => {
|
||||
const { AccountTransaction } = this.models;
|
||||
|
||||
return AccountTransaction.query().onBuild((query) => {
|
||||
query.sum('credit as credit');
|
||||
query.sum('debit as debit');
|
||||
query.groupBy('accountId');
|
||||
query.select(['accountId']);
|
||||
|
||||
query.modify('filterDateRange', null, openingDate);
|
||||
query.withGraphFetched('account');
|
||||
|
||||
this.commonFilterBranchesQuery(query);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Common branches filter query.
|
||||
* @param {Knex.QueryBuilder} query
|
||||
*/
|
||||
private commonFilterBranchesQuery = (query: Knex.QueryBuilder) => {
|
||||
if (!isEmpty(this.query.branchesIds)) {
|
||||
query.modify('filterByBranches', this.query.branchesIds);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
BALANCE_SHEET_SCHEMA_NODE_ID,
|
||||
BALANCE_SHEET_SCHEMA_NODE_TYPE,
|
||||
} from '@/interfaces';
|
||||
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
|
||||
import { FinancialSchema } from '../FinancialSchema';
|
||||
|
||||
|
||||
export const BalanceSheetSchema = (Base) =>
|
||||
class extends R.compose(FinancialSchema)(Base) {
|
||||
/**
|
||||
* Retrieves the balance sheet schema.
|
||||
* @returns
|
||||
*/
|
||||
getSchema = () => {
|
||||
return getBalanceSheetSchema();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the balance sheet report schema.
|
||||
*/
|
||||
export const getBalanceSheetSchema = () => [
|
||||
{
|
||||
name: 'balance_sheet.assets',
|
||||
id: BALANCE_SHEET_SCHEMA_NODE_ID.ASSETS,
|
||||
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE,
|
||||
children: [
|
||||
{
|
||||
name: 'balance_sheet.current_asset',
|
||||
id: BALANCE_SHEET_SCHEMA_NODE_ID.CURRENT_ASSETS,
|
||||
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE,
|
||||
children: [
|
||||
{
|
||||
name: 'balance_sheet.cash_and_cash_equivalents',
|
||||
id: BALANCE_SHEET_SCHEMA_NODE_ID.CASH_EQUIVALENTS,
|
||||
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS,
|
||||
accountsTypes: [ACCOUNT_TYPE.CASH, ACCOUNT_TYPE.BANK],
|
||||
},
|
||||
{
|
||||
name: 'balance_sheet.accounts_receivable',
|
||||
id: BALANCE_SHEET_SCHEMA_NODE_ID.ACCOUNTS_RECEIVABLE,
|
||||
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS,
|
||||
accountsTypes: [ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE],
|
||||
},
|
||||
{
|
||||
name: 'balance_sheet.inventory',
|
||||
id: BALANCE_SHEET_SCHEMA_NODE_ID.INVENTORY,
|
||||
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS,
|
||||
accountsTypes: [ACCOUNT_TYPE.INVENTORY],
|
||||
},
|
||||
{
|
||||
name: 'balance_sheet.other_current_assets',
|
||||
id: BALANCE_SHEET_SCHEMA_NODE_ID.OTHER_CURRENT_ASSET,
|
||||
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS,
|
||||
accountsTypes: [ACCOUNT_TYPE.OTHER_CURRENT_ASSET],
|
||||
},
|
||||
],
|
||||
alwaysShow: true,
|
||||
},
|
||||
{
|
||||
name: 'balance_sheet.fixed_asset',
|
||||
id: BALANCE_SHEET_SCHEMA_NODE_ID.FIXED_ASSET,
|
||||
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS,
|
||||
accountsTypes: [ACCOUNT_TYPE.FIXED_ASSET],
|
||||
},
|
||||
{
|
||||
name: 'balance_sheet.non_current_assets',
|
||||
id: BALANCE_SHEET_SCHEMA_NODE_ID.NON_CURRENT_ASSET,
|
||||
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS,
|
||||
accountsTypes: [ACCOUNT_TYPE.NON_CURRENT_ASSET],
|
||||
},
|
||||
],
|
||||
alwaysShow: true,
|
||||
},
|
||||
{
|
||||
name: 'balance_sheet.liabilities_and_equity',
|
||||
id: BALANCE_SHEET_SCHEMA_NODE_ID.LIABILITY_EQUITY,
|
||||
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE,
|
||||
children: [
|
||||
{
|
||||
name: 'balance_sheet.liabilities',
|
||||
id: BALANCE_SHEET_SCHEMA_NODE_ID.LIABILITY,
|
||||
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE,
|
||||
children: [
|
||||
{
|
||||
name: 'balance_sheet.current_liabilties',
|
||||
id: BALANCE_SHEET_SCHEMA_NODE_ID.CURRENT_LIABILITY,
|
||||
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS,
|
||||
accountsTypes: [
|
||||
ACCOUNT_TYPE.ACCOUNTS_PAYABLE,
|
||||
ACCOUNT_TYPE.TAX_PAYABLE,
|
||||
ACCOUNT_TYPE.CREDIT_CARD,
|
||||
ACCOUNT_TYPE.OTHER_CURRENT_LIABILITY,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'balance_sheet.long_term_liabilities',
|
||||
id: BALANCE_SHEET_SCHEMA_NODE_ID.LOGN_TERM_LIABILITY,
|
||||
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS,
|
||||
accountsTypes: [ACCOUNT_TYPE.LOGN_TERM_LIABILITY],
|
||||
},
|
||||
{
|
||||
name: 'balance_sheet.non_current_liabilities',
|
||||
id: BALANCE_SHEET_SCHEMA_NODE_ID.NON_CURRENT_LIABILITY,
|
||||
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS,
|
||||
accountsTypes: [ACCOUNT_TYPE.NON_CURRENT_LIABILITY],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'balance_sheet.equity',
|
||||
id: BALANCE_SHEET_SCHEMA_NODE_ID.EQUITY,
|
||||
type: BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS,
|
||||
accountsTypes: [ACCOUNT_TYPE.EQUITY],
|
||||
},
|
||||
],
|
||||
alwaysShow: true,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,138 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
IBalanceSheetStatementService,
|
||||
IBalanceSheetQuery,
|
||||
IBalanceSheetStatement,
|
||||
IBalanceSheetMeta,
|
||||
} from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import Journal from '@/services/Accounting/JournalPoster';
|
||||
import BalanceSheetStatement from './BalanceSheet';
|
||||
import InventoryService from '@/services/Inventory/Inventory';
|
||||
import { parseBoolean } from 'utils';
|
||||
import { Tenant } from '@/system/models';
|
||||
import BalanceSheetRepository from './BalanceSheetRepository';
|
||||
|
||||
@Service()
|
||||
export default class BalanceSheetStatementService
|
||||
implements IBalanceSheetStatementService
|
||||
{
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
inventoryService: InventoryService;
|
||||
|
||||
/**
|
||||
* Defaults balance sheet filter query.
|
||||
* @return {IBalanceSheetQuery}
|
||||
*/
|
||||
get defaultQuery(): IBalanceSheetQuery {
|
||||
return {
|
||||
displayColumnsType: 'total',
|
||||
displayColumnsBy: 'month',
|
||||
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
divideOn1000: false,
|
||||
showZero: false,
|
||||
formatMoney: 'total',
|
||||
negativeFormat: 'mines',
|
||||
},
|
||||
noneZero: false,
|
||||
noneTransactions: false,
|
||||
|
||||
basis: 'cash',
|
||||
accountIds: [],
|
||||
|
||||
percentageOfColumn: false,
|
||||
percentageOfRow: false,
|
||||
|
||||
previousPeriod: false,
|
||||
previousPeriodAmountChange: false,
|
||||
previousPeriodPercentageChange: false,
|
||||
|
||||
previousYear: false,
|
||||
previousYearAmountChange: false,
|
||||
previousYearPercentageChange: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the balance sheet meta.
|
||||
* @param {number} tenantId -
|
||||
* @returns {IBalanceSheetMeta}
|
||||
*/
|
||||
private reportMetadata(tenantId: number): IBalanceSheetMeta {
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
|
||||
const isCostComputeRunning =
|
||||
this.inventoryService.isItemsCostComputeRunning(tenantId);
|
||||
|
||||
const organizationName = settings.get({
|
||||
group: 'organization',
|
||||
key: 'name',
|
||||
});
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
|
||||
return {
|
||||
isCostComputeRunning: parseBoolean(isCostComputeRunning, false),
|
||||
organizationName,
|
||||
baseCurrency,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve balance sheet statement.
|
||||
* -------------
|
||||
* @param {number} tenantId
|
||||
* @param {IBalanceSheetQuery} query
|
||||
*
|
||||
* @return {IBalanceSheetStatement}
|
||||
*/
|
||||
public async balanceSheet(
|
||||
tenantId: number,
|
||||
query: IBalanceSheetQuery
|
||||
): Promise<IBalanceSheetStatement> {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
|
||||
const tenant = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
const models = this.tenancy.models(tenantId);
|
||||
const balanceSheetRepo = new BalanceSheetRepository(models, filter);
|
||||
|
||||
await balanceSheetRepo.asyncInitialize();
|
||||
|
||||
// Balance sheet report instance.
|
||||
const balanceSheetInstanace = new BalanceSheetStatement(
|
||||
filter,
|
||||
balanceSheetRepo,
|
||||
tenant.metadata.baseCurrency,
|
||||
i18n
|
||||
);
|
||||
// Balance sheet data.
|
||||
const balanceSheetData = balanceSheetInstanace.reportData();
|
||||
|
||||
return {
|
||||
data: balanceSheetData,
|
||||
query: filter,
|
||||
meta: this.reportMetadata(tenantId),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
IBalanceSheetStatementData,
|
||||
ITableColumnAccessor,
|
||||
IBalanceSheetQuery,
|
||||
ITableColumn,
|
||||
ITableRow,
|
||||
BALANCE_SHEET_SCHEMA_NODE_TYPE,
|
||||
IBalanceSheetDataNode,
|
||||
IBalanceSheetSchemaNode,
|
||||
} from '@/interfaces';
|
||||
import { tableRowMapper } from 'utils';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
import { BalanceSheetComparsionPreviousYear } from './BalanceSheetComparsionPreviousYear';
|
||||
import { IROW_TYPE, DISPLAY_COLUMNS_BY } from './constants';
|
||||
import { BalanceSheetComparsionPreviousPeriod } from './BalanceSheetComparsionPreviousPeriod';
|
||||
import { BalanceSheetPercentage } from './BalanceSheetPercentage';
|
||||
import { FinancialSheetStructure } from '../FinancialSheetStructure';
|
||||
import { BalanceSheetBase } from './BalanceSheetBase';
|
||||
import { BalanceSheetTablePercentage } from './BalanceSheetTablePercentage';
|
||||
import { BalanceSheetTablePreviousYear } from './BalanceSheetTablePreviousYear';
|
||||
import { BalanceSheetTablePreviousPeriod } from './BalanceSheetTablePreviousPeriod';
|
||||
import { FinancialTable } from '../FinancialTable';
|
||||
import { BalanceSheetQuery } from './BalanceSheetQuery';
|
||||
import { BalanceSheetTableDatePeriods } from './BalanceSheetTableDatePeriods';
|
||||
|
||||
export default class BalanceSheetTable extends R.compose(
|
||||
BalanceSheetTablePreviousPeriod,
|
||||
BalanceSheetTablePreviousYear,
|
||||
BalanceSheetTableDatePeriods,
|
||||
BalanceSheetTablePercentage,
|
||||
BalanceSheetComparsionPreviousYear,
|
||||
BalanceSheetComparsionPreviousPeriod,
|
||||
BalanceSheetPercentage,
|
||||
FinancialSheetStructure,
|
||||
FinancialTable,
|
||||
BalanceSheetBase
|
||||
)(FinancialSheet) {
|
||||
/**
|
||||
* @param {}
|
||||
*/
|
||||
reportData: IBalanceSheetStatementData;
|
||||
|
||||
/**
|
||||
* Balance sheet query.
|
||||
* @parma {}
|
||||
*/
|
||||
query: BalanceSheetQuery;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {IBalanceSheetStatementData} reportData -
|
||||
* @param {IBalanceSheetQuery} query -
|
||||
*/
|
||||
constructor(
|
||||
reportData: IBalanceSheetStatementData,
|
||||
query: IBalanceSheetQuery,
|
||||
i18n: any
|
||||
) {
|
||||
super();
|
||||
|
||||
this.reportData = reportData;
|
||||
this.query = new BalanceSheetQuery(query);
|
||||
this.i18n = i18n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines the node type of the given schema node.
|
||||
* @param {IBalanceSheetStructureSection} node -
|
||||
* @param {string} type -
|
||||
* @return {boolean}
|
||||
*/
|
||||
protected isNodeType = R.curry(
|
||||
(type: string, node: IBalanceSheetSchemaNode): boolean => {
|
||||
return node.nodeType === type;
|
||||
}
|
||||
);
|
||||
|
||||
// -------------------------
|
||||
// # Accessors.
|
||||
// -------------------------
|
||||
/**
|
||||
* Retrieve the common columns for all report nodes.
|
||||
* @param {ITableColumnAccessor[]}
|
||||
*/
|
||||
private commonColumnsAccessors = (): ITableColumnAccessor[] => {
|
||||
return R.compose(
|
||||
R.concat([{ key: 'name', accessor: 'name' }]),
|
||||
R.ifElse(
|
||||
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
|
||||
R.concat(this.datePeriodsColumnsAccessors()),
|
||||
R.concat(this.totalColumnAccessor())
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the total column accessor.
|
||||
* @return {ITableColumnAccessor[]}
|
||||
*/
|
||||
private totalColumnAccessor = (): ITableColumnAccessor[] => {
|
||||
return R.pipe(
|
||||
R.concat(this.previousPeriodColumnAccessor()),
|
||||
R.concat(this.previousYearColumnAccessor()),
|
||||
R.concat(this.percentageColumnsAccessor()),
|
||||
R.concat([{ key: 'total', accessor: 'total.formattedAmount' }])
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param node
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
private aggregateNodeTableRowsMapper = (node): ITableRow => {
|
||||
const columns = this.commonColumnsAccessors();
|
||||
const meta = {
|
||||
rowTypes: [IROW_TYPE.AGGREGATE],
|
||||
id: node.id,
|
||||
};
|
||||
return tableRowMapper(node, columns, meta);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param node
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
private accountsNodeTableRowsMapper = (node): ITableRow => {
|
||||
const columns = this.commonColumnsAccessors();
|
||||
const meta = {
|
||||
rowTypes: [IROW_TYPE.ACCOUNTS],
|
||||
id: node.id,
|
||||
};
|
||||
return tableRowMapper(node, columns, meta);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {} node
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
private accountNodeTableRowsMapper = (node): ITableRow => {
|
||||
const columns = this.commonColumnsAccessors();
|
||||
|
||||
const meta = {
|
||||
rowTypes: [IROW_TYPE.ACCOUNT],
|
||||
id: node.id,
|
||||
};
|
||||
return tableRowMapper(node, columns, meta);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mappes the given report node to table rows.
|
||||
* @param {IBalanceSheetDataNode} node -
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
private nodeToTableRowsMapper = (node: IBalanceSheetDataNode): ITableRow => {
|
||||
return R.cond([
|
||||
[
|
||||
this.isNodeType(BALANCE_SHEET_SCHEMA_NODE_TYPE.AGGREGATE),
|
||||
this.aggregateNodeTableRowsMapper,
|
||||
],
|
||||
[
|
||||
this.isNodeType(BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNTS),
|
||||
this.accountsNodeTableRowsMapper,
|
||||
],
|
||||
[
|
||||
this.isNodeType(BALANCE_SHEET_SCHEMA_NODE_TYPE.ACCOUNT),
|
||||
this.accountNodeTableRowsMapper,
|
||||
],
|
||||
])(node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mappes the given report sections to table rows.
|
||||
* @param {IBalanceSheetDataNode[]} nodes -
|
||||
* @return {ITableRow}
|
||||
*/
|
||||
private nodesToTableRowsMapper = (
|
||||
nodes: IBalanceSheetDataNode[]
|
||||
): ITableRow[] => {
|
||||
return this.mapNodesDeep(nodes, this.nodeToTableRowsMapper);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the total children columns.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
private totalColumnChildren = (): ITableColumn[] => {
|
||||
return R.compose(
|
||||
R.unless(
|
||||
R.isEmpty,
|
||||
R.concat([{ key: 'total', Label: this.i18n.__('balance_sheet.total') }])
|
||||
),
|
||||
R.concat(this.percentageColumns()),
|
||||
R.concat(this.getPreviousYearColumns()),
|
||||
R.concat(this.previousPeriodColumns())
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the total column.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
private totalColumn = (): ITableColumn[] => {
|
||||
return [
|
||||
{
|
||||
key: 'total',
|
||||
label: this.i18n.__('balance_sheet.total'),
|
||||
children: this.totalColumnChildren(),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the report table rows.
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
public tableRows = (): ITableRow[] => {
|
||||
return R.compose(
|
||||
this.addTotalRows,
|
||||
this.nodesToTableRowsMapper
|
||||
)(this.reportData);
|
||||
};
|
||||
|
||||
// -------------------------
|
||||
// # Columns.
|
||||
// -------------------------
|
||||
/**
|
||||
* Retrieve the report table columns.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
public tableColumns = (): ITableColumn[] => {
|
||||
return R.compose(
|
||||
this.tableColumnsCellIndexing,
|
||||
R.concat([
|
||||
{ key: 'name', label: this.i18n.__('balance_sheet.account_name') },
|
||||
]),
|
||||
R.ifElse(
|
||||
this.query.isDatePeriodsColumnsType,
|
||||
R.concat(this.datePeriodsColumns()),
|
||||
R.concat(this.totalColumn())
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import * as R from 'ramda';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
ITableColumn,
|
||||
IDateRange,
|
||||
ICashFlowDateRange,
|
||||
ITableColumnAccessor,
|
||||
} from '@/interfaces';
|
||||
import { FinancialDatePeriods } from '../FinancialDatePeriods';
|
||||
|
||||
export const BalanceSheetTableDatePeriods = (Base) =>
|
||||
class extends R.compose(FinancialDatePeriods)(Base) {
|
||||
/**
|
||||
* Retrieves the date periods based on the report query.
|
||||
* @returns {IDateRange[]}
|
||||
*/
|
||||
get datePeriods() {
|
||||
return this.getDateRanges(
|
||||
this.query.fromDate,
|
||||
this.query.toDate,
|
||||
this.query.displayColumnsBy
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the formatted column label from the given date range.
|
||||
* @param {ICashFlowDateRange} dateRange -
|
||||
* @return {string}
|
||||
*/
|
||||
private formatColumnLabel = (dateRange: ICashFlowDateRange) => {
|
||||
const monthFormat = (range) => moment(range.toDate).format('YYYY-MM');
|
||||
const yearFormat = (range) => moment(range.toDate).format('YYYY');
|
||||
const dayFormat = (range) => moment(range.toDate).format('YYYY-MM-DD');
|
||||
|
||||
const conditions = [
|
||||
['month', monthFormat],
|
||||
['year', yearFormat],
|
||||
['day', dayFormat],
|
||||
['quarter', monthFormat],
|
||||
['week', dayFormat],
|
||||
];
|
||||
const conditionsPairs = R.map(
|
||||
([type, formatFn]) => [
|
||||
R.always(this.query.isDisplayColumnsBy(type)),
|
||||
formatFn,
|
||||
],
|
||||
conditions
|
||||
);
|
||||
return R.compose(R.cond(conditionsPairs))(dateRange);
|
||||
};
|
||||
|
||||
// -------------------------
|
||||
// # Accessors.
|
||||
// -------------------------
|
||||
/**
|
||||
* Date period columns accessor.
|
||||
* @param {IDateRange} dateRange -
|
||||
* @param {number} index -
|
||||
*/
|
||||
private datePeriodColumnsAccessor = R.curry(
|
||||
(dateRange: IDateRange, index: number) => {
|
||||
return R.pipe(
|
||||
R.concat(this.previousPeriodHorizColumnAccessors(index)),
|
||||
R.concat(this.previousYearHorizontalColumnAccessors(index)),
|
||||
R.concat(this.percetangeDatePeriodColumnsAccessor(index)),
|
||||
R.concat([
|
||||
{
|
||||
key: `date-range-${index}`,
|
||||
accessor: `horizontalTotals[${index}].total.formattedAmount`,
|
||||
},
|
||||
])
|
||||
)([]);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieve the date periods columns accessors.
|
||||
* @returns {ITableColumnAccessor[]}
|
||||
*/
|
||||
protected datePeriodsColumnsAccessors = (): ITableColumnAccessor[] => {
|
||||
return R.compose(
|
||||
R.flatten,
|
||||
R.addIndex(R.map)(this.datePeriodColumnsAccessor)
|
||||
)(this.datePeriods);
|
||||
};
|
||||
|
||||
// -------------------------
|
||||
// # Columns.
|
||||
// -------------------------
|
||||
/**
|
||||
*
|
||||
* @param {number} index
|
||||
* @param {} dateRange
|
||||
* @returns {}
|
||||
*/
|
||||
private datePeriodChildrenColumns = (
|
||||
index: number,
|
||||
dateRange: IDateRange
|
||||
) => {
|
||||
return R.compose(
|
||||
R.unless(
|
||||
R.isEmpty,
|
||||
R.concat([
|
||||
{ key: `total`, label: this.i18n.__('balance_sheet.total') },
|
||||
])
|
||||
),
|
||||
R.concat(this.percentageColumns()),
|
||||
R.concat(this.getPreviousYearHorizontalColumns(dateRange)),
|
||||
R.concat(this.previousPeriodHorizontalColumns(dateRange))
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param dateRange
|
||||
* @param index
|
||||
* @returns
|
||||
*/
|
||||
private datePeriodColumn = (
|
||||
dateRange: IDateRange,
|
||||
index: number
|
||||
): ITableColumn => {
|
||||
return {
|
||||
key: `date-range-${index}`,
|
||||
label: this.formatColumnLabel(dateRange),
|
||||
children: this.datePeriodChildrenColumns(index, dateRange),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Date periods columns.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected datePeriodsColumns = (): ITableColumn[] => {
|
||||
return this.datePeriods.map(this.datePeriodColumn);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
import * as R from 'ramda';
|
||||
import { ITableColumn } from '@/interfaces';
|
||||
|
||||
export const BalanceSheetTablePercentage = (Base) =>
|
||||
class extends Base {
|
||||
// --------------------
|
||||
// # Columns
|
||||
// --------------------
|
||||
/**
|
||||
* Retrieve percentage of column/row columns.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected percentageColumns = (): ITableColumn[] => {
|
||||
return R.pipe(
|
||||
R.when(
|
||||
this.query.isColumnsPercentageActive,
|
||||
R.append({
|
||||
key: 'percentage_of_column',
|
||||
label: this.i18n.__('balance_sheet.percentage_of_column'),
|
||||
})
|
||||
),
|
||||
R.when(
|
||||
this.query.isRowsPercentageActive,
|
||||
R.append({
|
||||
key: 'percentage_of_row',
|
||||
label: this.i18n.__('balance_sheet.percentage_of_row'),
|
||||
})
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
|
||||
// --------------------
|
||||
// # Accessors
|
||||
// --------------------
|
||||
/**
|
||||
* Retrieves percentage of column/row accessors.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected percentageColumnsAccessor = (): ITableColumn[] => {
|
||||
return R.pipe(
|
||||
R.when(
|
||||
this.query.isColumnsPercentageActive,
|
||||
R.append({
|
||||
key: 'percentage_of_column',
|
||||
accessor: 'percentageColumn.formattedAmount',
|
||||
})
|
||||
),
|
||||
R.when(
|
||||
this.query.isRowsPercentageActive,
|
||||
R.append({
|
||||
key: 'percentage_of_row',
|
||||
accessor: 'percentageRow.formattedAmount',
|
||||
})
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Percentage columns accessors for date period columns.
|
||||
* @param {number} index
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected percetangeDatePeriodColumnsAccessor = (
|
||||
index: number
|
||||
): ITableColumn[] => {
|
||||
return R.pipe(
|
||||
R.when(
|
||||
this.query.isColumnsPercentageActive,
|
||||
R.append({
|
||||
key: `percentage_of_column-${index}`,
|
||||
accessor: `horizontalTotals[${index}].percentageColumn.formattedAmount`,
|
||||
})
|
||||
),
|
||||
R.when(
|
||||
this.query.isRowsPercentageActive,
|
||||
R.append({
|
||||
key: `percentage_of_row-${index}`,
|
||||
accessor: `horizontalTotals[${index}].percentageRow.formattedAmount`,
|
||||
})
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
import * as R from 'ramda';
|
||||
import { IDateRange, ITableColumn } from '@/interfaces';
|
||||
import { BalanceSheetQuery } from './BalanceSheetQuery';
|
||||
import { FinancialTablePreviousPeriod } from '../FinancialTablePreviousPeriod';
|
||||
import { FinancialDateRanges } from '../FinancialDateRanges';
|
||||
|
||||
export const BalanceSheetTablePreviousPeriod = (Base) =>
|
||||
class extends R.compose(
|
||||
FinancialTablePreviousPeriod,
|
||||
FinancialDateRanges
|
||||
)(Base) {
|
||||
readonly query: BalanceSheetQuery;
|
||||
|
||||
// --------------------
|
||||
// # Columns
|
||||
// --------------------
|
||||
/**
|
||||
* Retrieves the previous period columns.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected previousPeriodColumns = (
|
||||
dateRange?: IDateRange
|
||||
): ITableColumn[] => {
|
||||
return R.pipe(
|
||||
// Previous period columns.
|
||||
R.when(
|
||||
this.query.isPreviousPeriodActive,
|
||||
R.append(this.getPreviousPeriodTotalColumn(dateRange))
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodChangeActive,
|
||||
R.append(this.getPreviousPeriodChangeColumn())
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodPercentageActive,
|
||||
R.append(this.getPreviousPeriodPercentageColumn())
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Previous period for date periods
|
||||
* @param {IDateRange} dateRange
|
||||
* @returns {ITableColumn}
|
||||
*/
|
||||
protected previousPeriodHorizontalColumns = (
|
||||
dateRange: IDateRange
|
||||
): ITableColumn[] => {
|
||||
const PPDateRange = this.getPPDatePeriodDateRange(
|
||||
dateRange.fromDate,
|
||||
dateRange.toDate,
|
||||
this.query.displayColumnsBy
|
||||
);
|
||||
return this.previousPeriodColumns({
|
||||
fromDate: PPDateRange.fromDate,
|
||||
toDate: PPDateRange.toDate,
|
||||
});
|
||||
};
|
||||
|
||||
// --------------------
|
||||
// # Accessors
|
||||
// --------------------
|
||||
/**
|
||||
* Retrieves previous period columns accessors.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected previousPeriodColumnAccessor = (): ITableColumn[] => {
|
||||
return R.pipe(
|
||||
// Previous period columns.
|
||||
R.when(
|
||||
this.query.isPreviousPeriodActive,
|
||||
R.append(this.getPreviousPeriodTotalAccessor())
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodChangeActive,
|
||||
R.append(this.getPreviousPeriodChangeAccessor())
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodPercentageActive,
|
||||
R.append(this.getPreviousPeriodPercentageAccessor())
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} index
|
||||
* @returns
|
||||
*/
|
||||
protected previousPeriodHorizColumnAccessors = (
|
||||
index: number
|
||||
): ITableColumn[] => {
|
||||
return R.pipe(
|
||||
// Previous period columns.
|
||||
R.when(
|
||||
this.query.isPreviousPeriodActive,
|
||||
R.append(this.getPreviousPeriodTotalHorizAccessor(index))
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodChangeActive,
|
||||
R.append(this.getPreviousPeriodChangeHorizAccessor(index))
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodPercentageActive,
|
||||
R.append(this.getPreviousPeriodPercentageHorizAccessor(index))
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import * as R from 'ramda';
|
||||
import { IDateRange, ITableColumn } from '@/interfaces';
|
||||
import { FinancialTablePreviousYear } from '../FinancialTablePreviousYear';
|
||||
import { FinancialDateRanges } from '../FinancialDateRanges';
|
||||
|
||||
export const BalanceSheetTablePreviousYear = (Base) =>
|
||||
class extends R.compose(FinancialTablePreviousYear, FinancialDateRanges)(Base) {
|
||||
// --------------------
|
||||
// # Columns.
|
||||
// --------------------
|
||||
/**
|
||||
* Retrieves pervious year comparison columns.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected getPreviousYearColumns = (
|
||||
dateRange?: IDateRange
|
||||
): ITableColumn[] => {
|
||||
return R.pipe(
|
||||
// Previous year columns.
|
||||
R.when(
|
||||
this.query.isPreviousYearActive,
|
||||
R.append(this.getPreviousYearTotalColumn(dateRange))
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearChangeActive,
|
||||
R.append(this.getPreviousYearChangeColumn())
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearPercentageActive,
|
||||
R.append(this.getPreviousYearPercentageColumn())
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IDateRange} dateRange
|
||||
* @returns
|
||||
*/
|
||||
protected getPreviousYearHorizontalColumns = (dateRange: IDateRange) => {
|
||||
const PYDateRange = this.getPreviousYearDateRange(
|
||||
dateRange.fromDate,
|
||||
dateRange.toDate
|
||||
);
|
||||
return this.getPreviousYearColumns(PYDateRange);
|
||||
};
|
||||
|
||||
// --------------------
|
||||
// # Accessors.
|
||||
// --------------------
|
||||
/**
|
||||
* Retrieves previous year columns accessors.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected previousYearColumnAccessor = (): ITableColumn[] => {
|
||||
return R.pipe(
|
||||
// Previous year columns.
|
||||
R.when(
|
||||
this.query.isPreviousYearActive,
|
||||
R.append(this.getPreviousYearTotalAccessor())
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearChangeActive,
|
||||
R.append(this.getPreviousYearChangeAccessor())
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearPercentageActive,
|
||||
R.append(this.getPreviousYearPercentageAccessor())
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Previous year period column accessor.
|
||||
* @param {number} index
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected previousYearHorizontalColumnAccessors = (
|
||||
index: number
|
||||
): ITableColumn[] => {
|
||||
return R.pipe(
|
||||
// Previous year columns.
|
||||
R.when(
|
||||
this.query.isPreviousYearActive,
|
||||
R.append(this.getPreviousYearTotalHorizAccessor(index))
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearChangeActive,
|
||||
R.append(this.getPreviousYearChangeHorizAccessor(index))
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearPercentageActive,
|
||||
R.append(this.getPreviousYearPercentageHorizAccessor(index))
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import * as R from 'ramda';
|
||||
|
||||
export const BalanceSheetTotal = (Base: any) => class extends Base {};
|
||||
@@ -0,0 +1,13 @@
|
||||
export const MAP_CONFIG = { childrenPath: 'children', pathFormat: 'array' };
|
||||
|
||||
export const DISPLAY_COLUMNS_BY = {
|
||||
DATE_PERIODS: 'date_periods',
|
||||
TOTAL: 'total',
|
||||
};
|
||||
|
||||
export enum IROW_TYPE {
|
||||
AGGREGATE = 'AGGREGATE',
|
||||
ACCOUNTS = 'ACCOUNTS',
|
||||
ACCOUNT = 'ACCOUNT',
|
||||
TOTAL = 'TOTAL',
|
||||
}
|
||||
@@ -0,0 +1,703 @@
|
||||
import * as R from 'ramda';
|
||||
import { defaultTo, map, set, sumBy, isEmpty, mapValues, get } from 'lodash';
|
||||
import * as mathjs from 'mathjs';
|
||||
import moment from 'moment';
|
||||
import { compose } from 'lodash/fp';
|
||||
import {
|
||||
IAccount,
|
||||
ILedger,
|
||||
INumberFormatQuery,
|
||||
ICashFlowSchemaSection,
|
||||
ICashFlowStatementQuery,
|
||||
ICashFlowStatementNetIncomeSection,
|
||||
ICashFlowStatementAccountSection,
|
||||
ICashFlowSchemaSectionAccounts,
|
||||
ICashFlowStatementAccountMeta,
|
||||
ICashFlowSchemaAccountRelation,
|
||||
ICashFlowStatementSectionType,
|
||||
ICashFlowStatementData,
|
||||
ICashFlowSchemaTotalSection,
|
||||
ICashFlowStatementTotalSection,
|
||||
ICashFlowStatementSection,
|
||||
ICashFlowCashBeginningNode,
|
||||
ICashFlowStatementAggregateSection,
|
||||
} from '@/interfaces';
|
||||
import CASH_FLOW_SCHEMA from './schema';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
import { transformToMapBy, accumSum } from 'utils';
|
||||
import { ACCOUNT_ROOT_TYPE } from '@/data/AccountTypes';
|
||||
import { CashFlowStatementDatePeriods } from './CashFlowDatePeriods';
|
||||
import I18nService from '@/services/I18n/I18nService';
|
||||
import { DISPLAY_COLUMNS_BY } from './constants';
|
||||
import { FinancialSheetStructure } from '../FinancialSheetStructure';
|
||||
|
||||
export default class CashFlowStatement extends compose(
|
||||
CashFlowStatementDatePeriods,
|
||||
FinancialSheetStructure
|
||||
)(FinancialSheet) {
|
||||
readonly baseCurrency: string;
|
||||
readonly i18n: I18nService;
|
||||
readonly sectionsByIds = {};
|
||||
readonly cashFlowSchemaMap: Map<string, ICashFlowSchemaSection>;
|
||||
readonly cashFlowSchemaSeq: Array<string>;
|
||||
readonly accountByTypeMap: Map<string, IAccount[]>;
|
||||
readonly accountsByRootType: Map<string, IAccount[]>;
|
||||
readonly ledger: ILedger;
|
||||
readonly cashLedger: ILedger;
|
||||
readonly netIncomeLedger: ILedger;
|
||||
readonly query: ICashFlowStatementQuery;
|
||||
readonly numberFormat: INumberFormatQuery;
|
||||
readonly comparatorDateType: string;
|
||||
readonly dateRangeSet: { fromDate: Date; toDate: Date }[];
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @constructor
|
||||
*/
|
||||
constructor(
|
||||
accounts: IAccount[],
|
||||
ledger: ILedger,
|
||||
cashLedger: ILedger,
|
||||
netIncomeLedger: ILedger,
|
||||
query: ICashFlowStatementQuery,
|
||||
baseCurrency: string,
|
||||
i18n
|
||||
) {
|
||||
super();
|
||||
|
||||
this.baseCurrency = baseCurrency;
|
||||
this.i18n = i18n;
|
||||
this.ledger = ledger;
|
||||
this.cashLedger = cashLedger;
|
||||
this.netIncomeLedger = netIncomeLedger;
|
||||
this.accountByTypeMap = transformToMapBy(accounts, 'accountType');
|
||||
this.accountsByRootType = transformToMapBy(accounts, 'accountRootType');
|
||||
this.query = query;
|
||||
this.numberFormat = this.query.numberFormat;
|
||||
this.dateRangeSet = [];
|
||||
this.comparatorDateType =
|
||||
query.displayColumnsType === 'total' ? 'day' : query.displayColumnsBy;
|
||||
|
||||
this.initDateRangeCollection();
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
// # GENERAL UTILITIES
|
||||
// --------------------------------------------
|
||||
/**
|
||||
* Retrieve the expense accounts ids.
|
||||
* @return {number[]}
|
||||
*/
|
||||
private getAccountsIdsByType = (accountType: string): number[] => {
|
||||
const expenseAccounts = this.accountsByRootType.get(accountType);
|
||||
const expenseAccountsIds = map(expenseAccounts, 'id');
|
||||
|
||||
return expenseAccountsIds;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines the given display columns by type.
|
||||
* @param {string} displayColumnsBy
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private isDisplayColumnsBy = (displayColumnsBy: string): boolean => {
|
||||
return this.query.displayColumnsType === displayColumnsBy;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adjustments the given amount.
|
||||
* @param {string} direction
|
||||
* @param {number} amount -
|
||||
* @return {number}
|
||||
*/
|
||||
private amountAdjustment = (direction: 'mines' | 'plus', amount): number => {
|
||||
return R.when(
|
||||
R.always(R.equals(direction, 'mines')),
|
||||
R.multiply(-1)
|
||||
)(amount);
|
||||
};
|
||||
|
||||
// --------------------------------------------
|
||||
// # NET INCOME NODE
|
||||
// --------------------------------------------
|
||||
/**
|
||||
* Retrieve the accounts net income.
|
||||
* @returns {number} - Amount of net income.
|
||||
*/
|
||||
private getAccountsNetIncome(): number {
|
||||
// Mapping income/expense accounts ids.
|
||||
const incomeAccountsIds = this.getAccountsIdsByType(
|
||||
ACCOUNT_ROOT_TYPE.INCOME
|
||||
);
|
||||
const expenseAccountsIds = this.getAccountsIdsByType(
|
||||
ACCOUNT_ROOT_TYPE.EXPENSE
|
||||
);
|
||||
// Income closing balance.
|
||||
const incomeClosingBalance = accumSum(incomeAccountsIds, (id) =>
|
||||
this.netIncomeLedger.whereAccountId(id).getClosingBalance()
|
||||
);
|
||||
// Expense closing balance.
|
||||
const expenseClosingBalance = accumSum(expenseAccountsIds, (id) =>
|
||||
this.netIncomeLedger.whereAccountId(id).getClosingBalance()
|
||||
);
|
||||
// Net income = income - expenses.
|
||||
const netIncome = incomeClosingBalance - expenseClosingBalance;
|
||||
|
||||
return netIncome;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the net income section from the given section schema.
|
||||
* @param {ICashFlowSchemaSection} nodeSchema - Report section schema.
|
||||
* @returns {ICashFlowStatementNetIncomeSection}
|
||||
*/
|
||||
private netIncomeSectionMapper = (
|
||||
nodeSchema: ICashFlowSchemaSection
|
||||
): ICashFlowStatementNetIncomeSection => {
|
||||
const netIncome = this.getAccountsNetIncome();
|
||||
|
||||
const node = {
|
||||
id: nodeSchema.id,
|
||||
label: this.i18n.__(nodeSchema.label),
|
||||
total: this.getAmountMeta(netIncome),
|
||||
sectionType: ICashFlowStatementSectionType.NET_INCOME,
|
||||
};
|
||||
return R.compose(
|
||||
R.when(
|
||||
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
|
||||
this.assocPeriodsToNetIncomeNode
|
||||
)
|
||||
)(node);
|
||||
};
|
||||
|
||||
// --------------------------------------------
|
||||
// # ACCOUNT NODE
|
||||
// --------------------------------------------
|
||||
/**
|
||||
* Retrieve account meta.
|
||||
* @param {ICashFlowSchemaAccountRelation} relation - Account relation.
|
||||
* @param {IAccount} account -
|
||||
* @returns {ICashFlowStatementAccountMeta}
|
||||
*/
|
||||
private accountMetaMapper = (
|
||||
relation: ICashFlowSchemaAccountRelation,
|
||||
account: IAccount
|
||||
): ICashFlowStatementAccountMeta => {
|
||||
// Retrieve the closing balance of the given account.
|
||||
const getClosingBalance = (id) =>
|
||||
this.ledger.whereAccountId(id).getClosingBalance();
|
||||
|
||||
const closingBalance = R.compose(
|
||||
// Multiplies the amount by -1 in case the relation in mines.
|
||||
R.curry(this.amountAdjustment)(relation.direction)
|
||||
)(getClosingBalance(account.id));
|
||||
|
||||
const node = {
|
||||
id: account.id,
|
||||
code: account.code,
|
||||
label: account.name,
|
||||
accountType: account.accountType,
|
||||
adjusmentType: relation.direction,
|
||||
total: this.getAmountMeta(closingBalance),
|
||||
sectionType: ICashFlowStatementSectionType.ACCOUNT,
|
||||
};
|
||||
return R.compose(
|
||||
R.when(
|
||||
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
|
||||
this.assocPeriodsToAccountNode
|
||||
)
|
||||
)(node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve accounts sections by the given schema relation.
|
||||
* @param {ICashFlowSchemaAccountRelation} relation
|
||||
* @returns {ICashFlowStatementAccountMeta[]}
|
||||
*/
|
||||
private getAccountsBySchemaRelation = (
|
||||
relation: ICashFlowSchemaAccountRelation
|
||||
): ICashFlowStatementAccountMeta[] => {
|
||||
const accounts = defaultTo(this.accountByTypeMap.get(relation.type), []);
|
||||
const accountMetaMapper = R.curry(this.accountMetaMapper)(relation);
|
||||
return R.map(accountMetaMapper)(accounts);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the accounts meta.
|
||||
* @param {string[]} types
|
||||
* @returns {ICashFlowStatementAccountMeta[]}
|
||||
*/
|
||||
private getAccountsBySchemaRelations = (
|
||||
relations: ICashFlowSchemaAccountRelation[]
|
||||
): ICashFlowStatementAccountMeta[] => {
|
||||
return R.pipe(
|
||||
R.append(R.map(this.getAccountsBySchemaRelation)(relations)),
|
||||
R.flatten
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the accounts total
|
||||
* @param {ICashFlowStatementAccountMeta[]} accounts
|
||||
* @returns {number}
|
||||
*/
|
||||
private getAccountsMetaTotal = (
|
||||
accounts: ICashFlowStatementAccountMeta[]
|
||||
): number => {
|
||||
return sumBy(accounts, 'total.amount');
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the accounts section from the section schema.
|
||||
* @param {ICashFlowSchemaSectionAccounts} sectionSchema
|
||||
* @returns {ICashFlowStatementAccountSection}
|
||||
*/
|
||||
private accountsSectionParser = (
|
||||
sectionSchema: ICashFlowSchemaSectionAccounts
|
||||
): ICashFlowStatementAccountSection => {
|
||||
const { accountsRelations } = sectionSchema;
|
||||
|
||||
const accounts = this.getAccountsBySchemaRelations(accountsRelations);
|
||||
const accountsTotal = this.getAccountsMetaTotal(accounts);
|
||||
const total = this.getTotalAmountMeta(accountsTotal);
|
||||
|
||||
const node = {
|
||||
sectionType: ICashFlowStatementSectionType.ACCOUNTS,
|
||||
id: sectionSchema.id,
|
||||
label: this.i18n.__(sectionSchema.label),
|
||||
footerLabel: this.i18n.__(sectionSchema.footerLabel),
|
||||
children: accounts,
|
||||
total,
|
||||
};
|
||||
return R.compose(
|
||||
R.when(
|
||||
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
|
||||
this.assocPeriodsToAggregateNode
|
||||
)
|
||||
)(node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines the schema section type.
|
||||
* @param {string} type
|
||||
* @param {ICashFlowSchemaSection} section
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private isSchemaSectionType = R.curry(
|
||||
(type: string, section: ICashFlowSchemaSection): boolean => {
|
||||
return type === section.sectionType;
|
||||
}
|
||||
);
|
||||
|
||||
// --------------------------------------------
|
||||
// # AGGREGATE NODE
|
||||
// --------------------------------------------
|
||||
/**
|
||||
* Aggregate schema node parser to aggregate report node.
|
||||
* @param {ICashFlowSchemaSection} schemaSection
|
||||
* @returns {ICashFlowStatementAggregateSection}
|
||||
*/
|
||||
private regularSectionParser = R.curry(
|
||||
(
|
||||
children,
|
||||
schemaSection: ICashFlowSchemaSection
|
||||
): ICashFlowStatementAggregateSection => {
|
||||
const node = {
|
||||
id: schemaSection.id,
|
||||
label: this.i18n.__(schemaSection.label),
|
||||
footerLabel: this.i18n.__(schemaSection.footerLabel),
|
||||
sectionType: ICashFlowStatementSectionType.AGGREGATE,
|
||||
children,
|
||||
};
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.isSchemaSectionType(ICashFlowStatementSectionType.AGGREGATE),
|
||||
this.assocRegularSectionTotal
|
||||
),
|
||||
R.when(
|
||||
this.isSchemaSectionType(ICashFlowStatementSectionType.AGGREGATE),
|
||||
R.when(
|
||||
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
|
||||
this.assocPeriodsToAggregateNode
|
||||
)
|
||||
)
|
||||
)(node);
|
||||
}
|
||||
);
|
||||
|
||||
private transformSectionsToMap = (sections: ICashFlowSchemaSection[]) => {
|
||||
return this.reduceNodesDeep(
|
||||
sections,
|
||||
(acc, section) => {
|
||||
if (section.id) {
|
||||
acc[`${section.id}`] = section;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
};
|
||||
|
||||
// --------------------------------------------
|
||||
// # TOTAL EQUATION NODE
|
||||
// --------------------------------------------
|
||||
|
||||
private sectionsMapToTotal = (mappedSections: { [key: number]: any }) => {
|
||||
return mapValues(mappedSections, (node) => get(node, 'total.amount') || 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Evauluate equaation string with the given scope table.
|
||||
* @param {string} equation -
|
||||
* @param {{ [key: string]: number }} scope -
|
||||
* @return {number}
|
||||
*/
|
||||
private evaluateEquation = (
|
||||
equation: string,
|
||||
scope: { [key: string | number]: number }
|
||||
): number => {
|
||||
return mathjs.evaluate(equation, scope);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the total section from the eqauation parser.
|
||||
* @param {ICashFlowSchemaTotalSection} sectionSchema
|
||||
* @param {ICashFlowSchemaSection[]} accumlatedSections
|
||||
* @returns {ICashFlowStatementTotalSection}
|
||||
*/
|
||||
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)(
|
||||
mappedSectionsById,
|
||||
sectionSchema.equation
|
||||
)
|
||||
)
|
||||
)({
|
||||
sectionType: ICashFlowStatementSectionType.TOTAL,
|
||||
id: sectionSchema.id,
|
||||
label: this.i18n.__(sectionSchema.label),
|
||||
total: this.getTotalAmountMeta(total),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the beginning cash from date.
|
||||
* @param {Date|string} fromDate -
|
||||
* @return {Date}
|
||||
*/
|
||||
private beginningCashFrom = (fromDate: string | Date): Date => {
|
||||
return moment(fromDate).subtract(1, 'days').toDate();
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve account meta.
|
||||
* @param {ICashFlowSchemaAccountRelation} relation
|
||||
* @param {IAccount} account
|
||||
* @returns {ICashFlowStatementAccountMeta}
|
||||
*/
|
||||
private cashAccountMetaMapper = (
|
||||
relation: ICashFlowSchemaAccountRelation,
|
||||
account: IAccount
|
||||
): ICashFlowStatementAccountMeta => {
|
||||
const cashToDate = this.beginningCashFrom(this.query.fromDate);
|
||||
|
||||
const closingBalance = this.cashLedger
|
||||
.whereToDate(cashToDate)
|
||||
.whereAccountId(account.id)
|
||||
.getClosingBalance();
|
||||
|
||||
const node = {
|
||||
id: account.id,
|
||||
code: account.code,
|
||||
label: account.name,
|
||||
accountType: account.accountType,
|
||||
adjusmentType: relation.direction,
|
||||
total: this.getAmountMeta(closingBalance),
|
||||
sectionType: ICashFlowStatementSectionType.ACCOUNT,
|
||||
};
|
||||
return R.compose(
|
||||
R.when(
|
||||
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
|
||||
this.assocCashAtBeginningAccountDatePeriods
|
||||
)
|
||||
)(node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve accounts sections by the given schema relation.
|
||||
* @param {ICashFlowSchemaAccountRelation} relation
|
||||
* @returns {ICashFlowStatementAccountMeta[]}
|
||||
*/
|
||||
private getCashAccountsBySchemaRelation = (
|
||||
relation: ICashFlowSchemaAccountRelation
|
||||
): ICashFlowStatementAccountMeta[] => {
|
||||
const accounts = this.accountByTypeMap.get(relation.type) || [];
|
||||
const accountMetaMapper = R.curry(this.cashAccountMetaMapper)(relation);
|
||||
return accounts.map(accountMetaMapper);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the accounts meta.
|
||||
* @param {string[]} types
|
||||
* @returns {ICashFlowStatementAccountMeta[]}
|
||||
*/
|
||||
private getCashAccountsBySchemaRelations = (
|
||||
relations: ICashFlowSchemaAccountRelation[]
|
||||
): ICashFlowStatementAccountMeta[] => {
|
||||
return R.concat(...R.map(this.getCashAccountsBySchemaRelation)(relations));
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses the cash at beginning section.
|
||||
* @param {ICashFlowSchemaTotalSection} sectionSchema -
|
||||
* @return {ICashFlowCashBeginningNode}
|
||||
*/
|
||||
private cashAtBeginningSectionParser = (
|
||||
nodeSchema: ICashFlowSchemaSection
|
||||
): ICashFlowCashBeginningNode => {
|
||||
const { accountsRelations } = nodeSchema;
|
||||
const children = this.getCashAccountsBySchemaRelations(accountsRelations);
|
||||
const total = this.getAccountsMetaTotal(children);
|
||||
|
||||
const node = {
|
||||
sectionType: ICashFlowStatementSectionType.CASH_AT_BEGINNING,
|
||||
id: nodeSchema.id,
|
||||
label: this.i18n.__(nodeSchema.label),
|
||||
children,
|
||||
total: this.getTotalAmountMeta(total),
|
||||
};
|
||||
return R.compose(
|
||||
R.when(
|
||||
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
|
||||
this.assocCashAtBeginningDatePeriods
|
||||
)
|
||||
)(node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses the schema section.
|
||||
* @param {ICashFlowSchemaSection} schemaNode
|
||||
* @returns {ICashFlowSchemaSection}
|
||||
*/
|
||||
private schemaSectionParser = (
|
||||
schemaNode: ICashFlowSchemaSection,
|
||||
children
|
||||
): ICashFlowSchemaSection | ICashFlowStatementSection => {
|
||||
return R.compose(
|
||||
// Accounts node.
|
||||
R.when(
|
||||
this.isSchemaSectionType(ICashFlowStatementSectionType.ACCOUNTS),
|
||||
this.accountsSectionParser
|
||||
),
|
||||
// Net income node.
|
||||
R.when(
|
||||
this.isSchemaSectionType(ICashFlowStatementSectionType.NET_INCOME),
|
||||
this.netIncomeSectionMapper
|
||||
),
|
||||
// Cash at beginning node.
|
||||
R.when(
|
||||
this.isSchemaSectionType(
|
||||
ICashFlowStatementSectionType.CASH_AT_BEGINNING
|
||||
),
|
||||
this.cashAtBeginningSectionParser
|
||||
),
|
||||
// Aggregate node. (that has no section type).
|
||||
R.when(
|
||||
this.isSchemaSectionType(ICashFlowStatementSectionType.AGGREGATE),
|
||||
this.regularSectionParser(children)
|
||||
)
|
||||
)(schemaNode);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses the schema section.
|
||||
* @param {ICashFlowSchemaSection | ICashFlowStatementSection} section
|
||||
* @param {number} key
|
||||
* @param {ICashFlowSchemaSection[]} parentValue
|
||||
* @param {(ICashFlowSchemaSection | ICashFlowStatementSection)[]} accumlatedSections
|
||||
* @returns {ICashFlowSchemaSection}
|
||||
*/
|
||||
private schemaSectionTotalParser = (
|
||||
section: ICashFlowSchemaSection | ICashFlowStatementSection,
|
||||
key: number,
|
||||
parentValue: ICashFlowSchemaSection[],
|
||||
context,
|
||||
accumlatedSections: (ICashFlowSchemaSection | ICashFlowStatementSection)[]
|
||||
): ICashFlowSchemaSection | ICashFlowStatementSection => {
|
||||
return R.compose(
|
||||
// Total equation section.
|
||||
R.when(
|
||||
this.isSchemaSectionType(ICashFlowStatementSectionType.TOTAL),
|
||||
R.curry(this.totalEquationSectionParser)(accumlatedSections)
|
||||
)
|
||||
)(section);
|
||||
};
|
||||
|
||||
/**
|
||||
* Schema sections parser.
|
||||
* @param {ICashFlowSchemaSection[]}schema
|
||||
* @returns {ICashFlowStatementSection[]}
|
||||
*/
|
||||
private schemaSectionsParser = (
|
||||
schema: ICashFlowSchemaSection[]
|
||||
): ICashFlowStatementSection[] => {
|
||||
return this.mapNodesDeepReverse(schema, this.schemaSectionParser);
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes the `total` property to the aggregate node.
|
||||
* @param {ICashFlowStatementSection} section
|
||||
* @return {ICashFlowStatementSection}
|
||||
*/
|
||||
private assocRegularSectionTotal = (section: ICashFlowStatementSection) => {
|
||||
const total = this.getAccountsMetaTotal(section.children);
|
||||
return R.assoc('total', this.getTotalAmountMeta(total), section);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses total schema nodes.
|
||||
* @param {(ICashFlowSchemaSection | ICashFlowStatementSection)[]} sections
|
||||
* @returns {(ICashFlowSchemaSection | ICashFlowStatementSection)[]}
|
||||
*/
|
||||
private totalSectionsParser = (
|
||||
sections: (ICashFlowSchemaSection | ICashFlowStatementSection)[]
|
||||
): (ICashFlowSchemaSection | ICashFlowStatementSection)[] => {
|
||||
return this.reduceNodesDeep(
|
||||
sections,
|
||||
(acc, value, key, parentValue, context) => {
|
||||
set(
|
||||
acc,
|
||||
context.path,
|
||||
this.schemaSectionTotalParser(value, key, parentValue, context, acc)
|
||||
);
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
// --------------------------------------------
|
||||
// REPORT FILTERING
|
||||
// --------------------------------------------
|
||||
/**
|
||||
* Detarmines the given section has children and not empty.
|
||||
* @param {ICashFlowStatementSection} section
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private isSectionHasChildren = (
|
||||
section: ICashFlowStatementSection
|
||||
): boolean => {
|
||||
return !isEmpty(section.children);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether the section has no zero amount.
|
||||
* @param {ICashFlowStatementSection} section
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private isSectionNoneZero = (section: ICashFlowStatementSection): boolean => {
|
||||
return section.total.amount !== 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether the parent accounts sections has children.
|
||||
* @param {ICashFlowStatementSection} section
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private isAccountsSectionHasChildren = (
|
||||
section: ICashFlowStatementSection[]
|
||||
): boolean => {
|
||||
return R.ifElse(
|
||||
this.isSchemaSectionType(ICashFlowStatementSectionType.ACCOUNTS),
|
||||
this.isSectionHasChildren,
|
||||
R.always(true)
|
||||
)(section);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines the account section has no zero otherwise returns true.
|
||||
* @param {ICashFlowStatementSection} section
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private isAccountLeafNoneZero = (
|
||||
section: ICashFlowStatementSection[]
|
||||
): boolean => {
|
||||
return R.ifElse(
|
||||
this.isSchemaSectionType(ICashFlowStatementSectionType.ACCOUNT),
|
||||
this.isSectionNoneZero,
|
||||
R.always(true)
|
||||
)(section);
|
||||
};
|
||||
|
||||
/**
|
||||
* Deep filters the non-zero accounts leafs of the report sections.
|
||||
* @param {ICashFlowStatementSection[]} sections
|
||||
* @returns {ICashFlowStatementSection[]}
|
||||
*/
|
||||
private filterNoneZeroAccountsLeafs = (
|
||||
sections: ICashFlowStatementSection[]
|
||||
): ICashFlowStatementSection[] => {
|
||||
return this.filterNodesDeep(sections, this.isAccountLeafNoneZero);
|
||||
};
|
||||
|
||||
/**
|
||||
* Deep filter the non-children sections of the report sections.
|
||||
* @param {ICashFlowStatementSection[]} sections
|
||||
* @returns {ICashFlowStatementSection[]}
|
||||
*/
|
||||
private filterNoneChildrenSections = (
|
||||
sections: ICashFlowStatementSection[]
|
||||
): ICashFlowStatementSection[] => {
|
||||
return this.filterNodesDeep(sections, this.isAccountsSectionHasChildren);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters the report data.
|
||||
* @param {ICashFlowStatementSection[]} sections
|
||||
* @returns {ICashFlowStatementSection[]}
|
||||
*/
|
||||
private filterReportData = (
|
||||
sections: ICashFlowStatementSection[]
|
||||
): ICashFlowStatementSection[] => {
|
||||
return R.compose(
|
||||
this.filterNoneChildrenSections,
|
||||
this.filterNoneZeroAccountsLeafs
|
||||
)(sections);
|
||||
};
|
||||
|
||||
/**
|
||||
* Schema parser.
|
||||
* @param {ICashFlowSchemaSection[]} schema
|
||||
* @returns {ICashFlowSchemaSection[]}
|
||||
*/
|
||||
private schemaParser = (
|
||||
schema: ICashFlowSchemaSection[]
|
||||
): ICashFlowSchemaSection[] => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
R.always(this.query.noneTransactions || this.query.noneZero),
|
||||
this.filterReportData
|
||||
),
|
||||
this.totalSectionsParser,
|
||||
this.schemaSectionsParser
|
||||
)(schema);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow statement data.
|
||||
* @return {ICashFlowStatementData}
|
||||
*/
|
||||
public reportData = (): ICashFlowStatementData => {
|
||||
return this.schemaParser(R.clone(CASH_FLOW_SCHEMA));
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
import * as R from 'ramda';
|
||||
import { sumBy, mapValues, get } from 'lodash';
|
||||
import { ACCOUNT_ROOT_TYPE } from '@/data/AccountTypes';
|
||||
import { accumSum, dateRangeFromToCollection } from 'utils';
|
||||
import {
|
||||
ICashFlowDatePeriod,
|
||||
ICashFlowStatementNetIncomeSection,
|
||||
ICashFlowStatementAccountSection,
|
||||
ICashFlowStatementSection,
|
||||
ICashFlowSchemaTotalSection,
|
||||
IFormatNumberSettings,
|
||||
ICashFlowStatementTotalSection,
|
||||
IDateRange,
|
||||
ICashFlowStatementQuery,
|
||||
} from '@/interfaces';
|
||||
|
||||
export const CashFlowStatementDatePeriods = (Base) =>
|
||||
class extends Base {
|
||||
dateRangeSet: IDateRange[];
|
||||
query: ICashFlowStatementQuery;
|
||||
|
||||
/**
|
||||
* Initialize date range set.
|
||||
*/
|
||||
private initDateRangeCollection() {
|
||||
this.dateRangeSet = dateRangeFromToCollection(
|
||||
this.query.fromDate,
|
||||
this.query.toDate,
|
||||
this.comparatorDateType
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the date period meta.
|
||||
* @param {number} total - Total amount.
|
||||
* @param {Date} fromDate - From date.
|
||||
* @param {Date} toDate - To date.
|
||||
* @return {ICashFlowDatePeriod}
|
||||
*/
|
||||
private getDatePeriodTotalMeta = (
|
||||
total: number,
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
overrideSettings: IFormatNumberSettings = {}
|
||||
): ICashFlowDatePeriod => {
|
||||
return this.getDatePeriodMeta(total, fromDate, toDate, {
|
||||
money: true,
|
||||
...overrideSettings,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the date period meta.
|
||||
* @param {number} total - Total amount.
|
||||
* @param {Date} fromDate - From date.
|
||||
* @param {Date} toDate - To date.
|
||||
* @return {ICashFlowDatePeriod}
|
||||
*/
|
||||
private getDatePeriodMeta = (
|
||||
total: number,
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
overrideSettings?: IFormatNumberSettings
|
||||
): ICashFlowDatePeriod => {
|
||||
return {
|
||||
fromDate: this.getDateMeta(fromDate),
|
||||
toDate: this.getDateMeta(toDate),
|
||||
total: this.getAmountMeta(total, overrideSettings),
|
||||
};
|
||||
};
|
||||
|
||||
// Net income --------------------
|
||||
/**
|
||||
* Retrieve the net income between the given date range.
|
||||
* @param {Date} fromDate
|
||||
* @param {Date} toDate
|
||||
* @returns {number}
|
||||
*/
|
||||
private getNetIncomeDateRange = (fromDate: Date, toDate: Date) => {
|
||||
// Mapping income/expense accounts ids.
|
||||
const incomeAccountsIds = this.getAccountsIdsByType(
|
||||
ACCOUNT_ROOT_TYPE.INCOME
|
||||
);
|
||||
const expenseAccountsIds = this.getAccountsIdsByType(
|
||||
ACCOUNT_ROOT_TYPE.EXPENSE
|
||||
);
|
||||
// Income closing balance.
|
||||
const incomeClosingBalance = accumSum(incomeAccountsIds, (id) =>
|
||||
this.netIncomeLedger
|
||||
.whereFromDate(fromDate)
|
||||
.whereToDate(toDate)
|
||||
.whereAccountId(id)
|
||||
.getClosingBalance()
|
||||
);
|
||||
// Expense closing balance.
|
||||
const expenseClosingBalance = accumSum(expenseAccountsIds, (id) =>
|
||||
this.netIncomeLedger
|
||||
.whereToDate(toDate)
|
||||
.whereFromDate(fromDate)
|
||||
.whereAccountId(id)
|
||||
.getClosingBalance()
|
||||
);
|
||||
// Net income = income - expenses.
|
||||
const netIncome = incomeClosingBalance - expenseClosingBalance;
|
||||
|
||||
return netIncome;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the net income of date period.
|
||||
* @param {IDateRange} dateRange -
|
||||
* @retrun {ICashFlowDatePeriod}
|
||||
*/
|
||||
private getNetIncomeDatePeriod = (dateRange): ICashFlowDatePeriod => {
|
||||
const total = this.getNetIncomeDateRange(
|
||||
dateRange.fromDate,
|
||||
dateRange.toDate
|
||||
);
|
||||
return this.getDatePeriodMeta(
|
||||
total,
|
||||
dateRange.fromDate,
|
||||
dateRange.toDate
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the net income node between the given date ranges.
|
||||
* @param {Date} fromDate
|
||||
* @param {Date} toDate
|
||||
* @returns {ICashFlowDatePeriod[]}
|
||||
*/
|
||||
private getNetIncomeDatePeriods = (
|
||||
section: ICashFlowStatementNetIncomeSection
|
||||
): ICashFlowDatePeriod[] => {
|
||||
return this.dateRangeSet.map(this.getNetIncomeDatePeriod.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes periods property to net income section.
|
||||
* @param {ICashFlowStatementNetIncomeSection} section
|
||||
* @returns {ICashFlowStatementNetIncomeSection}
|
||||
*/
|
||||
protected assocPeriodsToNetIncomeNode = (
|
||||
section: ICashFlowStatementNetIncomeSection
|
||||
): ICashFlowStatementNetIncomeSection => {
|
||||
const incomeDatePeriods = this.getNetIncomeDatePeriods(section);
|
||||
return R.assoc('periods', incomeDatePeriods, section);
|
||||
};
|
||||
|
||||
// Account nodes --------------------
|
||||
/**
|
||||
* Retrieve the account total between date range.
|
||||
* @param {Date} fromDate - From date.
|
||||
* @param {Date} toDate - To date.
|
||||
* @return {number}
|
||||
*/
|
||||
private getAccountTotalDateRange = (
|
||||
node: ICashFlowStatementAccountSection,
|
||||
fromDate: Date,
|
||||
toDate: Date
|
||||
): number => {
|
||||
const closingBalance = this.ledger
|
||||
.whereFromDate(fromDate)
|
||||
.whereToDate(toDate)
|
||||
.whereAccountId(node.id)
|
||||
.getClosingBalance();
|
||||
|
||||
return this.amountAdjustment(node.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}
|
||||
*/
|
||||
protected assocPeriodsToAccountNode = (
|
||||
node: ICashFlowStatementAccountSection
|
||||
): ICashFlowStatementAccountSection => {
|
||||
const datePeriods = this.getAccountDatePeriods(node);
|
||||
return R.assoc('periods', datePeriods, node);
|
||||
}
|
||||
|
||||
// Aggregate node -------------------------
|
||||
/**
|
||||
* Retrieve total of the given period index for node that has children nodes.
|
||||
* @return {number}
|
||||
*/
|
||||
private getChildrenTotalPeriodByIndex = (
|
||||
node: ICashFlowStatementSection,
|
||||
index: number
|
||||
): number => {
|
||||
return sumBy(node.children, `periods[${index}].total.amount`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve date period meta of the given node index.
|
||||
* @param {ICashFlowStatementSection} node -
|
||||
* @param {number} index - Loop index.
|
||||
* @param {Date} fromDate - From date.
|
||||
* @param {Date} toDate - To date.
|
||||
*/
|
||||
private getChildrenTotalPeriodMetaByIndex(
|
||||
node: ICashFlowStatementSection,
|
||||
index: number,
|
||||
fromDate: Date,
|
||||
toDate: Date
|
||||
) {
|
||||
const total = this.getChildrenTotalPeriodByIndex(node, index);
|
||||
return this.getDatePeriodTotalMeta(total, fromDate, toDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the date periods of aggregate node.
|
||||
* @param {ICashFlowStatementSection} node
|
||||
*/
|
||||
private getAggregateNodeDatePeriods(node: ICashFlowStatementSection) {
|
||||
const getChildrenTotalPeriodMetaByIndex = R.curry(
|
||||
this.getChildrenTotalPeriodMetaByIndex.bind(this)
|
||||
)(node);
|
||||
|
||||
return this.dateRangeSet.map((dateRange, index) =>
|
||||
getChildrenTotalPeriodMetaByIndex(
|
||||
index,
|
||||
dateRange.fromDate,
|
||||
dateRange.toDate
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes `periods` property to aggregate section node.
|
||||
* @param {ICashFlowStatementSection} node -
|
||||
* @return {ICashFlowStatementSection}
|
||||
*/
|
||||
protected assocPeriodsToAggregateNode = (
|
||||
node: ICashFlowStatementSection
|
||||
): ICashFlowStatementSection => {
|
||||
const datePeriods = this.getAggregateNodeDatePeriods(node);
|
||||
return R.assoc('periods', datePeriods, node);
|
||||
};
|
||||
|
||||
// Total equation node --------------------
|
||||
|
||||
private sectionsMapToTotalPeriod = (
|
||||
mappedSections: { [key: number]: any },
|
||||
index
|
||||
) => {
|
||||
return mapValues(
|
||||
mappedSections,
|
||||
(node) => get(node, `periods[${index}].total.amount`) || 0
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the date periods of the given total equation.
|
||||
* @param {ICashFlowSchemaTotalSection}
|
||||
* @param {string} equation -
|
||||
* @return {ICashFlowDatePeriod[]}
|
||||
*/
|
||||
private getTotalEquationDatePeriods = (
|
||||
node: ICashFlowSchemaTotalSection,
|
||||
equation: string,
|
||||
nodesTable
|
||||
): ICashFlowDatePeriod[] => {
|
||||
return this.getNodeDatePeriods(node, (node, fromDate, toDate, index) => {
|
||||
const periodScope = this.sectionsMapToTotalPeriod(nodesTable, index);
|
||||
const total = this.evaluateEquation(equation, periodScope);
|
||||
|
||||
return this.getDatePeriodTotalMeta(total, fromDate, toDate);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Associates the total periods of total equation to the ginve total node..
|
||||
* @param {ICashFlowSchemaTotalSection} totalSection -
|
||||
* @return {ICashFlowStatementTotalSection}
|
||||
*/
|
||||
protected assocTotalEquationDatePeriods = (
|
||||
nodesTable: any,
|
||||
equation: string,
|
||||
node: ICashFlowSchemaTotalSection
|
||||
): ICashFlowStatementTotalSection => {
|
||||
const datePeriods = this.getTotalEquationDatePeriods(
|
||||
node,
|
||||
equation,
|
||||
nodesTable
|
||||
);
|
||||
|
||||
return R.assoc('periods', datePeriods, node);
|
||||
};
|
||||
|
||||
// Cash at beginning ----------------------
|
||||
|
||||
/**
|
||||
* Retrieve the date preioods of the given node and 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);
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes `periods` property to cash at beginning date periods.
|
||||
* @param {ICashFlowStatementSection} section -
|
||||
* @return {ICashFlowStatementSection}
|
||||
*/
|
||||
protected assocCashAtBeginningDatePeriods = (
|
||||
node: ICashFlowStatementSection
|
||||
): ICashFlowStatementSection => {
|
||||
const datePeriods = this.getAggregateNodeDatePeriods(node);
|
||||
return R.assoc('periods', datePeriods, node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Associates `periods` propery to cash at beginning account node.
|
||||
* @param {ICashFlowStatementSection} node -
|
||||
* @return {ICashFlowStatementSection}
|
||||
*/
|
||||
protected assocCashAtBeginningAccountDatePeriods = (
|
||||
node: ICashFlowStatementSection
|
||||
): ICashFlowStatementSection => {
|
||||
const datePeriods = this.getBeginningCashAccountPeriods(node);
|
||||
return R.assoc('periods', datePeriods, node);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,173 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import { Knex } from 'knex';
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
ICashFlowStatementQuery,
|
||||
IAccountTransaction,
|
||||
IAccount,
|
||||
} from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export default class CashFlowRepository {
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Retrieve the group type from periods type.
|
||||
* @param {string} displayType
|
||||
* @returns {string}
|
||||
*/
|
||||
protected getGroupTypeFromPeriodsType(displayType: string) {
|
||||
const displayTypes = {
|
||||
year: 'year',
|
||||
day: 'day',
|
||||
month: 'month',
|
||||
quarter: 'month',
|
||||
week: 'day',
|
||||
};
|
||||
return displayTypes[displayType] || 'month';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow accounts.
|
||||
* @returns {Promise<IAccount[]>}
|
||||
*/
|
||||
public async cashFlowAccounts(tenantId: number): Promise<IAccount[]> {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
const accounts = await Account.query();
|
||||
|
||||
return accounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve total of csah at beginning transactions.
|
||||
* @param {number} tenantId -
|
||||
* @param {ICashFlowStatementQuery} filter -
|
||||
* @return {Promise<IAccountTransaction[]>}
|
||||
*/
|
||||
public cashAtBeginningTotalTransactions(
|
||||
tenantId: number,
|
||||
filter: ICashFlowStatementQuery
|
||||
): Promise<IAccountTransaction[]> {
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
const cashBeginningPeriod = moment(filter.fromDate)
|
||||
.subtract(1, 'day')
|
||||
.toDate();
|
||||
|
||||
return AccountTransaction.query().onBuild((query) => {
|
||||
query.modify('creditDebitSummation');
|
||||
|
||||
query.select('accountId');
|
||||
query.groupBy('accountId');
|
||||
|
||||
query.withGraphFetched('account');
|
||||
query.modify('filterDateRange', null, cashBeginningPeriod);
|
||||
|
||||
this.commonFilterBranchesQuery(filter, query);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve accounts transactions.
|
||||
* @param {number} tenantId -
|
||||
* @param {ICashFlowStatementQuery} filter
|
||||
* @return {Promise<IAccountTransaction>}
|
||||
*/
|
||||
public getAccountsTransactions(
|
||||
tenantId: number,
|
||||
filter: ICashFlowStatementQuery
|
||||
): Promise<IAccountTransaction[]> {
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
const groupByDateType = this.getGroupTypeFromPeriodsType(
|
||||
filter.displayColumnsBy
|
||||
);
|
||||
|
||||
return AccountTransaction.query().onBuild((query) => {
|
||||
query.modify('creditDebitSummation');
|
||||
query.modify('groupByDateFormat', groupByDateType);
|
||||
|
||||
query.select('accountId');
|
||||
|
||||
query.groupBy('accountId');
|
||||
query.withGraphFetched('account');
|
||||
|
||||
query.modify('filterDateRange', filter.fromDate, filter.toDate);
|
||||
|
||||
this.commonFilterBranchesQuery(filter, query);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the net income tranasctions.
|
||||
* @param {number} tenantId -
|
||||
* @param {ICashFlowStatementQuery} query -
|
||||
* @return {Promise<IAccountTransaction[]>}
|
||||
*/
|
||||
public getNetIncomeTransactions(
|
||||
tenantId: number,
|
||||
filter: ICashFlowStatementQuery
|
||||
): Promise<IAccountTransaction[]> {
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
const groupByDateType = this.getGroupTypeFromPeriodsType(
|
||||
filter.displayColumnsBy
|
||||
);
|
||||
|
||||
return AccountTransaction.query().onBuild((query) => {
|
||||
query.modify('creditDebitSummation');
|
||||
query.modify('groupByDateFormat', groupByDateType);
|
||||
|
||||
query.select('accountId');
|
||||
query.groupBy('accountId');
|
||||
|
||||
query.withGraphFetched('account');
|
||||
query.modify('filterDateRange', filter.fromDate, filter.toDate);
|
||||
|
||||
this.commonFilterBranchesQuery(filter, query);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve peridos of cash at beginning transactions.
|
||||
* @param {number} tenantId -
|
||||
* @param {ICashFlowStatementQuery} filter -
|
||||
* @return {Promise<IAccountTransaction[]>}
|
||||
*/
|
||||
public cashAtBeginningPeriodTransactions(
|
||||
tenantId: number,
|
||||
filter: ICashFlowStatementQuery
|
||||
): Promise<IAccountTransaction[]> {
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
const groupByDateType = this.getGroupTypeFromPeriodsType(
|
||||
filter.displayColumnsBy
|
||||
);
|
||||
|
||||
return AccountTransaction.query().onBuild((query) => {
|
||||
query.modify('creditDebitSummation');
|
||||
query.modify('groupByDateFormat', groupByDateType);
|
||||
|
||||
query.select('accountId');
|
||||
query.groupBy('accountId');
|
||||
|
||||
query.withGraphFetched('account');
|
||||
query.modify('filterDateRange', filter.fromDate, filter.toDate);
|
||||
|
||||
this.commonFilterBranchesQuery(filter, query);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Common branches filter query.
|
||||
* @param {Knex.QueryBuilder} query
|
||||
*/
|
||||
private commonFilterBranchesQuery = (
|
||||
query: ICashFlowStatementQuery,
|
||||
knexQuery: Knex.QueryBuilder
|
||||
) => {
|
||||
if (!isEmpty(query.branchesIds)) {
|
||||
knexQuery.modify('filterByBranches', query.branchesIds);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import moment from 'moment';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import * as R from 'ramda';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
import {
|
||||
ICashFlowStatementService,
|
||||
ICashFlowStatementQuery,
|
||||
ICashFlowStatementDOO,
|
||||
IAccountTransaction,
|
||||
ICashFlowStatementMeta
|
||||
} from '@/interfaces';
|
||||
import CashFlowStatement from './CashFlow';
|
||||
import Ledger from '@/services/Accounting/Ledger';
|
||||
import CashFlowRepository from './CashFlowRepository';
|
||||
import InventoryService from '@/services/Inventory/Inventory';
|
||||
import { parseBoolean } from 'utils';
|
||||
import { Tenant } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export default class CashFlowStatementService
|
||||
extends FinancialSheet
|
||||
implements ICashFlowStatementService
|
||||
{
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
cashFlowRepo: CashFlowRepository;
|
||||
|
||||
@Inject()
|
||||
inventoryService: InventoryService;
|
||||
|
||||
/**
|
||||
* 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<ICashFlowStatementDOO>}
|
||||
*/
|
||||
public async cashFlow(
|
||||
tenantId: number,
|
||||
query: ICashFlowStatementQuery
|
||||
): Promise<ICashFlowStatementDOO> {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
|
||||
// Retrieve all accounts on the storage.
|
||||
const accounts = await this.cashFlowRepo.cashFlowAccounts(tenantId);
|
||||
|
||||
const tenant = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
// Retrieve the accounts transactions.
|
||||
const transactions = await this.cashFlowRepo.getAccountsTransactions(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
// Retrieve the net income transactions.
|
||||
const netIncome = await this.cashFlowRepo.getNetIncomeTransactions(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
// Retrieve the cash at beginning transactions.
|
||||
const cashAtBeginningTransactions = await this.cashAtBeginningTransactions(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
// Transformes the transactions to ledgers.
|
||||
const ledger = Ledger.fromTransactions(transactions);
|
||||
const cashLedger = Ledger.fromTransactions(cashAtBeginningTransactions);
|
||||
const netIncomeLedger = Ledger.fromTransactions(netIncome);
|
||||
|
||||
// Cash flow statement.
|
||||
const cashFlowInstance = new CashFlowStatement(
|
||||
accounts,
|
||||
ledger,
|
||||
cashLedger,
|
||||
netIncomeLedger,
|
||||
filter,
|
||||
tenant.metadata.baseCurrency,
|
||||
i18n
|
||||
);
|
||||
|
||||
return {
|
||||
data: cashFlowInstance.reportData(),
|
||||
query: filter,
|
||||
meta: this.reportMetadata(tenantId),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the balance sheet meta.
|
||||
* @param {number} tenantId -
|
||||
* @returns {ICashFlowStatementMeta}
|
||||
*/
|
||||
private reportMetadata(tenantId: number): ICashFlowStatementMeta {
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
|
||||
const isCostComputeRunning = this.inventoryService
|
||||
.isItemsCostComputeRunning(tenantId);
|
||||
|
||||
const organizationName = settings.get({
|
||||
group: 'organization',
|
||||
key: 'name',
|
||||
});
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
|
||||
return {
|
||||
isCostComputeRunning: parseBoolean(isCostComputeRunning, false),
|
||||
organizationName,
|
||||
baseCurrency
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
import * as R from 'ramda';
|
||||
import { isEmpty, times } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
ICashFlowStatementSection,
|
||||
ICashFlowStatementSectionType,
|
||||
ICashFlowStatement,
|
||||
ITableRow,
|
||||
ITableColumn,
|
||||
ICashFlowStatementQuery,
|
||||
IDateRange,
|
||||
ICashFlowStatementDOO,
|
||||
} from '@/interfaces';
|
||||
import { dateRangeFromToCollection, tableRowMapper } from 'utils';
|
||||
import { mapValuesDeep } from 'utils/deepdash';
|
||||
|
||||
enum IROW_TYPE {
|
||||
AGGREGATE = 'AGGREGATE',
|
||||
NET_INCOME = 'NET_INCOME',
|
||||
ACCOUNTS = 'ACCOUNTS',
|
||||
ACCOUNT = 'ACCOUNT',
|
||||
TOTAL = 'TOTAL',
|
||||
}
|
||||
const DEEP_CONFIG = { childrenPath: 'children', pathFormat: 'array' };
|
||||
const DISPLAY_COLUMNS_BY = {
|
||||
DATE_PERIODS: 'date_periods',
|
||||
TOTAL: 'total',
|
||||
};
|
||||
|
||||
export default class CashFlowTable implements ICashFlowTable {
|
||||
private report: ICashFlowStatementDOO;
|
||||
private i18n;
|
||||
private dateRangeSet: IDateRange[];
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {ICashFlowStatement} reportStatement
|
||||
*/
|
||||
constructor(reportStatement: ICashFlowStatementDOO, i18n) {
|
||||
this.report = reportStatement;
|
||||
this.i18n = i18n;
|
||||
this.dateRangeSet = [];
|
||||
this.initDateRangeCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize date range set.
|
||||
*/
|
||||
private initDateRangeCollection() {
|
||||
this.dateRangeSet = dateRangeFromToCollection(
|
||||
this.report.query.fromDate,
|
||||
this.report.query.toDate,
|
||||
this.report.query.displayColumnsBy
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the date periods columns accessors.
|
||||
*/
|
||||
private datePeriodsColumnsAccessors = () => {
|
||||
return this.dateRangeSet.map((dateRange: IDateRange, index) => ({
|
||||
key: `date-range-${index}`,
|
||||
accessor: `periods[${index}].total.formattedAmount`,
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the total column accessor.
|
||||
*/
|
||||
private totalColumnAccessor = () => {
|
||||
return [{ key: 'total', accessor: 'total.formattedAmount' }];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the common columns for all report nodes.
|
||||
*/
|
||||
private commonColumns = () => {
|
||||
return R.compose(
|
||||
R.concat([{ key: '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.AGGREGATE),
|
||||
this.regularSectionMapper
|
||||
),
|
||||
R.when(
|
||||
isSectionHasType(ICashFlowStatementSectionType.CASH_AT_BEGINNING),
|
||||
this.regularSectionMapper
|
||||
),
|
||||
R.when(
|
||||
isSectionHasType(ICashFlowStatementSectionType.NET_INCOME),
|
||||
this.netIncomeSectionMapper
|
||||
),
|
||||
R.when(
|
||||
isSectionHasType(ICashFlowStatementSectionType.ACCOUNTS),
|
||||
this.accountsSectionMapper
|
||||
),
|
||||
R.when(
|
||||
isSectionHasType(ICashFlowStatementSectionType.ACCOUNT),
|
||||
this.accountSectionMapper
|
||||
),
|
||||
R.when(
|
||||
isSectionHasType(ICashFlowStatementSectionType.TOTAL),
|
||||
this.totalSectionMapper
|
||||
)
|
||||
)(section);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mappes the sections to the table rows.
|
||||
* @param {ICashFlowStatementSection[]} sections
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
private mapSectionsToTableRows = (
|
||||
sections: ICashFlowStatementSection[]
|
||||
): ITableRow[] => {
|
||||
return mapValuesDeep(sections, this.sectionMapper.bind(this), DEEP_CONFIG);
|
||||
};
|
||||
|
||||
/**
|
||||
* Appends the total to section's children.
|
||||
* @param {ICashFlowStatementSection} section
|
||||
* @returns {ICashFlowStatementSection}
|
||||
*/
|
||||
private appendTotalToSectionChildren = (
|
||||
section: ICashFlowStatementSection
|
||||
): ICashFlowStatementSection => {
|
||||
const label = section.footerLabel
|
||||
? section.footerLabel
|
||||
: this.i18n.__('Total {{accountName}}', { accountName: section.label });
|
||||
|
||||
section.children.push({
|
||||
sectionType: ICashFlowStatementSectionType.TOTAL,
|
||||
label,
|
||||
periods: section.periods,
|
||||
total: section.total,
|
||||
});
|
||||
return section;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ICashFlowStatementSection} section
|
||||
* @returns {ICashFlowStatementSection}
|
||||
*/
|
||||
private mapSectionsToAppendTotalChildren = (
|
||||
section: ICashFlowStatementSection
|
||||
): ICashFlowStatementSection => {
|
||||
const isSectionHasChildren = (section) => !isEmpty(section.children);
|
||||
|
||||
return R.compose(
|
||||
R.when(isSectionHasChildren, this.appendTotalToSectionChildren.bind(this))
|
||||
)(section);
|
||||
};
|
||||
|
||||
/**
|
||||
* Appends total node to children section.
|
||||
* @param {ICashFlowStatementSection[]} sections
|
||||
* @returns {ICashFlowStatementSection[]}
|
||||
*/
|
||||
private appendTotalToChildren = (sections: ICashFlowStatementSection[]) => {
|
||||
return mapValuesDeep(
|
||||
sections,
|
||||
this.mapSectionsToAppendTotalChildren.bind(this),
|
||||
DEEP_CONFIG
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the table rows of cash flow statement.
|
||||
* @param {ICashFlowStatementSection[]} sections
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
public tableRows = (): ITableRow[] => {
|
||||
const sections = this.report.data;
|
||||
|
||||
return R.pipe(
|
||||
this.appendTotalToChildren,
|
||||
this.mapSectionsToTableRows
|
||||
)(sections);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the total columns.
|
||||
* @returns {ITableColumn}
|
||||
*/
|
||||
private totalColumns = (): ITableColumn[] => {
|
||||
return [{ key: 'total', label: this.i18n.__('Total') }];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the formatted column label from the given date range.
|
||||
* @param {ICashFlowDateRange} dateRange -
|
||||
* @return {string}
|
||||
*/
|
||||
private formatColumnLabel = (dateRange: ICashFlowDateRange) => {
|
||||
const monthFormat = (range) => moment(range.toDate).format('YYYY-MM');
|
||||
const yearFormat = (range) => moment(range.toDate).format('YYYY');
|
||||
const dayFormat = (range) => moment(range.toDate).format('YYYY-MM-DD');
|
||||
|
||||
const conditions = [
|
||||
['month', monthFormat],
|
||||
['year', yearFormat],
|
||||
['day', dayFormat],
|
||||
['quarter', monthFormat],
|
||||
['week', dayFormat],
|
||||
];
|
||||
const conditionsPairs = R.map(
|
||||
([type, formatFn]) => [
|
||||
R.always(this.isDisplayColumnsType(type)),
|
||||
formatFn,
|
||||
],
|
||||
conditions
|
||||
);
|
||||
|
||||
return R.compose(R.cond(conditionsPairs))(dateRange);
|
||||
};
|
||||
|
||||
/**
|
||||
* Date periods columns.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
private datePeriodsColumns = (): ITableColumn[] => {
|
||||
return this.dateRangeSet.map((dateRange, index) => ({
|
||||
key: `date-range-${index}`,
|
||||
label: this.formatColumnLabel(dateRange),
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines the given column type is the current.
|
||||
* @reutrns {boolean}
|
||||
*/
|
||||
private isDisplayColumnsBy = (displayColumnsType: string): Boolean => {
|
||||
return this.report.query.displayColumnsType === displayColumnsType;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether the given display columns type is the current.
|
||||
* @param {string} displayColumnsBy
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private isDisplayColumnsType = (displayColumnsBy: string): Boolean => {
|
||||
return this.report.query.displayColumnsBy === displayColumnsBy;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the table columns.
|
||||
* @return {ITableColumn[]}
|
||||
*/
|
||||
public tableColumns = (): ITableColumn[] => {
|
||||
return R.compose(
|
||||
R.concat([{ key: 'name', label: this.i18n.__('Account name') }]),
|
||||
R.when(
|
||||
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
|
||||
R.concat(this.datePeriodsColumns())
|
||||
),
|
||||
R.concat(this.totalColumns())
|
||||
)([]);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
|
||||
export const DISPLAY_COLUMNS_BY = {
|
||||
DATE_PERIODS: 'date_periods',
|
||||
TOTAL: 'total',
|
||||
};
|
||||
|
||||
export const MAP_CONFIG = { childrenPath: 'children', pathFormat: 'array' };
|
||||
@@ -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[];
|
||||
@@ -0,0 +1,151 @@
|
||||
import R from 'ramda';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
ICashflowAccountTransaction,
|
||||
ICashflowAccountTransactionsQuery,
|
||||
INumberFormatQuery,
|
||||
} from '@/interfaces';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
import { runningAmount } from 'utils';
|
||||
|
||||
export default class CashflowAccountTransactionReport extends FinancialSheet {
|
||||
private transactions: any;
|
||||
private openingBalance: number;
|
||||
private runningBalance: any;
|
||||
private numberFormat: INumberFormatQuery;
|
||||
private baseCurrency: string;
|
||||
private query: ICashflowAccountTransactionsQuery;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {IAccountTransaction[]} transactions -
|
||||
* @param {number} openingBalance -
|
||||
* @param {ICashflowAccountTransactionsQuery} query -
|
||||
*/
|
||||
constructor(
|
||||
transactions,
|
||||
openingBalance: number,
|
||||
query: ICashflowAccountTransactionsQuery
|
||||
) {
|
||||
super();
|
||||
|
||||
this.transactions = transactions;
|
||||
this.openingBalance = openingBalance;
|
||||
|
||||
this.runningBalance = runningAmount(this.openingBalance);
|
||||
this.query = query;
|
||||
this.numberFormat = query.numberFormat;
|
||||
this.baseCurrency = 'USD';
|
||||
}
|
||||
|
||||
/**
|
||||
*Transformes the account transaction to to cashflow transaction node.
|
||||
* @param {IAccountTransaction} transaction
|
||||
* @returns {ICashflowAccountTransaction}
|
||||
*/
|
||||
private transactionNode = (transaction: any): ICashflowAccountTransaction => {
|
||||
return {
|
||||
date: transaction.date,
|
||||
formattedDate: moment(transaction.date).format('YYYY-MM-DD'),
|
||||
|
||||
withdrawal: transaction.credit,
|
||||
deposit: transaction.debit,
|
||||
|
||||
formattedDeposit: this.formatNumber(transaction.debit),
|
||||
formattedWithdrawal: this.formatNumber(transaction.credit),
|
||||
|
||||
referenceId: transaction.referenceId,
|
||||
referenceType: transaction.referenceType,
|
||||
|
||||
formattedTransactionType: transaction.referenceTypeFormatted,
|
||||
|
||||
transactionNumber: transaction.transactionNumber,
|
||||
referenceNumber: transaction.referenceNumber,
|
||||
|
||||
runningBalance: this.runningBalance.amount(),
|
||||
formattedRunningBalance: this.formatNumber(this.runningBalance.amount()),
|
||||
|
||||
balance: 0,
|
||||
formattedBalance: '',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Associate cashflow transaction node with running balance attribute.
|
||||
* @param {IAccountTransaction} transaction
|
||||
* @returns {ICashflowAccountTransaction}
|
||||
*/
|
||||
private transactionRunningBalance = (
|
||||
transaction: ICashflowAccountTransaction
|
||||
): ICashflowAccountTransaction => {
|
||||
const amount = transaction.deposit - transaction.withdrawal;
|
||||
|
||||
const biggerThanZero = R.lt(0, amount);
|
||||
const lowerThanZero = R.gt(0, amount);
|
||||
|
||||
const absAmount = Math.abs(amount);
|
||||
|
||||
R.when(R.always(biggerThanZero), this.runningBalance.decrement)(absAmount);
|
||||
R.when(R.always(lowerThanZero), this.runningBalance.increment)(absAmount);
|
||||
|
||||
const runningBalance = this.runningBalance.amount();
|
||||
|
||||
return {
|
||||
...transaction,
|
||||
runningBalance,
|
||||
formattedRunningBalance: this.formatNumber(runningBalance),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Associate to balance attribute to cashflow transaction node.
|
||||
* @param {ICashflowAccountTransaction} transaction
|
||||
* @returns {ICashflowAccountTransaction}
|
||||
*/
|
||||
private transactionBalance = (
|
||||
transaction: ICashflowAccountTransaction
|
||||
): ICashflowAccountTransaction => {
|
||||
const balance =
|
||||
transaction.runningBalance +
|
||||
transaction.withdrawal * -1 +
|
||||
transaction.deposit;
|
||||
|
||||
return {
|
||||
...transaction,
|
||||
balance,
|
||||
formattedBalance: this.formatNumber(balance),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes the given account transaction to cashflow report transaction.
|
||||
* @param {ICashflowAccountTransaction} transaction
|
||||
* @returns {ICashflowAccountTransaction}
|
||||
*/
|
||||
private transactionTransformer = (
|
||||
transaction
|
||||
): ICashflowAccountTransaction => {
|
||||
return R.compose(
|
||||
this.transactionBalance,
|
||||
this.transactionRunningBalance,
|
||||
this.transactionNode
|
||||
)(transaction);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the report transactions node.
|
||||
* @param {} transactions
|
||||
* @returns {ICashflowAccountTransaction[]}
|
||||
*/
|
||||
private transactionsNode = (transactions): ICashflowAccountTransaction[] => {
|
||||
return R.map(this.transactionTransformer)(transactions);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the reprot data node.
|
||||
* @returns {ICashflowAccountTransaction[]}
|
||||
*/
|
||||
public reportData(): ICashflowAccountTransaction[] {
|
||||
return this.transactionsNode(this.transactions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ICashflowAccountTransactionsQuery, IPaginationMeta } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class CashflowAccountTransactionsRepo {
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow account transactions.
|
||||
* @param {number} tenantId -
|
||||
* @param {ICashflowAccountTransactionsQuery} query -
|
||||
*/
|
||||
async getCashflowAccountTransactions(
|
||||
tenantId: number,
|
||||
query: ICashflowAccountTransactionsQuery
|
||||
) {
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
return AccountTransaction.query()
|
||||
.where('account_id', query.accountId)
|
||||
.orderBy([
|
||||
{ column: 'date', order: 'desc' },
|
||||
{ column: 'created_at', order: 'desc' },
|
||||
])
|
||||
.pagination(query.page - 1, query.pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow account opening balance.
|
||||
* @param {number} tenantId
|
||||
* @param {number} accountId
|
||||
* @param {IPaginationMeta} pagination
|
||||
* @return {Promise<number>}
|
||||
*/
|
||||
async getCashflowAccountOpeningBalance(
|
||||
tenantId: number,
|
||||
accountId: number,
|
||||
pagination: IPaginationMeta
|
||||
): Promise<number> {
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieve the opening balance of credit and debit balances.
|
||||
const openingBalancesSubquery = AccountTransaction.query()
|
||||
.where('account_id', accountId)
|
||||
.orderBy([
|
||||
{ column: 'date', order: 'desc' },
|
||||
{ column: 'created_at', order: 'desc' },
|
||||
])
|
||||
.limit(pagination.total)
|
||||
.offset(pagination.pageSize * (pagination.page - 1));
|
||||
|
||||
// Sumation of credit and debit balance.
|
||||
const openingBalances = await AccountTransaction.query()
|
||||
.sum('credit as credit')
|
||||
.sum('debit as debit')
|
||||
.from(openingBalancesSubquery.as('T'))
|
||||
.first();
|
||||
|
||||
const openingBalance = openingBalances.debit - openingBalances.credit;
|
||||
|
||||
return openingBalance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { includes } from 'lodash';
|
||||
import * as qim from 'qim';
|
||||
import { ICashflowAccountTransactionsQuery, IAccount } from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
import CashflowAccountTransactionsRepo from './CashflowAccountTransactionsRepo';
|
||||
import CashflowAccountTransactionsReport from './CashflowAccountTransactions';
|
||||
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { ERRORS } from './constants';
|
||||
import I18nService from '@/services/I18n/I18nService';
|
||||
|
||||
@Service()
|
||||
export default class CashflowAccountTransactionsService extends FinancialSheet {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
cashflowTransactionsRepo: CashflowAccountTransactionsRepo;
|
||||
|
||||
@Inject()
|
||||
i18nService: I18nService;
|
||||
|
||||
/**
|
||||
* Defaults balance sheet filter query.
|
||||
* @return {IBalanceSheetQuery}
|
||||
*/
|
||||
private get defaultQuery(): Partial<ICashflowAccountTransactionsQuery> {
|
||||
return {
|
||||
pageSize: 50,
|
||||
page: 1,
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
divideOn1000: false,
|
||||
showZero: false,
|
||||
formatMoney: 'total',
|
||||
negativeFormat: 'mines',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow accouynt transactions report data.
|
||||
* @param {number} tenantId -
|
||||
* @param {ICashflowAccountTransactionsQuery} query -
|
||||
* @return {Promise<IInvetoryItemDetailDOO>}
|
||||
*/
|
||||
public async cashflowAccountTransactions(
|
||||
tenantId: number,
|
||||
query: ICashflowAccountTransactionsQuery
|
||||
) {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
const parsedQuery = { ...this.defaultQuery, ...query };
|
||||
|
||||
// Retrieve the given account or throw not found service error.
|
||||
const account = await Account.query().findById(parsedQuery.accountId);
|
||||
|
||||
// Validates the cashflow account type.
|
||||
this.validateCashflowAccountType(account);
|
||||
|
||||
// Retrieve the cashflow account transactions.
|
||||
const { results: transactions, pagination } =
|
||||
await this.cashflowTransactionsRepo.getCashflowAccountTransactions(
|
||||
tenantId,
|
||||
parsedQuery
|
||||
);
|
||||
// Retrieve the cashflow account opening balance.
|
||||
const openingBalance =
|
||||
await this.cashflowTransactionsRepo.getCashflowAccountOpeningBalance(
|
||||
tenantId,
|
||||
parsedQuery.accountId,
|
||||
pagination
|
||||
);
|
||||
// Retrieve the computed report.
|
||||
const report = new CashflowAccountTransactionsReport(
|
||||
transactions,
|
||||
openingBalance,
|
||||
parsedQuery
|
||||
);
|
||||
const reportTranasctions = report.reportData();
|
||||
|
||||
return {
|
||||
transactions: this.i18nService.i18nApply(
|
||||
[[qim.$each, 'formattedTransactionType']],
|
||||
reportTranasctions,
|
||||
tenantId
|
||||
),
|
||||
pagination,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the cashflow account type.
|
||||
* @param {IAccount} account -
|
||||
*/
|
||||
private validateCashflowAccountType(account: IAccount) {
|
||||
const cashflowTypes = [
|
||||
ACCOUNT_TYPE.CASH,
|
||||
ACCOUNT_TYPE.CREDIT_CARD,
|
||||
ACCOUNT_TYPE.BANK,
|
||||
];
|
||||
|
||||
if (!includes(cashflowTypes, account.accountType)) {
|
||||
throw new ServiceError(ERRORS.ACCOUNT_ID_HAS_INVALID_TYPE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const ERRORS = {
|
||||
ACCOUNT_ID_HAS_INVALID_TYPE: 'ACCOUNT_ID_HAS_INVALID_TYPE',
|
||||
};
|
||||
@@ -0,0 +1,204 @@
|
||||
import { sumBy, isEmpty } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
import {
|
||||
IContactBalanceSummaryContact,
|
||||
IContactBalanceSummaryTotal,
|
||||
IContactBalanceSummaryAmount,
|
||||
IContactBalanceSummaryPercentage,
|
||||
ILedger,
|
||||
IContactBalanceSummaryQuery,
|
||||
} from '@/interfaces';
|
||||
import { allPassedConditionsPass } from 'utils';
|
||||
|
||||
export class ContactBalanceSummaryReport extends FinancialSheet {
|
||||
readonly baseCurrency: string;
|
||||
readonly ledger: ILedger;
|
||||
readonly filter: IContactBalanceSummaryQuery;
|
||||
|
||||
/**
|
||||
* Calculates the contact percentage of column.
|
||||
* @param {number} customerBalance - Contact balance.
|
||||
* @param {number} totalBalance - Total contacts balance.
|
||||
* @returns {number}
|
||||
*/
|
||||
protected getContactPercentageOfColumn = (
|
||||
customerBalance: number,
|
||||
totalBalance: number
|
||||
): number => {
|
||||
return totalBalance / customerBalance;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the contacts total.
|
||||
* @param {IContactBalanceSummaryContact} contacts
|
||||
* @returns {number}
|
||||
*/
|
||||
protected getContactsTotal = (
|
||||
contacts: IContactBalanceSummaryContact[]
|
||||
): number => {
|
||||
return sumBy(
|
||||
contacts,
|
||||
(contact: IContactBalanceSummaryContact) => contact.total.amount
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Assoc total percentage of column.
|
||||
* @param {IContactBalanceSummaryTotal} node
|
||||
* @returns {IContactBalanceSummaryTotal}
|
||||
*/
|
||||
protected assocTotalPercentageOfColumn = (
|
||||
node: IContactBalanceSummaryTotal
|
||||
): IContactBalanceSummaryTotal => {
|
||||
return R.assoc('percentageOfColumn', this.getPercentageMeta(1), node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the contacts total section.
|
||||
* @param {IContactBalanceSummaryContact[]} contacts
|
||||
* @returns {IContactBalanceSummaryTotal}
|
||||
*/
|
||||
protected getContactsTotalSection = (
|
||||
contacts: IContactBalanceSummaryContact[]
|
||||
): IContactBalanceSummaryTotal => {
|
||||
const customersTotal = this.getContactsTotal(contacts);
|
||||
const node = {
|
||||
total: this.getTotalFormat(customersTotal),
|
||||
};
|
||||
return R.compose(
|
||||
R.when(
|
||||
R.always(this.filter.percentageColumn),
|
||||
this.assocTotalPercentageOfColumn
|
||||
)
|
||||
)(node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the contact summary section with percentage of column.
|
||||
* @param {number} total
|
||||
* @param {IContactBalanceSummaryContact} contact
|
||||
* @returns {IContactBalanceSummaryContact}
|
||||
*/
|
||||
private contactCamparsionPercentageOfColumnMapper = (
|
||||
total: number,
|
||||
contact: IContactBalanceSummaryContact
|
||||
): IContactBalanceSummaryContact => {
|
||||
const amount = this.getContactPercentageOfColumn(
|
||||
total,
|
||||
contact.total.amount
|
||||
);
|
||||
return {
|
||||
...contact,
|
||||
percentageOfColumn: this.getPercentageMeta(amount),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Mappes the contacts summary sections with percentage of column.
|
||||
* @param {IContactBalanceSummaryContact[]} contacts -
|
||||
* @return {IContactBalanceSummaryContact[]}
|
||||
*/
|
||||
protected contactCamparsionPercentageOfColumn = (
|
||||
contacts: IContactBalanceSummaryContact[]
|
||||
): IContactBalanceSummaryContact[] => {
|
||||
const customersTotal = this.getContactsTotal(contacts);
|
||||
const camparsionPercentageOfColummn = R.curry(
|
||||
this.contactCamparsionPercentageOfColumnMapper
|
||||
)(customersTotal);
|
||||
|
||||
return contacts.map(camparsionPercentageOfColummn);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the contact total format.
|
||||
* @param {number} amount -
|
||||
* @return {IContactBalanceSummaryAmount}
|
||||
*/
|
||||
protected getContactTotalFormat = (
|
||||
amount: number
|
||||
): IContactBalanceSummaryAmount => {
|
||||
return {
|
||||
amount,
|
||||
formattedAmount: this.formatNumber(amount, { money: true }),
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the total amount of contacts sections.
|
||||
* @param {number} amount
|
||||
* @returns {IContactBalanceSummaryAmount}
|
||||
*/
|
||||
protected getTotalFormat = (amount: number): IContactBalanceSummaryAmount => {
|
||||
return {
|
||||
amount,
|
||||
formattedAmount: this.formatTotalNumber(amount, { money: true }),
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the percentage amount object.
|
||||
* @param {number} amount
|
||||
* @returns {IContactBalanceSummaryPercentage}
|
||||
*/
|
||||
protected getPercentageMeta = (
|
||||
amount: number
|
||||
): IContactBalanceSummaryPercentage => {
|
||||
return {
|
||||
amount,
|
||||
formattedAmount: this.formatPercentage(amount),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters customer has none transactions.
|
||||
* @param {ICustomerBalanceSummaryCustomer} customer -
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private filterContactNoneTransactions = (
|
||||
contact: IContactBalanceSummaryContact
|
||||
): boolean => {
|
||||
const entries = this.ledger.whereContactId(contact.id).getEntries();
|
||||
|
||||
return !isEmpty(entries);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters the customer that has zero total amount.
|
||||
* @param {ICustomerBalanceSummaryCustomer} customer
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private filterContactNoneZero = (
|
||||
node: IContactBalanceSummaryContact
|
||||
): boolean => {
|
||||
return node.total.amount !== 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters the given customer node;
|
||||
* @param {ICustomerBalanceSummaryCustomer} customer
|
||||
*/
|
||||
private contactNodeFilter = (contact: IContactBalanceSummaryContact) => {
|
||||
const { noneTransactions, noneZero } = this.filter;
|
||||
|
||||
// Conditions pair filter detarminer.
|
||||
const condsPairFilters = [
|
||||
[noneTransactions, this.filterContactNoneTransactions],
|
||||
[noneZero, this.filterContactNoneZero],
|
||||
];
|
||||
return allPassedConditionsPass(condsPairFilters)(contact);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters the given customers nodes.
|
||||
* @param {ICustomerBalanceSummaryCustomer[]} nodes
|
||||
* @returns {ICustomerBalanceSummaryCustomer[]}
|
||||
*/
|
||||
protected contactsFilter = (
|
||||
nodes: IContactBalanceSummaryContact[]
|
||||
): IContactBalanceSummaryContact[] => {
|
||||
return nodes.filter(this.contactNodeFilter);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
ILedger,
|
||||
ICustomer,
|
||||
ICustomerBalanceSummaryCustomer,
|
||||
ICustomerBalanceSummaryQuery,
|
||||
ICustomerBalanceSummaryData,
|
||||
INumberFormatQuery,
|
||||
} from '@/interfaces';
|
||||
import { ContactBalanceSummaryReport } from '../ContactBalanceSummary/ContactBalanceSummary';
|
||||
|
||||
export class CustomerBalanceSummaryReport extends ContactBalanceSummaryReport {
|
||||
readonly ledger: ILedger;
|
||||
readonly baseCurrency: string;
|
||||
readonly customers: ICustomer[];
|
||||
readonly filter: ICustomerBalanceSummaryQuery;
|
||||
readonly numberFormat: INumberFormatQuery;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {IJournalPoster} receivableLedger
|
||||
* @param {ICustomer[]} customers
|
||||
* @param {ICustomerBalanceSummaryQuery} filter
|
||||
* @param {string} baseCurrency
|
||||
*/
|
||||
constructor(
|
||||
ledger: ILedger,
|
||||
customers: ICustomer[],
|
||||
filter: ICustomerBalanceSummaryQuery,
|
||||
baseCurrency: string
|
||||
) {
|
||||
super();
|
||||
|
||||
this.ledger = ledger;
|
||||
this.baseCurrency = baseCurrency;
|
||||
this.customers = customers;
|
||||
this.filter = filter;
|
||||
this.numberFormat = this.filter.numberFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer section mapper.
|
||||
* @param {ICustomer} customer
|
||||
* @returns {ICustomerBalanceSummaryCustomer}
|
||||
*/
|
||||
private customerMapper = (
|
||||
customer: ICustomer
|
||||
): ICustomerBalanceSummaryCustomer => {
|
||||
const closingBalance = this.ledger
|
||||
.whereContactId(customer.id)
|
||||
.getClosingBalance();
|
||||
|
||||
return {
|
||||
id: customer.id,
|
||||
customerName: customer.displayName,
|
||||
total: this.getContactTotalFormat(closingBalance),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Mappes the customer model object to customer balance summary section.
|
||||
* @param {ICustomer[]} customers - Customers.
|
||||
* @returns {ICustomerBalanceSummaryCustomer[]}
|
||||
*/
|
||||
private customersMapper = (
|
||||
customers: ICustomer[]
|
||||
): ICustomerBalanceSummaryCustomer[] => {
|
||||
return customers.map(this.customerMapper);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether the customers post filter is active.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private isCustomersPostFilter = () => {
|
||||
return isEmpty(this.filter.customersIds);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the customers sections of the report.
|
||||
* @param {ICustomer} customers
|
||||
* @returns {ICustomerBalanceSummaryCustomer[]}
|
||||
*/
|
||||
private getCustomersSection = (
|
||||
customers: ICustomer[]
|
||||
): ICustomerBalanceSummaryCustomer[] => {
|
||||
return R.compose(
|
||||
R.when(this.isCustomersPostFilter, this.contactsFilter),
|
||||
R.when(
|
||||
R.always(this.filter.percentageColumn),
|
||||
this.contactCamparsionPercentageOfColumn
|
||||
),
|
||||
this.customersMapper
|
||||
)(customers);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the report statement data.
|
||||
* @returns {ICustomerBalanceSummaryData}
|
||||
*/
|
||||
public reportData = (): ICustomerBalanceSummaryData => {
|
||||
const customersSections = this.getCustomersSection(this.customers);
|
||||
const customersTotal = this.getContactsTotalSection(customersSections);
|
||||
|
||||
return {
|
||||
customers: customersSections,
|
||||
total: customersTotal,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { Inject } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import { isEmpty, map } from 'lodash';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
ICustomerBalanceSummaryService,
|
||||
ICustomerBalanceSummaryQuery,
|
||||
ICustomerBalanceSummaryStatement,
|
||||
ICustomer,
|
||||
ILedgerEntry,
|
||||
} from '@/interfaces';
|
||||
import { CustomerBalanceSummaryReport } from './CustomerBalanceSummary';
|
||||
|
||||
import Ledger from '@/services/Accounting/Ledger';
|
||||
import CustomerBalanceSummaryRepository from './CustomerBalanceSummaryRepository';
|
||||
import { Tenant } from '@/system/models';
|
||||
|
||||
export default class CustomerBalanceSummaryService
|
||||
implements ICustomerBalanceSummaryService
|
||||
{
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
reportRepository: CustomerBalanceSummaryRepository;
|
||||
|
||||
/**
|
||||
* Defaults balance sheet filter query.
|
||||
* @return {ICustomerBalanceSummaryQuery}
|
||||
*/
|
||||
get defaultQuery(): ICustomerBalanceSummaryQuery {
|
||||
return {
|
||||
asDate: moment().format('YYYY-MM-DD'),
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
divideOn1000: false,
|
||||
showZero: false,
|
||||
formatMoney: 'total',
|
||||
negativeFormat: 'mines',
|
||||
},
|
||||
percentageColumn: false,
|
||||
|
||||
noneZero: false,
|
||||
noneTransactions: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the customers ledger entries mapped from accounts transactions.
|
||||
* @param {number} tenantId
|
||||
* @param {Date|string} asDate
|
||||
* @returns {Promise<ILedgerEntry[]>}
|
||||
*/
|
||||
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))(transactions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the statment of customer balance summary report.
|
||||
* @param {number} tenantId
|
||||
* @param {ICustomerBalanceSummaryQuery} query
|
||||
* @return {Promise<ICustomerBalanceSummaryStatement>}
|
||||
*/
|
||||
async customerBalanceSummary(
|
||||
tenantId: number,
|
||||
query: ICustomerBalanceSummaryQuery
|
||||
): Promise<ICustomerBalanceSummaryStatement> {
|
||||
const tenant = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
// Merges the default query and request query.
|
||||
const filter = { ...this.defaultQuery, ...query };
|
||||
|
||||
this.logger.info(
|
||||
'[customer_balance_summary] trying to calculate the report.',
|
||||
{
|
||||
filter,
|
||||
tenantId,
|
||||
}
|
||||
);
|
||||
// Retrieve the customers list ordered by the display name.
|
||||
const customers = await this.reportRepository.getCustomers(
|
||||
tenantId,
|
||||
query.customersIds
|
||||
);
|
||||
// Retrieve the customers debit/credit totals.
|
||||
const customersEntries = await this.getReportCustomersEntries(
|
||||
tenantId,
|
||||
filter.asDate
|
||||
);
|
||||
// Ledger query.
|
||||
const ledger = new Ledger(customersEntries);
|
||||
|
||||
// Report instance.
|
||||
const report = new CustomerBalanceSummaryReport(
|
||||
ledger,
|
||||
customers,
|
||||
filter,
|
||||
tenant.metadata.baseCurrency,
|
||||
);
|
||||
|
||||
return {
|
||||
data: report.reportData(),
|
||||
query: filter,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
ICustomerBalanceSummaryData,
|
||||
ICustomerBalanceSummaryCustomer,
|
||||
ICustomerBalanceSummaryTotal,
|
||||
ITableRow,
|
||||
IColumnMapperMeta,
|
||||
ICustomerBalanceSummaryQuery,
|
||||
ITableColumn,
|
||||
} from '@/interfaces';
|
||||
import { tableMapper, tableRowMapper } from 'utils';
|
||||
|
||||
enum TABLE_ROWS_TYPES {
|
||||
CUSTOMER = 'CUSTOMER',
|
||||
TOTAL = 'TOTAL',
|
||||
}
|
||||
|
||||
export default class CustomerBalanceSummaryTable {
|
||||
report: ICustomerBalanceSummaryData;
|
||||
query: ICustomerBalanceSummaryQuery;
|
||||
i18n: any;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
constructor(
|
||||
report: ICustomerBalanceSummaryData,
|
||||
query: ICustomerBalanceSummaryQuery,
|
||||
i18n
|
||||
) {
|
||||
this.report = report;
|
||||
this.i18n = i18n;
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve percentage columns accessor.
|
||||
* @returns {IColumnMapperMeta[]}
|
||||
*/
|
||||
private getPercentageColumnsAccessor = (): IColumnMapperMeta[] => {
|
||||
return [
|
||||
{
|
||||
key: 'percentageOfColumn',
|
||||
accessor: 'percentageOfColumn.formattedAmount',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve customer node columns accessor.
|
||||
* @returns {IColumnMapperMeta[]}
|
||||
*/
|
||||
private getCustomerColumnsAccessor = (): IColumnMapperMeta[] => {
|
||||
const columns = [
|
||||
{ key: 'customerName', accessor: 'customerName' },
|
||||
{ key: 'total', accessor: 'total.formattedAmount' },
|
||||
];
|
||||
return R.compose(
|
||||
R.concat(columns),
|
||||
R.when(
|
||||
R.always(this.query.percentageColumn),
|
||||
R.concat(this.getPercentageColumnsAccessor())
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes the customers to table rows.
|
||||
* @param {ICustomerBalanceSummaryCustomer[]} customers
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
private customersTransformer(
|
||||
customers: ICustomerBalanceSummaryCustomer[]
|
||||
): ITableRow[] {
|
||||
const columns = this.getCustomerColumnsAccessor();
|
||||
|
||||
return tableMapper(customers, columns, {
|
||||
rowTypes: [TABLE_ROWS_TYPES.CUSTOMER],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve total node columns accessor.
|
||||
* @returns {IColumnMapperMeta[]}
|
||||
*/
|
||||
private getTotalColumnsAccessor = (): IColumnMapperMeta[] => {
|
||||
const columns = [
|
||||
{ key: 'total', value: this.i18n.__('Total') },
|
||||
{ key: 'total', accessor: 'total.formattedAmount' },
|
||||
];
|
||||
return R.compose(
|
||||
R.concat(columns),
|
||||
R.when(
|
||||
R.always(this.query.percentageColumn),
|
||||
R.concat(this.getPercentageColumnsAccessor())
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes the total to table row.
|
||||
* @param {ICustomerBalanceSummaryTotal} total
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
private totalTransformer = (
|
||||
total: ICustomerBalanceSummaryTotal
|
||||
): ITableRow => {
|
||||
const columns = this.getTotalColumnsAccessor();
|
||||
|
||||
return tableRowMapper(total, columns, {
|
||||
rowTypes: [TABLE_ROWS_TYPES.TOTAL],
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes the customer balance summary to table rows.
|
||||
* @param {ICustomerBalanceSummaryData} customerBalanceSummary
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
public tableRows(): ITableRow[] {
|
||||
const customers = this.customersTransformer(this.report.customers);
|
||||
const total = this.totalTransformer(this.report.total);
|
||||
|
||||
return customers.length > 0 ? [...customers, total] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the report statement columns
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
public tableColumns = (): ITableColumn[] => {
|
||||
const columns = [
|
||||
{
|
||||
key: 'name',
|
||||
label: this.i18n.__('contact_summary_balance.account_name'),
|
||||
},
|
||||
{ key: 'total', label: this.i18n.__('contact_summary_balance.total') },
|
||||
];
|
||||
return R.compose(
|
||||
R.when(
|
||||
R.always(this.query.percentageColumn),
|
||||
R.append({
|
||||
key: 'percentage_of_column',
|
||||
label: this.i18n.__('contact_summary_balance.percentage_column'),
|
||||
})
|
||||
),
|
||||
R.concat(columns)
|
||||
)([]);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import * as R from 'ramda';
|
||||
import { memoize } from 'lodash';
|
||||
import { compose } from 'lodash/fp';
|
||||
import {
|
||||
IAccountTransactionsGroupBy,
|
||||
IFinancialDatePeriodsUnit,
|
||||
IFinancialSheetTotalPeriod,
|
||||
IFormatNumberSettings,
|
||||
} from '@/interfaces';
|
||||
import { dateRangeFromToCollection } from 'utils';
|
||||
import { FinancialDateRanges } from './FinancialDateRanges';
|
||||
|
||||
export const FinancialDatePeriods = (Base) =>
|
||||
class extends compose(FinancialDateRanges)(Base) {
|
||||
/**
|
||||
*
|
||||
* @param {Date} fromDate -
|
||||
* @param {Date} toDate
|
||||
* @param {string} unit
|
||||
*/
|
||||
protected getDateRanges = memoize(
|
||||
(fromDate: Date, toDate: Date, unit: string) => {
|
||||
return dateRangeFromToCollection(fromDate, toDate, unit);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieve the date period meta.
|
||||
* @param {number} total - Total amount.
|
||||
* @param {Date} fromDate - From date.
|
||||
* @param {Date} toDate - To date.
|
||||
* @return {ICashFlowDatePeriod}
|
||||
*/
|
||||
protected getDatePeriodMeta = (
|
||||
total: number,
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
overrideSettings?: IFormatNumberSettings
|
||||
): IFinancialSheetTotalPeriod => {
|
||||
return {
|
||||
fromDate: this.getDateMeta(fromDate),
|
||||
toDate: this.getDateMeta(toDate),
|
||||
total: this.getAmountMeta(total, overrideSettings),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the date period meta.
|
||||
* @param {number} total - Total amount.
|
||||
* @param {Date} fromDate - From date.
|
||||
* @param {Date} toDate - To date.
|
||||
* @return {ICashFlowDatePeriod}
|
||||
*/
|
||||
protected getDatePeriodTotalMeta = (
|
||||
total: number,
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
overrideSettings: IFormatNumberSettings = {}
|
||||
) => {
|
||||
return this.getDatePeriodMeta(total, fromDate, toDate, {
|
||||
money: true,
|
||||
...overrideSettings,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the date preioods of the given node and accumlated function.
|
||||
* @param {IBalanceSheetAccountNode} node
|
||||
* @param {(fromDate: Date, toDate: Date, index: number) => any}
|
||||
* @return {}
|
||||
*/
|
||||
protected getNodeDatePeriods = R.curry(
|
||||
(
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
periodsUnit: string,
|
||||
node: any,
|
||||
callback: (
|
||||
node: any,
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
index: number
|
||||
) => any
|
||||
) => {
|
||||
const curriedCallback = R.curry(callback)(node);
|
||||
|
||||
// Retrieves memorized date ranges.
|
||||
const dateRanges = this.getDateRanges(fromDate, toDate, periodsUnit);
|
||||
|
||||
return dateRanges.map((dateRange, index) => {
|
||||
return curriedCallback(dateRange.fromDate, dateRange.toDate, index);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieve the accounts transactions group type from display columns by.
|
||||
* @param {IAccountTransactionsGroupBy} columnsBy
|
||||
* @returns {IAccountTransactionsGroupBy}
|
||||
*/
|
||||
protected getGroupByFromDisplayColumnsBy = (
|
||||
columnsBy: IFinancialDatePeriodsUnit
|
||||
): IAccountTransactionsGroupBy => {
|
||||
const paris = {
|
||||
week: IAccountTransactionsGroupBy.Day,
|
||||
quarter: IAccountTransactionsGroupBy.Month,
|
||||
year: IAccountTransactionsGroupBy.Year,
|
||||
month: IAccountTransactionsGroupBy.Month,
|
||||
day: IAccountTransactionsGroupBy.Day,
|
||||
};
|
||||
return paris[columnsBy];
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
import { IDateRange, IFinancialDatePeriodsUnit } from '@/interfaces';
|
||||
import moment from 'moment';
|
||||
|
||||
export const FinancialDateRanges = (Base) =>
|
||||
class extends Base {
|
||||
/**
|
||||
* Retrieve previous period (PP) date of the given date.
|
||||
* @param {Date} fromDate -
|
||||
* @param {Date} toDate -
|
||||
* @param {IFinancialDatePeriodsUnit} unit -
|
||||
* @returns {Date}
|
||||
*/
|
||||
protected getPreviousPeriodDate = (
|
||||
date: Date,
|
||||
value: number = 1,
|
||||
unit: IFinancialDatePeriodsUnit = IFinancialDatePeriodsUnit.Day
|
||||
): Date => {
|
||||
return moment(date).subtract(value, unit).toDate();
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the different
|
||||
* @param {Date} fromDate
|
||||
* @param {Date} toDate
|
||||
* @returns
|
||||
*/
|
||||
protected getPreviousPeriodDiff = (fromDate: Date, toDate: Date) => {
|
||||
return moment(toDate).diff(fromDate, 'days') + 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the periods period dates.
|
||||
* @param {Date} fromDate -
|
||||
* @param {Date} toDate -
|
||||
*/
|
||||
protected getPreviousPeriodDateRange = (
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
unit: IFinancialDatePeriodsUnit,
|
||||
amount: number = 1
|
||||
): IDateRange => {
|
||||
const PPToDate = this.getPreviousPeriodDate(toDate, amount, unit);
|
||||
const PPFromDate = this.getPreviousPeriodDate(fromDate, amount, unit);
|
||||
|
||||
return { toDate: PPToDate, fromDate: PPFromDate };
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the previous period (PP) date range of total column.
|
||||
* @param {Date} fromDate
|
||||
* @param {Date} toDate
|
||||
* @returns {IDateRange}
|
||||
*/
|
||||
protected getPPTotalDateRange = (
|
||||
fromDate: Date,
|
||||
toDate: Date
|
||||
): IDateRange => {
|
||||
const unit = this.getPreviousPeriodDiff(fromDate, toDate);
|
||||
|
||||
return this.getPreviousPeriodDateRange(
|
||||
fromDate,
|
||||
toDate,
|
||||
IFinancialDatePeriodsUnit.Day,
|
||||
unit
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the previous period (PP) date range of date periods columns.
|
||||
* @param {Date} fromDate -
|
||||
* @param {Date} toDate -
|
||||
* @param {IFinancialDatePeriodsUnit}
|
||||
* @returns {IDateRange}
|
||||
*/
|
||||
protected getPPDatePeriodDateRange = (
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
unit: IFinancialDatePeriodsUnit
|
||||
): IDateRange => {
|
||||
return this.getPreviousPeriodDateRange(fromDate, toDate, unit, 1);
|
||||
};
|
||||
|
||||
// ------------------------
|
||||
// Previous Year (PY).
|
||||
// ------------------------
|
||||
/**
|
||||
* Retrieve the previous year of the given date.
|
||||
* @params {Date} date
|
||||
* @returns {Date}
|
||||
*/
|
||||
getPreviousYearDate = (date: Date) => {
|
||||
return moment(date).subtract(1, 'years').toDate();
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves previous year date range.
|
||||
* @param {Date} fromDate
|
||||
* @param {Date} toDate
|
||||
* @returns {IDateRange}
|
||||
*/
|
||||
protected getPreviousYearDateRange = (
|
||||
fromDate: Date,
|
||||
toDate: Date
|
||||
): IDateRange => {
|
||||
const PYFromDate = this.getPreviousYearDate(fromDate);
|
||||
const PYToDate = this.getPreviousYearDate(toDate);
|
||||
|
||||
return { fromDate: PYFromDate, toDate: PYToDate };
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import * as mathjs from 'mathjs';
|
||||
import * as R from 'ramda';
|
||||
import { compose } from 'lodash/fp';
|
||||
import { omit, get, mapValues } from 'lodash';
|
||||
import { FinancialSheetStructure } from './FinancialSheetStructure';
|
||||
|
||||
export const FinancialEvaluateEquation = (Base) =>
|
||||
class extends compose(FinancialSheetStructure)(Base) {
|
||||
/**
|
||||
* Evauluate equaation string with the given scope table.
|
||||
* @param {string} equation -
|
||||
* @param {{ [key: string]: number }} scope -
|
||||
* @return {number}
|
||||
*/
|
||||
protected evaluateEquation = (
|
||||
equation: string,
|
||||
scope: { [key: string | number]: number }
|
||||
): number => {
|
||||
return mathjs.evaluate(equation, scope);
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes the given nodes nested array to object key/value by id.
|
||||
* @param nodes
|
||||
* @returns
|
||||
*/
|
||||
private transformNodesToMap = (nodes: any[]) => {
|
||||
return this.mapAccNodesDeep(
|
||||
nodes,
|
||||
(node, key, parentValue, acc, context) => {
|
||||
if (node.id) {
|
||||
acc[`${node.id}`] = omit(node, ['children']);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param nodesById
|
||||
* @returns
|
||||
*/
|
||||
private mapNodesToTotal = R.curry(
|
||||
(path: string, nodesById: { [key: number]: any }) => {
|
||||
return mapValues(nodesById, (node) => get(node, path, 0));
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
protected getNodesTableForEvaluating = R.curry(
|
||||
(path = 'total.amount', nodes) => {
|
||||
return R.compose(
|
||||
this.mapNodesToTotal(path),
|
||||
this.transformNodesToMap
|
||||
)(nodes);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
export const FinancialFilter = (Base) =>
|
||||
class extends Base {
|
||||
/**
|
||||
* Detarmines whether the given node has children.
|
||||
* @param {IBalanceSheetCommonNode} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
protected isNodeHasChildren = (node: IBalanceSheetCommonNode): boolean =>
|
||||
!isEmpty(node.children);
|
||||
|
||||
/**
|
||||
* Detarmines whether the given node has no zero amount.
|
||||
* @param {IBalanceSheetCommonNode} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isNodeNoneZero = (node) =>{
|
||||
return node.total.amount !== 0;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import * as R from 'ramda';
|
||||
import { get, isEmpty } from 'lodash';
|
||||
|
||||
export const FinancialHorizTotals = (Base) =>
|
||||
class extends Base {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
protected assocNodePercentage = R.curry(
|
||||
(assocPath, parentTotal: number, node: any) => {
|
||||
const percentage = this.getPercentageBasis(
|
||||
parentTotal,
|
||||
node.total.amount
|
||||
);
|
||||
return R.assoc(
|
||||
assocPath,
|
||||
this.getPercentageAmountMeta(percentage),
|
||||
node
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {} parentNode -
|
||||
* @param {} horTotalNode -
|
||||
* @param {number} index -
|
||||
*/
|
||||
protected assocPercentageHorizTotal = R.curry(
|
||||
(assocPercentagePath: string, parentNode, horTotalNode, index) => {
|
||||
const parentTotal = get(
|
||||
parentNode,
|
||||
`horizontalTotals[${index}].total.amount`,
|
||||
0
|
||||
);
|
||||
return this.assocNodePercentage(
|
||||
assocPercentagePath,
|
||||
parentTotal,
|
||||
horTotalNode
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param assocPercentagePath
|
||||
* @param parentNode
|
||||
* @param node
|
||||
* @returns
|
||||
*/
|
||||
protected assocPercentageHorizTotals = R.curry(
|
||||
(assocPercentagePath: string, parentNode, node) => {
|
||||
const assocColPerc = this.assocPercentageHorizTotal(
|
||||
assocPercentagePath,
|
||||
parentNode
|
||||
);
|
||||
return R.addIndex(R.map)(assocColPerc)(node.horizontalTotals);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
assocRowPercentageHorizTotal = R.curry(
|
||||
(assocPercentagePath: string, node, horizTotalNode) => {
|
||||
return this.assocNodePercentage(
|
||||
assocPercentagePath,
|
||||
node.total.amount,
|
||||
horizTotalNode
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
protected assocHorizontalPercentageTotals = R.curry(
|
||||
(assocPercentagePath: string, node) => {
|
||||
const assocColPerc = this.assocRowPercentageHorizTotal(
|
||||
assocPercentagePath,
|
||||
node
|
||||
);
|
||||
|
||||
return R.map(assocColPerc)(node.horizontalTotals);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param node
|
||||
* @returns
|
||||
*/
|
||||
protected isNodeHasHorizTotals = (node) => {
|
||||
return !isEmpty(node.horizontalTotals);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
import { sumBy } from 'lodash';
|
||||
import {
|
||||
IFinancialDatePeriodsUnit,
|
||||
IFinancialNodeWithPreviousPeriod,
|
||||
} from '@/interfaces';
|
||||
import * as R from 'ramda';
|
||||
|
||||
export const FinancialPreviousPeriod = (Base) =>
|
||||
class extends Base {
|
||||
// ---------------------------
|
||||
// # Common Node.
|
||||
// ---------------------------
|
||||
/**
|
||||
* Assoc previous period percentage attribute to account node.
|
||||
* @param {IProfitLossSheetAccountNode} accountNode
|
||||
* @returns {IFinancialNodeWithPreviousPeriod}
|
||||
*/
|
||||
protected assocPreviousPeriodPercentageNode = (
|
||||
accountNode: IProfitLossSheetAccountNode
|
||||
): IFinancialNodeWithPreviousPeriod => {
|
||||
const percentage = this.getPercentageBasis(
|
||||
accountNode.previousPeriod.amount,
|
||||
accountNode.previousPeriodChange.amount
|
||||
);
|
||||
return R.assoc(
|
||||
'previousPeriodPercentage',
|
||||
this.getPercentageAmountMeta(percentage),
|
||||
accountNode
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Assoc previous period total attribute to account node.
|
||||
* @param {IProfitLossSheetAccountNode} accountNode
|
||||
* @returns {IFinancialNodeWithPreviousPeriod}
|
||||
*/
|
||||
protected assocPreviousPeriodChangeNode = (
|
||||
accountNode: IProfitLossSheetAccountNode
|
||||
): IFinancialNodeWithPreviousPeriod => {
|
||||
const change = this.getAmountChange(
|
||||
accountNode.total.amount,
|
||||
accountNode.previousPeriod.amount
|
||||
);
|
||||
return R.assoc(
|
||||
'previousPeriodChange',
|
||||
this.getAmountMeta(change),
|
||||
accountNode
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Assoc previous period percentage attribute to account node.
|
||||
*
|
||||
* % change = Change ÷ Original Number × 100.
|
||||
*
|
||||
* @param {IProfitLossSheetAccountNode} accountNode
|
||||
* @returns {IFinancialNodeWithPreviousPeriod}
|
||||
*/
|
||||
protected assocPreviousPeriodTotalPercentageNode = (
|
||||
accountNode: IProfitLossSheetAccountNode
|
||||
): IFinancialNodeWithPreviousPeriod => {
|
||||
const percentage = this.getPercentageBasis(
|
||||
accountNode.previousPeriod.amount,
|
||||
accountNode.previousPeriodChange.amount
|
||||
);
|
||||
return R.assoc(
|
||||
'previousPeriodPercentage',
|
||||
this.getPercentageTotalAmountMeta(percentage),
|
||||
accountNode
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Assoc previous period total attribute to account node.
|
||||
* @param {IProfitLossSheetAccountNode} accountNode
|
||||
* @returns {IFinancialNodeWithPreviousPeriod}
|
||||
*/
|
||||
protected assocPreviousPeriodTotalChangeNode = (
|
||||
accountNode: any
|
||||
): IFinancialNodeWithPreviousPeriod => {
|
||||
const change = this.getAmountChange(
|
||||
accountNode.total.amount,
|
||||
accountNode.previousPeriod.amount
|
||||
);
|
||||
return R.assoc(
|
||||
'previousPeriodChange',
|
||||
this.getTotalAmountMeta(change),
|
||||
accountNode
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Assoc previous year from/to date to horizontal nodes.
|
||||
* @param horizNode
|
||||
* @returns {IFinancialNodeWithPreviousPeriod}
|
||||
*/
|
||||
protected assocPreviousPeriodHorizNodeFromToDates = R.curry(
|
||||
(
|
||||
periodUnit: IFinancialDatePeriodsUnit,
|
||||
horizNode: any
|
||||
): IFinancialNodeWithPreviousPeriod => {
|
||||
const { fromDate: PPFromDate, toDate: PPToDate } =
|
||||
this.getPreviousPeriodDateRange(
|
||||
horizNode.fromDate.date,
|
||||
horizNode.toDate.date,
|
||||
periodUnit
|
||||
);
|
||||
return R.compose(
|
||||
R.assoc('previousPeriodToDate', this.getDateMeta(PPToDate)),
|
||||
R.assoc('previousPeriodFromDate', this.getDateMeta(PPFromDate))
|
||||
)(horizNode);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieves PP total sumation of the given horiz index node.
|
||||
* @param {number} index
|
||||
* @param node
|
||||
* @returns {number}
|
||||
*/
|
||||
protected getPPHorizNodesTotalSumation = (index: number, node): number => {
|
||||
return sumBy(
|
||||
node.children,
|
||||
`horizontalTotals[${index}].previousPeriod.amount`
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
import * as R from 'ramda';
|
||||
import { sumBy } from 'lodash'
|
||||
import {
|
||||
IFinancialCommonHorizDatePeriodNode,
|
||||
IFinancialCommonNode,
|
||||
IFinancialNodeWithPreviousYear,
|
||||
} from '@/interfaces';
|
||||
|
||||
export const FinancialPreviousYear = (Base) =>
|
||||
class extends Base {
|
||||
// ---------------------------
|
||||
// # Common Node
|
||||
// ---------------------------
|
||||
/**
|
||||
* Assoc previous year change attribute to account node.
|
||||
* @param {IProfitLossSheetAccountNode} accountNode
|
||||
* @returns {IProfitLossSheetAccountNode}
|
||||
*/
|
||||
protected assocPreviousYearChangetNode = (
|
||||
node: IFinancialCommonNode & IFinancialNodeWithPreviousYear
|
||||
): IFinancialNodeWithPreviousYear => {
|
||||
const change = this.getAmountChange(
|
||||
node.total.amount,
|
||||
node.previousYear.amount
|
||||
);
|
||||
return R.assoc('previousYearChange', this.getAmountMeta(change), node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Assoc previous year percentage attribute to account node.
|
||||
*
|
||||
* % increase = Increase ÷ Original Number × 100.
|
||||
*
|
||||
* @param {IProfitLossSheetAccountNode} accountNode
|
||||
* @returns {IProfitLossSheetAccountNode}
|
||||
*/
|
||||
protected assocPreviousYearPercentageNode = (
|
||||
node: IFinancialCommonNode & IFinancialNodeWithPreviousYear
|
||||
): IFinancialNodeWithPreviousYear => {
|
||||
const percentage = this.getPercentageBasis(
|
||||
node.previousYear.amount,
|
||||
node.previousYearChange.amount
|
||||
);
|
||||
return R.assoc(
|
||||
'previousYearPercentage',
|
||||
this.getPercentageAmountMeta(percentage),
|
||||
node
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Assoc previous year change attribute to account node.
|
||||
* @param {IProfitLossSheetAccountNode} accountNode
|
||||
* @returns {IProfitLossSheetAccountNode}
|
||||
*/
|
||||
protected assocPreviousYearTotalChangeNode = (
|
||||
node: IFinancialCommonNode & IFinancialNodeWithPreviousYear
|
||||
): IFinancialNodeWithPreviousYear => {
|
||||
const change = this.getAmountChange(
|
||||
node.total.amount,
|
||||
node.previousYear.amount
|
||||
);
|
||||
return R.assoc(
|
||||
'previousYearChange',
|
||||
this.getTotalAmountMeta(change),
|
||||
node
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Assoc previous year percentage attribute to account node.
|
||||
* @param {IProfitLossSheetAccountNode} accountNode
|
||||
* @returns {IProfitLossSheetAccountNode}
|
||||
*/
|
||||
protected assocPreviousYearTotalPercentageNode = (
|
||||
node: IFinancialCommonNode & IFinancialNodeWithPreviousYear
|
||||
): IFinancialNodeWithPreviousYear => {
|
||||
const percentage = this.getPercentageBasis(
|
||||
node.previousYear.amount,
|
||||
node.previousYearChange.amount
|
||||
);
|
||||
return R.assoc(
|
||||
'previousYearPercentage',
|
||||
this.getPercentageTotalAmountMeta(percentage),
|
||||
node
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Assoc previous year from/to date to horizontal nodes.
|
||||
* @param horizNode
|
||||
* @returns
|
||||
*/
|
||||
protected assocPreviousYearHorizNodeFromToDates = (
|
||||
horizNode: IFinancialCommonHorizDatePeriodNode
|
||||
) => {
|
||||
const PYFromDate = this.getPreviousYearDate(horizNode.fromDate.date);
|
||||
const PYToDate = this.getPreviousYearDate(horizNode.toDate.date);
|
||||
|
||||
return R.compose(
|
||||
R.assoc('previousYearToDate', this.getDateMeta(PYToDate)),
|
||||
R.assoc('previousYearFromDate', this.getDateMeta(PYFromDate))
|
||||
)(horizNode);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves PP total sumation of the given horiz index node.
|
||||
* @param {number} index
|
||||
* @param {} node
|
||||
* @returns {number}
|
||||
*/
|
||||
protected getPYHorizNodesTotalSumation = (index: number, node): number => {
|
||||
return sumBy(
|
||||
node.children,
|
||||
`horizontalTotals[${index}].previousYear.amount`
|
||||
)
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export default class FinancialReportService {
|
||||
transformOrganizationMeta(tenant) {
|
||||
return {
|
||||
organizationName: tenant.metadata?.name,
|
||||
baseCurrency: tenant.metadata?.baseCurrency,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as R from 'ramda';
|
||||
import { FinancialSheetStructure } from './FinancialSheetStructure';
|
||||
|
||||
export const FinancialSchema = (Base) =>
|
||||
class extends R.compose(FinancialSheetStructure)(Base) {
|
||||
/**
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
getSchema() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string|number} id
|
||||
* @returns
|
||||
*/
|
||||
getSchemaNodeById = (id: string | number) => {
|
||||
const schema = this.getSchema();
|
||||
|
||||
return this.findNodeDeep(schema, (node) => node.id === id);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,177 @@
|
||||
import moment from 'moment';
|
||||
import {
|
||||
ICashFlowStatementTotal,
|
||||
IFormatNumberSettings,
|
||||
INumberFormatQuery,
|
||||
} from '@/interfaces';
|
||||
import { formatNumber } from 'utils';
|
||||
|
||||
export default class FinancialSheet {
|
||||
readonly numberFormat: INumberFormatQuery = {
|
||||
precision: 2,
|
||||
divideOn1000: false,
|
||||
showZero: false,
|
||||
formatMoney: 'total',
|
||||
negativeFormat: 'mines',
|
||||
};
|
||||
readonly baseCurrency: string;
|
||||
|
||||
/**
|
||||
* Transformes the number format query to settings
|
||||
*/
|
||||
protected transfromFormatQueryToSettings(): IFormatNumberSettings {
|
||||
const { numberFormat } = this;
|
||||
|
||||
return {
|
||||
precision: numberFormat.precision,
|
||||
divideOn1000: numberFormat.divideOn1000,
|
||||
excerptZero: !numberFormat.showZero,
|
||||
negativeFormat: numberFormat.negativeFormat,
|
||||
money: numberFormat.formatMoney === 'always',
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Formating amount based on the given report query.
|
||||
* @param {number} number -
|
||||
* @param {IFormatNumberSettings} overrideSettings -
|
||||
* @return {string}
|
||||
*/
|
||||
protected formatNumber(
|
||||
number,
|
||||
overrideSettings: IFormatNumberSettings = {}
|
||||
): string {
|
||||
const settings = {
|
||||
...this.transfromFormatQueryToSettings(),
|
||||
...overrideSettings,
|
||||
};
|
||||
return formatNumber(number, settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatting full amount with different format settings.
|
||||
* @param {number} amount -
|
||||
* @param {IFormatNumberSettings} settings -
|
||||
*/
|
||||
protected formatTotalNumber = (
|
||||
amount: number,
|
||||
settings: IFormatNumberSettings = {}
|
||||
): string => {
|
||||
const { numberFormat } = this;
|
||||
|
||||
return this.formatNumber(amount, {
|
||||
money: numberFormat.formatMoney === 'none' ? false : true,
|
||||
excerptZero: false,
|
||||
...settings,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Formates the amount to the percentage string.
|
||||
* @param {number} amount
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formatPercentage = (
|
||||
amount: number,
|
||||
overrideSettings: IFormatNumberSettings = {}
|
||||
): string => {
|
||||
const percentage = amount * 100;
|
||||
const settings = {
|
||||
excerptZero: true,
|
||||
...overrideSettings,
|
||||
symbol: '%',
|
||||
money: false,
|
||||
};
|
||||
return formatNumber(percentage, settings);
|
||||
};
|
||||
|
||||
/**
|
||||
* Format the given total percentage.
|
||||
* @param {number} amount -
|
||||
* @param {IFormatNumberSettings} settings -
|
||||
*/
|
||||
protected formatTotalPercentage = (
|
||||
amount: number,
|
||||
settings: IFormatNumberSettings = {}
|
||||
): string => {
|
||||
return this.formatPercentage(amount, {
|
||||
...settings,
|
||||
excerptZero: false,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the amount meta object.
|
||||
* @param {number} amount
|
||||
* @returns {ICashFlowStatementTotal}
|
||||
*/
|
||||
protected getAmountMeta(
|
||||
amount: number,
|
||||
overrideSettings?: IFormatNumberSettings
|
||||
): 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(),
|
||||
};
|
||||
}
|
||||
|
||||
getPercentageBasis = (base, amount) => {
|
||||
return base ? amount / base : 0;
|
||||
};
|
||||
|
||||
getAmountChange = (base, amount) => {
|
||||
return base - amount;
|
||||
};
|
||||
|
||||
protected getPercentageAmountMeta = (amount) => {
|
||||
const formattedAmount = this.formatPercentage(amount);
|
||||
|
||||
return {
|
||||
amount,
|
||||
formattedAmount,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Re
|
||||
* @param {number} amount
|
||||
* @returns
|
||||
*/
|
||||
protected getPercentageTotalAmountMeta = (amount: number) => {
|
||||
const formattedAmount = this.formatTotalPercentage(amount);
|
||||
|
||||
return { amount, formattedAmount };
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import * as R from 'ramda';
|
||||
import { set, sumBy } from 'lodash';
|
||||
import {
|
||||
mapValuesDeepReverse,
|
||||
mapValuesDeep,
|
||||
mapValues,
|
||||
condense,
|
||||
filterDeep,
|
||||
reduceDeep,
|
||||
findValueDeep,
|
||||
filterNodesDeep,
|
||||
} from 'utils/deepdash';
|
||||
|
||||
export const FinancialSheetStructure = (Base: Class) =>
|
||||
class extends Base {
|
||||
/**
|
||||
*
|
||||
* @param nodes
|
||||
* @param callback
|
||||
* @returns
|
||||
*/
|
||||
public mapNodesDeepReverse = (nodes, callback) => {
|
||||
return mapValuesDeepReverse(nodes, callback, {
|
||||
childrenPath: 'children',
|
||||
pathFormat: 'array',
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param nodes
|
||||
* @param callback
|
||||
* @returns
|
||||
*/
|
||||
public mapNodesDeep = (nodes, callback) => {
|
||||
return mapValuesDeep(nodes, callback, {
|
||||
childrenPath: 'children',
|
||||
pathFormat: 'array',
|
||||
});
|
||||
};
|
||||
|
||||
public mapNodes = (nodes, callback) => {
|
||||
return mapValues(nodes, callback, {
|
||||
childrenPath: 'children',
|
||||
pathFormat: 'array',
|
||||
});
|
||||
};
|
||||
|
||||
public filterNodesDeep2 = R.curry((predicate, nodes) => {
|
||||
return filterNodesDeep(predicate, nodes);
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @param
|
||||
*/
|
||||
public filterNodesDeep = (nodes, callback) => {
|
||||
return filterDeep(nodes, callback, {
|
||||
childrenPath: 'children',
|
||||
pathFormat: 'array',
|
||||
});
|
||||
};
|
||||
|
||||
findNodeDeep = (nodes, callback) => {
|
||||
return findValueDeep(nodes, callback, {
|
||||
childrenPath: 'children',
|
||||
pathFormat: 'array',
|
||||
});
|
||||
};
|
||||
|
||||
mapAccNodesDeep = (nodes, callback) => {
|
||||
return reduceDeep(
|
||||
nodes,
|
||||
(acc, value, key, parentValue, context) => {
|
||||
set(
|
||||
acc,
|
||||
context.path,
|
||||
callback(value, key, parentValue, acc, context)
|
||||
);
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
{
|
||||
childrenPath: 'children',
|
||||
pathFormat: 'array',
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public reduceNodesDeep = (nodes, iteratee, accumulator) => {
|
||||
return reduceDeep(nodes, iteratee, accumulator, {
|
||||
childrenPath: 'children',
|
||||
pathFormat: 'array',
|
||||
});
|
||||
};
|
||||
|
||||
getTotalOfChildrenNodes = (node) => {
|
||||
return this.getTotalOfNodes(node.children);
|
||||
};
|
||||
|
||||
getTotalOfNodes = (nodes) => {
|
||||
return sumBy(nodes, 'total.amount');
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import * as R from 'ramda';
|
||||
import { ITableColumn } from '@/interfaces';
|
||||
import { isEmpty, clone, cloneDeep, omit } from 'lodash';
|
||||
import { increment } from 'utils';
|
||||
import { ITableRow } from '@/interfaces';
|
||||
import { IROW_TYPE, DISPLAY_COLUMNS_BY } from './BalanceSheet/constants';
|
||||
|
||||
export const FinancialTable = (Base) =>
|
||||
class extends Base {
|
||||
/**
|
||||
* Table columns cell indexing.
|
||||
* @param {ITableColumn[]} columns
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected tableColumnsCellIndexing = (
|
||||
columns: ITableColumn[]
|
||||
): ITableColumn[] => {
|
||||
const cellIndex = increment(-1);
|
||||
|
||||
return this.mapNodesDeep(columns, (column) => {
|
||||
return isEmpty(column.children)
|
||||
? R.assoc('cellIndex', cellIndex(), column)
|
||||
: column;
|
||||
});
|
||||
};
|
||||
|
||||
addTotalRow = (node: ITableRow) => {
|
||||
const clonedNode = clone(node);
|
||||
|
||||
if (clonedNode.children) {
|
||||
const cells = cloneDeep(node.cells);
|
||||
cells[0].value = this.i18n.__('financial_sheet.total_row', {
|
||||
value: cells[0].value,
|
||||
});
|
||||
|
||||
clonedNode.children.push({
|
||||
...omit(clonedNode, 'children'),
|
||||
cells,
|
||||
rowTypes: [IROW_TYPE.TOTAL],
|
||||
});
|
||||
}
|
||||
return clonedNode;
|
||||
};
|
||||
|
||||
private addTotalRows = (nodes: ITableRow[]) => {
|
||||
return this.mapNodesDeep(nodes, this.addTotalRow);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
import moment from 'moment';
|
||||
import { ITableColumn, IDateRange, ITableColumnAccessor } from '@/interfaces';
|
||||
|
||||
export const FinancialTablePreviousPeriod = (Base) =>
|
||||
class extends Base {
|
||||
getTotalPreviousPeriod = () => {
|
||||
return this.query.PPToDate;
|
||||
};
|
||||
// ----------------------------
|
||||
// # Columns
|
||||
// ----------------------------
|
||||
/**
|
||||
* Retrive previous period total column.
|
||||
* @param {IDateRange} dateRange -
|
||||
* @returns {ITableColumn}
|
||||
*/
|
||||
protected getPreviousPeriodTotalColumn = (
|
||||
dateRange?: IDateRange
|
||||
): ITableColumn => {
|
||||
const PPDate = dateRange
|
||||
? dateRange.toDate
|
||||
: this.getTotalPreviousPeriod();
|
||||
const PPFormatted = moment(PPDate).format('YYYY-MM-DD');
|
||||
|
||||
return {
|
||||
key: 'previous_period',
|
||||
label: this.i18n.__(`financial_sheet.previoud_period_date`, {
|
||||
date: PPFormatted,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve previous period change column.
|
||||
* @returns {ITableColumn}
|
||||
*/
|
||||
protected getPreviousPeriodChangeColumn = (): ITableColumn => {
|
||||
return {
|
||||
key: 'previous_period_change',
|
||||
label: this.i18n.__('fianncial_sheet.previous_period_change'),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve previous period percentage column.
|
||||
* @returns {ITableColumn}
|
||||
*/
|
||||
protected getPreviousPeriodPercentageColumn = (): ITableColumn => {
|
||||
return {
|
||||
key: 'previous_period_percentage',
|
||||
label: this.i18n.__('financial_sheet.previous_period_percentage'),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves previous period total accessor.
|
||||
* @returns {ITableColumnAccessor}
|
||||
*/
|
||||
protected getPreviousPeriodTotalAccessor = (): ITableColumnAccessor => {
|
||||
return {
|
||||
key: 'previous_period',
|
||||
accessor: 'previousPeriod.formattedAmount',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves previous period change accessor.
|
||||
* @returns
|
||||
*/
|
||||
protected getPreviousPeriodChangeAccessor = () => {
|
||||
return {
|
||||
key: 'previous_period_change',
|
||||
accessor: 'previousPeriodChange.formattedAmount',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves previous period percentage accessor.
|
||||
* @returns {ITableColumnAccessor}
|
||||
*/
|
||||
protected getPreviousPeriodPercentageAccessor =
|
||||
(): ITableColumnAccessor => {
|
||||
return {
|
||||
key: 'previous_period_percentage',
|
||||
accessor: 'previousPeriodPercentage.formattedAmount',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves previous period total horizontal column accessor.
|
||||
* @param {number} index
|
||||
* @returns {ITableColumnAccessor}
|
||||
*/
|
||||
protected getPreviousPeriodTotalHorizAccessor = (
|
||||
index: number
|
||||
): ITableColumnAccessor => {
|
||||
return {
|
||||
key: 'previous_period',
|
||||
accessor: `horizontalTotals[${index}].previousPeriod.formattedAmount`,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves previous period change horizontal column accessor.
|
||||
* @param {number} index
|
||||
* @returns {ITableColumnAccessor}
|
||||
*/
|
||||
protected getPreviousPeriodChangeHorizAccessor = (
|
||||
index: number
|
||||
): ITableColumnAccessor => {
|
||||
return {
|
||||
key: 'previous_period_change',
|
||||
accessor: `horizontalTotals[${index}].previousPeriodChange.formattedAmount`,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves pervious period percentage horizontal column accessor.
|
||||
* @param {number} index
|
||||
* @returns {ITableColumnAccessor}
|
||||
*/
|
||||
protected getPreviousPeriodPercentageHorizAccessor = (
|
||||
index: number
|
||||
): ITableColumnAccessor => {
|
||||
return {
|
||||
key: 'previous_period_percentage',
|
||||
accessor: `horizontalTotals[${index}].previousPeriodPercentage.formattedAmount`,
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
import moment from 'moment';
|
||||
import { IDateRange, ITableColumn, ITableColumnAccessor } from '@/interfaces';
|
||||
|
||||
export const FinancialTablePreviousYear = (Base) =>
|
||||
class extends Base {
|
||||
getTotalPreviousYear = () => {
|
||||
return this.query.PYToDate;
|
||||
};
|
||||
// ------------------------------------
|
||||
// # Columns.
|
||||
// ------------------------------------
|
||||
/**
|
||||
* Retrive previous year total column.
|
||||
* @param {DateRange} previousYear -
|
||||
* @returns {ITableColumn}
|
||||
*/
|
||||
protected getPreviousYearTotalColumn = (
|
||||
dateRange?: IDateRange
|
||||
): ITableColumn => {
|
||||
const PYDate = dateRange ? dateRange.toDate : this.getTotalPreviousYear();
|
||||
const PYFormatted = moment(PYDate).format('YYYY-MM-DD');
|
||||
|
||||
return {
|
||||
key: 'previous_year',
|
||||
label: this.i18n.__('financial_sheet.previous_year_date', {
|
||||
date: PYFormatted,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve previous year change column.
|
||||
* @returns {ITableColumn}
|
||||
*/
|
||||
protected getPreviousYearChangeColumn = (): ITableColumn => {
|
||||
return {
|
||||
key: 'previous_year_change',
|
||||
label: this.i18n.__('financial_sheet.previous_year_change'),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve previous year percentage column.
|
||||
* @returns {ITableColumn}
|
||||
*/
|
||||
protected getPreviousYearPercentageColumn = (): ITableColumn => {
|
||||
return {
|
||||
key: 'previous_year_percentage',
|
||||
label: this.i18n.__('financial_sheet.previous_year_percentage'),
|
||||
};
|
||||
};
|
||||
|
||||
// ------------------------------------
|
||||
// # Accessors.
|
||||
// ------------------------------------
|
||||
/**
|
||||
* Retrieves previous year total column accessor.
|
||||
* @returns {ITableColumnAccessor}
|
||||
*/
|
||||
protected getPreviousYearTotalAccessor = (): ITableColumnAccessor => {
|
||||
return {
|
||||
key: 'previous_year',
|
||||
accessor: 'previousYear.formattedAmount',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves previous year change column accessor.
|
||||
* @returns {ITableColumnAccessor}
|
||||
*/
|
||||
protected getPreviousYearChangeAccessor = (): ITableColumnAccessor => {
|
||||
return {
|
||||
key: 'previous_year_change',
|
||||
accessor: 'previousYearChange.formattedAmount',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves previous year percentage column accessor.
|
||||
* @returns {ITableColumnAccessor}
|
||||
*/
|
||||
protected getPreviousYearPercentageAccessor = (): ITableColumnAccessor => {
|
||||
return {
|
||||
key: 'previous_year_percentage',
|
||||
accessor: 'previousYearPercentage.formattedAmount',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves previous year total horizontal column accessor.
|
||||
* @param {number} index
|
||||
* @returns {ITableColumnAccessor}
|
||||
*/
|
||||
protected getPreviousYearTotalHorizAccessor = (
|
||||
index: number
|
||||
): ITableColumnAccessor => {
|
||||
return {
|
||||
key: 'previous_year',
|
||||
accessor: `horizontalTotals[${index}].previousYear.formattedAmount`,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves previous previous year change horizontal column accessor.
|
||||
* @param {number} index
|
||||
* @returns {ITableColumnAccessor}
|
||||
*/
|
||||
protected getPreviousYearChangeHorizAccessor = (
|
||||
index: number
|
||||
): ITableColumnAccessor => {
|
||||
return {
|
||||
key: 'previous_year_change',
|
||||
accessor: `horizontalTotals[${index}].previousYearChange.formattedAmount`,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves previous year percentage horizontal column accessor.
|
||||
* @param {number} index
|
||||
* @returns {ITableColumnAccessor}
|
||||
*/
|
||||
protected getPreviousYearPercentageHorizAccessor = (
|
||||
index: number
|
||||
): ITableColumnAccessor => {
|
||||
return {
|
||||
key: 'previous_year_percentage',
|
||||
accessor: `horizontalTotals[${index}].previousYearPercentage.formattedAmount`,
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,240 @@
|
||||
import { isEmpty, get, last, sumBy } from 'lodash';
|
||||
import {
|
||||
IGeneralLedgerSheetQuery,
|
||||
IGeneralLedgerSheetAccount,
|
||||
IGeneralLedgerSheetAccountBalance,
|
||||
IGeneralLedgerSheetAccountTransaction,
|
||||
IAccount,
|
||||
IJournalPoster,
|
||||
IJournalEntry,
|
||||
IContact,
|
||||
} from '@/interfaces';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
|
||||
/**
|
||||
* General ledger sheet.
|
||||
*/
|
||||
export default class GeneralLedgerSheet extends FinancialSheet {
|
||||
tenantId: number;
|
||||
accounts: IAccount[];
|
||||
query: IGeneralLedgerSheetQuery;
|
||||
openingBalancesJournal: IJournalPoster;
|
||||
transactions: IJournalPoster;
|
||||
contactsMap: Map<number, IContact>;
|
||||
baseCurrency: string;
|
||||
i18n: any;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId -
|
||||
* @param {IAccount[]} accounts -
|
||||
* @param {IJournalPoster} transactions -
|
||||
* @param {IJournalPoster} openingBalancesJournal -
|
||||
* @param {IJournalPoster} closingBalancesJournal -
|
||||
*/
|
||||
constructor(
|
||||
tenantId: number,
|
||||
query: IGeneralLedgerSheetQuery,
|
||||
accounts: IAccount[],
|
||||
contactsByIdMap: Map<number, IContact>,
|
||||
transactions: IJournalPoster,
|
||||
openingBalancesJournal: IJournalPoster,
|
||||
baseCurrency: string,
|
||||
i18n
|
||||
) {
|
||||
super();
|
||||
|
||||
this.tenantId = tenantId;
|
||||
this.query = query;
|
||||
this.numberFormat = this.query.numberFormat;
|
||||
this.accounts = accounts;
|
||||
this.contactsMap = contactsByIdMap;
|
||||
this.transactions = transactions;
|
||||
this.openingBalancesJournal = openingBalancesJournal;
|
||||
this.baseCurrency = baseCurrency;
|
||||
this.i18n = i18n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction amount.
|
||||
* @param {number} credit - Credit amount.
|
||||
* @param {number} debit - Debit amount.
|
||||
* @param {string} normal - Credit or debit.
|
||||
*/
|
||||
getAmount(credit: number, debit: number, normal: string) {
|
||||
return normal === 'credit' ? credit - debit : debit - credit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry mapper.
|
||||
* @param {IJournalEntry} entry -
|
||||
* @return {IGeneralLedgerSheetAccountTransaction}
|
||||
*/
|
||||
entryReducer(
|
||||
entries: IGeneralLedgerSheetAccountTransaction[],
|
||||
entry: IJournalEntry,
|
||||
openingBalance: number
|
||||
): IGeneralLedgerSheetAccountTransaction[] {
|
||||
const lastEntry = last(entries);
|
||||
|
||||
const contact = this.contactsMap.get(entry.contactId);
|
||||
const amount = this.getAmount(
|
||||
entry.credit,
|
||||
entry.debit,
|
||||
entry.accountNormal
|
||||
);
|
||||
const runningBalance =
|
||||
amount + (!isEmpty(entries) ? lastEntry.runningBalance : openingBalance);
|
||||
|
||||
const newEntry = {
|
||||
date: entry.date,
|
||||
entryId: entry.id,
|
||||
|
||||
referenceType: entry.referenceType,
|
||||
referenceId: entry.referenceId,
|
||||
referenceTypeFormatted: this.i18n.__(entry.referenceTypeFormatted),
|
||||
|
||||
contactName: get(contact, 'displayName'),
|
||||
contactType: get(contact, 'contactService'),
|
||||
|
||||
transactionType: entry.transactionType,
|
||||
index: entry.index,
|
||||
note: entry.note,
|
||||
|
||||
credit: entry.credit,
|
||||
debit: entry.debit,
|
||||
amount,
|
||||
runningBalance,
|
||||
|
||||
formattedAmount: this.formatNumber(amount),
|
||||
formattedCredit: this.formatNumber(entry.credit),
|
||||
formattedDebit: this.formatNumber(entry.debit),
|
||||
formattedRunningBalance: this.formatNumber(runningBalance),
|
||||
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
entries.push(newEntry);
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping the account transactions to general ledger transactions of the given account.
|
||||
* @param {IAccount} account
|
||||
* @return {IGeneralLedgerSheetAccountTransaction[]}
|
||||
*/
|
||||
private accountTransactionsMapper(
|
||||
account: IAccount,
|
||||
openingBalance: number
|
||||
): IGeneralLedgerSheetAccountTransaction[] {
|
||||
const entries = this.transactions.getAccountEntries(account.id);
|
||||
|
||||
return entries.reduce(
|
||||
(
|
||||
entries: IGeneralLedgerSheetAccountTransaction[],
|
||||
entry: IJournalEntry
|
||||
) => {
|
||||
return this.entryReducer(entries, entry, openingBalance);
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve account opening balance.
|
||||
* @param {IAccount} account
|
||||
* @return {IGeneralLedgerSheetAccountBalance}
|
||||
*/
|
||||
private accountOpeningBalance(
|
||||
account: IAccount
|
||||
): IGeneralLedgerSheetAccountBalance {
|
||||
const amount = this.openingBalancesJournal.getAccountBalance(account.id);
|
||||
const formattedAmount = this.formatTotalNumber(amount);
|
||||
const currencyCode = this.baseCurrency;
|
||||
const date = this.query.fromDate;
|
||||
|
||||
return { amount, formattedAmount, currencyCode, date };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve account closing balance.
|
||||
* @param {IAccount} account
|
||||
* @return {IGeneralLedgerSheetAccountBalance}
|
||||
*/
|
||||
private accountClosingBalance(
|
||||
openingBalance: number,
|
||||
transactions: IGeneralLedgerSheetAccountTransaction[]
|
||||
): IGeneralLedgerSheetAccountBalance {
|
||||
const amount = this.calcClosingBalance(openingBalance, transactions);
|
||||
const formattedAmount = this.formatTotalNumber(amount);
|
||||
const currencyCode = this.baseCurrency;
|
||||
const date = this.query.toDate;
|
||||
|
||||
return { amount, formattedAmount, currencyCode, date };
|
||||
}
|
||||
|
||||
private calcClosingBalance(
|
||||
openingBalance: number,
|
||||
transactions: IGeneralLedgerSheetAccountTransaction[]
|
||||
) {
|
||||
return openingBalance + sumBy(transactions, (trans) => trans.amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retreive general ledger accounts sections.
|
||||
* @param {IAccount} account
|
||||
* @return {IGeneralLedgerSheetAccount}
|
||||
*/
|
||||
private accountMapper(account: IAccount): IGeneralLedgerSheetAccount {
|
||||
const openingBalance = this.accountOpeningBalance(account);
|
||||
|
||||
const transactions = this.accountTransactionsMapper(
|
||||
account,
|
||||
openingBalance.amount
|
||||
);
|
||||
const closingBalance = this.accountClosingBalance(
|
||||
openingBalance.amount,
|
||||
transactions
|
||||
);
|
||||
|
||||
return {
|
||||
id: account.id,
|
||||
name: account.name,
|
||||
code: account.code,
|
||||
index: account.index,
|
||||
parentAccountId: account.parentAccountId,
|
||||
openingBalance,
|
||||
transactions,
|
||||
closingBalance,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve mapped accounts with general ledger transactions and opeing/closing balance.
|
||||
* @param {IAccount[]} accounts -
|
||||
* @return {IGeneralLedgerSheetAccount[]}
|
||||
*/
|
||||
private accountsWalker(accounts: IAccount[]): IGeneralLedgerSheetAccount[] {
|
||||
return (
|
||||
accounts
|
||||
.map((account: IAccount) => this.accountMapper(account))
|
||||
// Filter general ledger accounts that have no transactions
|
||||
// when`noneTransactions` is on.
|
||||
.filter(
|
||||
(generalLedgerAccount: IGeneralLedgerSheetAccount) =>
|
||||
!(
|
||||
generalLedgerAccount.transactions.length === 0 &&
|
||||
this.query.noneTransactions
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve general ledger report data.
|
||||
* @return {IGeneralLedgerSheetAccount[]}
|
||||
*/
|
||||
public reportData(): IGeneralLedgerSheetAccount[] {
|
||||
return this.accountsWalker(this.accounts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { difference } from 'lodash';
|
||||
import { IGeneralLedgerSheetQuery, IGeneralLedgerMeta } from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import Journal from '@/services/Accounting/JournalPoster';
|
||||
import GeneralLedgerSheet from '@/services/FinancialStatements/GeneralLedger/GeneralLedger';
|
||||
import InventoryService from '@/services/Inventory/Inventory';
|
||||
import { transformToMap, parseBoolean } from 'utils';
|
||||
import { Tenant } from '@/system/models';
|
||||
|
||||
const ERRORS = {
|
||||
ACCOUNTS_NOT_FOUND: 'ACCOUNTS_NOT_FOUND',
|
||||
};
|
||||
|
||||
@Service()
|
||||
export default class GeneralLedgerService {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
inventoryService: InventoryService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Defaults general ledger report filter query.
|
||||
* @return {IBalanceSheetQuery}
|
||||
*/
|
||||
get defaultQuery() {
|
||||
return {
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
basis: 'cash',
|
||||
numberFormat: {
|
||||
noCents: false,
|
||||
divideOn1000: false,
|
||||
},
|
||||
noneZero: false,
|
||||
accountsIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates accounts existance on the storage.
|
||||
* @param {number} tenantId
|
||||
* @param {number[]} accountsIds
|
||||
*/
|
||||
async validateAccountsExistance(tenantId: number, accountsIds: number[]) {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
const storedAccounts = await Account.query().whereIn('id', accountsIds);
|
||||
const storedAccountsIds = storedAccounts.map((a) => a.id);
|
||||
|
||||
if (difference(accountsIds, storedAccountsIds).length > 0) {
|
||||
throw new ServiceError(ERRORS.ACCOUNTS_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the balance sheet meta.
|
||||
* @param {number} tenantId -
|
||||
* @returns {IGeneralLedgerMeta}
|
||||
*/
|
||||
reportMetadata(tenantId: number): IGeneralLedgerMeta {
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
|
||||
const isCostComputeRunning = this.inventoryService
|
||||
.isItemsCostComputeRunning(tenantId);
|
||||
|
||||
const organizationName = settings.get({
|
||||
group: 'organization',
|
||||
key: 'name',
|
||||
});
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
|
||||
return {
|
||||
isCostComputeRunning: parseBoolean(isCostComputeRunning, false),
|
||||
organizationName,
|
||||
baseCurrency
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve general ledger report statement.
|
||||
* ----------
|
||||
* @param {number} tenantId
|
||||
* @param {IGeneralLedgerSheetQuery} query
|
||||
* @return {IGeneralLedgerStatement}
|
||||
*/
|
||||
async generalLedger(
|
||||
tenantId: number,
|
||||
query: IGeneralLedgerSheetQuery
|
||||
): Promise<{
|
||||
data: any;
|
||||
query: IGeneralLedgerSheetQuery;
|
||||
meta: IGeneralLedgerMeta
|
||||
}> {
|
||||
const {
|
||||
accountRepository,
|
||||
transactionsRepository,
|
||||
contactRepository
|
||||
} = this.tenancy.repositories(tenantId);
|
||||
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
|
||||
const tenant = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
// Retrieve all accounts with associated type from the storage.
|
||||
const accounts = await accountRepository.all();
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
// Retrieve all contacts on the storage.
|
||||
const contacts = await contactRepository.all();
|
||||
const contactsByIdMap = transformToMap(contacts, 'id');
|
||||
|
||||
// Retreive journal transactions from/to the given date.
|
||||
const transactions = await transactionsRepository.journal({
|
||||
fromDate: filter.fromDate,
|
||||
toDate: filter.toDate,
|
||||
branchesIds: filter.branchesIds
|
||||
});
|
||||
// Retreive opening balance credit/debit sumation.
|
||||
const openingBalanceTrans = await transactionsRepository.journal({
|
||||
toDate: moment(filter.fromDate).subtract(1, 'day'),
|
||||
sumationCreditDebit: true,
|
||||
branchesIds: filter.branchesIds
|
||||
});
|
||||
// Transform array transactions to journal collection.
|
||||
const transactionsJournal = Journal.fromTransactions(
|
||||
transactions,
|
||||
tenantId,
|
||||
accountsGraph
|
||||
);
|
||||
// Accounts opening transactions.
|
||||
const openingTransJournal = Journal.fromTransactions(
|
||||
openingBalanceTrans,
|
||||
tenantId,
|
||||
accountsGraph
|
||||
);
|
||||
// General ledger report instance.
|
||||
const generalLedgerInstance = new GeneralLedgerSheet(
|
||||
tenantId,
|
||||
filter,
|
||||
accounts,
|
||||
contactsByIdMap,
|
||||
transactionsJournal,
|
||||
openingTransJournal,
|
||||
tenant.metadata.baseCurrency,
|
||||
i18n
|
||||
);
|
||||
// Retrieve general ledger report data.
|
||||
const reportData = generalLedgerInstance.reportData();
|
||||
|
||||
return {
|
||||
data: reportData,
|
||||
query: filter,
|
||||
meta: this.reportMetadata(tenantId),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
import * as R from 'ramda';
|
||||
import { defaultTo, sumBy, get } from 'lodash';
|
||||
import moment from 'moment';
|
||||
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';
|
||||
|
||||
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,
|
||||
i18n: any
|
||||
) {
|
||||
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;
|
||||
this.i18n = i18n;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = R.curry(
|
||||
(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 adjusmtent = b.direction === 'OUT' ? -1 : 1;
|
||||
const total = a.runningValuation.number + b.cost.number * adjusmtent;
|
||||
const totalMeta = this.getNumberMeta(total, { excerptZero: false });
|
||||
const accum = { ...b, runningValuation: totalMeta };
|
||||
|
||||
return [accum, accum];
|
||||
};
|
||||
return R.mapAccum(
|
||||
mapAccumAppender,
|
||||
{ runningValuation: initial },
|
||||
transactions
|
||||
)[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the inventory transaction total.
|
||||
* @param {IInventoryTransaction} transaction
|
||||
* @returns {number}
|
||||
*/
|
||||
private getTransactionTotal = (transaction: IInventoryTransaction) => {
|
||||
return transaction.quantity
|
||||
? transaction.quantity * transaction.rate
|
||||
: transaction.rate;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 total = this.getTransactionTotal(transaction);
|
||||
const amountMovement = this.adjustAmountMovement(transaction.direction);
|
||||
|
||||
// Quantity movement.
|
||||
const quantityMovement = amountMovement(transaction.quantity);
|
||||
const cost = get(transaction, 'costLotAggregated.cost', 0);
|
||||
|
||||
// Profit margin.
|
||||
const profitMargin = total - cost;
|
||||
|
||||
// Value from computed cost in `OUT` or from total sell price in `IN` transaction.
|
||||
const value = transaction.direction === 'OUT' ? cost : total;
|
||||
|
||||
// Value movement depends on transaction direction.
|
||||
const valueMovement = amountMovement(value);
|
||||
|
||||
return {
|
||||
nodeType: INodeTypes.TRANSACTION,
|
||||
date: this.getDateMeta(transaction.date),
|
||||
transactionType: this.i18n.__(transaction.transcationTypeFormatted),
|
||||
transactionNumber: transaction?.meta?.transactionNumber,
|
||||
direction: transaction.direction,
|
||||
|
||||
quantityMovement: this.getNumberMeta(quantityMovement),
|
||||
valueMovement: this.getNumberMeta(valueMovement),
|
||||
|
||||
quantity: this.getNumberMeta(transaction.quantity),
|
||||
total: this.getNumberMeta(total),
|
||||
|
||||
rate: this.getNumberMeta(transaction.rate),
|
||||
cost: this.getNumberMeta(cost),
|
||||
value: this.getNumberMeta(value),
|
||||
|
||||
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,
|
||||
openingValuation
|
||||
);
|
||||
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[],
|
||||
openingValuation: IInventoryDetailsOpening
|
||||
): IInventoryDetailsOpening {
|
||||
const value = sumBy(transactions, 'valueMovement.number');
|
||||
const quantity = sumBy(transactions, 'quantityMovement.number');
|
||||
const profitMargin = sumBy(transactions, 'profitMargin.number');
|
||||
|
||||
const closingQuantity = quantity + openingValuation.quantity.number;
|
||||
const closingValue = value + openingValuation.value.number;
|
||||
|
||||
return {
|
||||
nodeType: INodeTypes.CLOSING_ENTRY,
|
||||
date: this.getDateMeta(this.query.toDate),
|
||||
quantity: this.getTotalNumberMeta(closingQuantity),
|
||||
value: this.getTotalNumberMeta(closingValue),
|
||||
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[]) {
|
||||
const filtered = filterDeep(
|
||||
items,
|
||||
this.isFilterNode.bind(this),
|
||||
MAP_CONFIG
|
||||
);
|
||||
return defaultTo(filtered, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { Inject } from 'typedi';
|
||||
import { raw } from 'objection';
|
||||
import { isEmpty } from 'lodash';
|
||||
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,
|
||||
itemsIds?: number[]
|
||||
): Promise<IItem[]> {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
|
||||
return Item.query().onBuild((q) => {
|
||||
q.where('type', 'inventory');
|
||||
|
||||
if (!isEmpty(itemsIds)) {
|
||||
q.whereIn('id', itemsIds);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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', IF(`QUANTITY` IS NULL, `RATE`, `QUANTITY` * `RATE`), 0) as 'VALUE_IN'"
|
||||
)
|
||||
)
|
||||
.select(
|
||||
raw(
|
||||
"IF(`DIRECTION` = 'OUT', IF(`QUANTITY` IS NULL, `RATE`, `QUANTITY` * `RATE`), 0) as 'VALUE_OUT'"
|
||||
)
|
||||
)
|
||||
.modify('filterDateRange', null, openingBalanceDate)
|
||||
.orderBy('date', 'ASC')
|
||||
.as('inventory_transactions');
|
||||
|
||||
if (!isEmpty(filter.warehousesIds)) {
|
||||
openingTransactions.modify('filterByWarehouses', filter.warehousesIds);
|
||||
}
|
||||
if (!isEmpty(filter.branchesIds)) {
|
||||
openingTransactions.modify('filterByBranches', filter.branchesIds);
|
||||
}
|
||||
|
||||
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)
|
||||
.orderBy('date', 'ASC')
|
||||
.withGraphFetched('meta')
|
||||
.withGraphFetched('costLotAggregated');
|
||||
|
||||
if (!isEmpty(filter.branchesIds)) {
|
||||
inventoryTransactions.modify('filterByBranches', filter.branchesIds);
|
||||
}
|
||||
if (!isEmpty(filter.warehousesIds)) {
|
||||
inventoryTransactions.modify('filterByWarehouses', filter.warehousesIds);
|
||||
}
|
||||
return inventoryTransactions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import moment from 'moment';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import {
|
||||
IInventoryDetailsQuery,
|
||||
IInvetoryItemDetailDOO,
|
||||
IInventoryItemDetailMeta,
|
||||
} from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import InventoryDetails from './InventoryDetails';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
import InventoryDetailsRepository from './InventoryDetailsRepository';
|
||||
import InventoryService from '@/services/Inventory/Inventory';
|
||||
import { parseBoolean } from 'utils';
|
||||
import { Tenant } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export default class InventoryDetailsService extends FinancialSheet {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
reportRepo: InventoryDetailsRepository;
|
||||
|
||||
@Inject()
|
||||
inventoryService: InventoryService;
|
||||
|
||||
/**
|
||||
* Defaults balance sheet filter query.
|
||||
* @return {IBalanceSheetQuery}
|
||||
*/
|
||||
private get defaultQuery(): IInventoryDetailsQuery {
|
||||
return {
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
itemsIds: [],
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
divideOn1000: false,
|
||||
showZero: false,
|
||||
formatMoney: 'total',
|
||||
negativeFormat: 'mines',
|
||||
},
|
||||
noneTransactions: false,
|
||||
branchesIds: [],
|
||||
warehousesIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the balance sheet meta.
|
||||
* @param {number} tenantId -
|
||||
* @returns {IInventoryItemDetailMeta}
|
||||
*/
|
||||
private reportMetadata(tenantId: number): IInventoryItemDetailMeta {
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
|
||||
const isCostComputeRunning =
|
||||
this.inventoryService.isItemsCostComputeRunning(tenantId);
|
||||
|
||||
const organizationName = settings.get({
|
||||
group: 'organization',
|
||||
key: 'name',
|
||||
});
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
|
||||
return {
|
||||
isCostComputeRunning: parseBoolean(isCostComputeRunning, false),
|
||||
organizationName,
|
||||
baseCurrency,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the inventory details report data.
|
||||
* @param {number} tenantId -
|
||||
* @param {IInventoryDetailsQuery} query -
|
||||
* @return {Promise<IInvetoryItemDetailDOO>}
|
||||
*/
|
||||
public async inventoryDetails(
|
||||
tenantId: number,
|
||||
query: IInventoryDetailsQuery
|
||||
): Promise<IInvetoryItemDetailDOO> {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
|
||||
const tenant = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
// Retrieves the items.
|
||||
const items = await this.reportRepo.getInventoryItems(
|
||||
tenantId,
|
||||
filter.itemsIds
|
||||
);
|
||||
// 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,
|
||||
tenant.metadata.baseCurrency,
|
||||
i18n
|
||||
);
|
||||
|
||||
return {
|
||||
data: inventoryDetailsInstance.reportData(),
|
||||
query: filter,
|
||||
meta: this.reportMetadata(tenantId),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
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 {
|
||||
i18n: any;
|
||||
report: any;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {ICashFlowStatement} reportStatement - Report statement.
|
||||
*/
|
||||
constructor(reportStatement, i18n) {
|
||||
this.report = reportStatement;
|
||||
this.i18n = i18n;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: 'total', accessor: 'total.formattedNumber' },
|
||||
{ key: 'value', accessor: 'valueMovement.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: this.i18n.__('Opening balance') },
|
||||
{ key: 'empty' },
|
||||
{ key: 'quantity', accessor: 'quantity.formattedNumber' },
|
||||
{ key: 'empty' },
|
||||
{ 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: this.i18n.__('Closing balance') },
|
||||
{ key: 'empty' },
|
||||
{ key: 'quantity', accessor: 'quantity.formattedNumber' },
|
||||
{ key: 'empty' },
|
||||
{ key: 'empty' },
|
||||
{ key: 'value', accessor: 'value.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
|
||||
)
|
||||
)(node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mappes the items nodes to table rows.
|
||||
* @param {IInventoryDetailsItem[]} items
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
private itemsMapper = (items: IInventoryDetailsItem[]): ITableRow[] => {
|
||||
return mapValuesDeep(items, this.itemMapper, 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: this.i18n.__('Date') },
|
||||
{ key: 'transaction_type', label: this.i18n.__('Transaction type') },
|
||||
{ key: 'transaction_id', label: this.i18n.__('Transaction #') },
|
||||
{ key: 'quantity', label: this.i18n.__('Quantity') },
|
||||
{ key: 'rate', label: this.i18n.__('Rate') },
|
||||
{ key: 'total', label: this.i18n.__('Total') },
|
||||
{ key: 'value', label: this.i18n.__('Value') },
|
||||
{ key: 'profit_margin', label: this.i18n.__('Profit Margin') },
|
||||
{ key: 'running_quantity', label: this.i18n.__('Running quantity') },
|
||||
{ key: 'running_value', label: this.i18n.__('Running Value') },
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
import { sumBy, get, isEmpty } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
import {
|
||||
IItem,
|
||||
IInventoryValuationReportQuery,
|
||||
IInventoryValuationItem,
|
||||
InventoryCostLotTracker,
|
||||
IInventoryValuationStatement,
|
||||
IInventoryValuationTotal,
|
||||
} from '@/interfaces';
|
||||
import { allPassedConditionsPass, transformToMap } from 'utils';
|
||||
|
||||
export default class InventoryValuationSheet extends FinancialSheet {
|
||||
readonly query: IInventoryValuationReportQuery;
|
||||
readonly items: IItem[];
|
||||
readonly INInventoryCostLots: Map<number, InventoryCostLotTracker>;
|
||||
readonly OUTInventoryCostLots: Map<number, InventoryCostLotTracker>;
|
||||
readonly baseCurrency: string;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {IInventoryValuationReportQuery} query
|
||||
* @param items
|
||||
* @param INInventoryCostLots
|
||||
* @param OUTInventoryCostLots
|
||||
* @param baseCurrency
|
||||
*/
|
||||
constructor(
|
||||
query: IInventoryValuationReportQuery,
|
||||
items: IItem[],
|
||||
INInventoryCostLots: Map<number, InventoryCostLotTracker[]>,
|
||||
OUTInventoryCostLots: Map<number, InventoryCostLotTracker[]>,
|
||||
baseCurrency: string
|
||||
) {
|
||||
super();
|
||||
|
||||
this.query = query;
|
||||
this.items = items;
|
||||
this.INInventoryCostLots = transformToMap(INInventoryCostLots, 'itemId');
|
||||
this.OUTInventoryCostLots = transformToMap(OUTInventoryCostLots, 'itemId');
|
||||
this.baseCurrency = baseCurrency;
|
||||
this.numberFormat = this.query.numberFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the item cost and quantity from the given transaction map.
|
||||
* @param {Map<number, InventoryCostLotTracker[]>} transactionsMap
|
||||
* @param {number} itemId
|
||||
* @returns
|
||||
*/
|
||||
private getItemTransaction(
|
||||
transactionsMap: Map<number, InventoryCostLotTracker[]>,
|
||||
itemId: number
|
||||
): { cost: number; quantity: number } {
|
||||
const meta = transactionsMap.get(itemId);
|
||||
|
||||
const cost = get(meta, 'cost', 0);
|
||||
const quantity = get(meta, 'quantity', 0);
|
||||
|
||||
return { cost, quantity };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cost and quantity of the givne item from `IN` transactions.
|
||||
* @param {number} itemId -
|
||||
*/
|
||||
private getItemINTransaction(itemId: number): {
|
||||
cost: number;
|
||||
quantity: number;
|
||||
} {
|
||||
return this.getItemTransaction(this.INInventoryCostLots, itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cost and quantity of the given item from `OUT` transactions.
|
||||
* @param {number} itemId -
|
||||
*/
|
||||
private getItemOUTTransaction(itemId: number): {
|
||||
cost: number;
|
||||
quantity: number;
|
||||
} {
|
||||
return this.getItemTransaction(this.OUTInventoryCostLots, itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the item closing valuation.
|
||||
* @param {number} itemId - Item id.
|
||||
*/
|
||||
private getItemValuation(itemId: number): number {
|
||||
const { cost: INValuation } = this.getItemINTransaction(itemId);
|
||||
const { cost: OUTValuation } = this.getItemOUTTransaction(itemId);
|
||||
|
||||
return Math.max(INValuation - OUTValuation, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the item closing quantity.
|
||||
* @param {number} itemId - Item id.
|
||||
*/
|
||||
private getItemQuantity(itemId: number): number {
|
||||
const { quantity: INQuantity } = this.getItemINTransaction(itemId);
|
||||
const { quantity: OUTQuantity } = this.getItemOUTTransaction(itemId);
|
||||
|
||||
return INQuantity - OUTQuantity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the item weighted average cost from the given valuation and quantity.
|
||||
* @param {number} valuation
|
||||
* @param {number} quantity
|
||||
* @returns {number}
|
||||
*/
|
||||
private calcAverage(valuation: number, quantity: number): number {
|
||||
return quantity ? valuation / quantity : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping the item model object to inventory valuation item
|
||||
* @param {IItem} item
|
||||
* @returns {IInventoryValuationItem}
|
||||
*/
|
||||
private itemMapper(item: IItem): IInventoryValuationItem {
|
||||
const valuation = this.getItemValuation(item.id);
|
||||
const quantity = this.getItemQuantity(item.id);
|
||||
const average = this.calcAverage(valuation, quantity);
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
code: item.code,
|
||||
valuation,
|
||||
quantity,
|
||||
average,
|
||||
valuationFormatted: this.formatNumber(valuation),
|
||||
quantityFormatted: this.formatNumber(quantity, { money: false }),
|
||||
averageFormatted: this.formatNumber(average, { money: false }),
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter none transactions items.
|
||||
* @param {IInventoryValuationItem} valuationItem -
|
||||
* @return {boolean}
|
||||
*/
|
||||
private filterNoneTransactions = (
|
||||
valuationItem: IInventoryValuationItem
|
||||
): boolean => {
|
||||
const transactionIN = this.INInventoryCostLots.get(valuationItem.id);
|
||||
const transactionOUT = this.OUTInventoryCostLots.get(valuationItem.id);
|
||||
|
||||
return transactionOUT || transactionIN;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter active only items.
|
||||
* @param {IInventoryValuationItem} valuationItem -
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private filterActiveOnly = (
|
||||
valuationItem: IInventoryValuationItem
|
||||
): boolean => {
|
||||
return (
|
||||
valuationItem.average !== 0 ||
|
||||
valuationItem.quantity !== 0 ||
|
||||
valuationItem.valuation !== 0
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter none-zero total valuation items.
|
||||
* @param {IInventoryValuationItem} valuationItem
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private filterNoneZero = (valuationItem: IInventoryValuationItem) => {
|
||||
return valuationItem.valuation !== 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters the inventory valuation items based on query.
|
||||
* @param {IInventoryValuationItem} valuationItem
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private itemFilter = (valuationItem: IInventoryValuationItem): boolean => {
|
||||
const { noneTransactions, noneZero, onlyActive } = this.query;
|
||||
|
||||
// Conditions pair filter detarminer.
|
||||
const condsPairFilters = [
|
||||
[noneTransactions, this.filterNoneTransactions],
|
||||
[noneZero, this.filterNoneZero],
|
||||
[onlyActive, this.filterActiveOnly],
|
||||
];
|
||||
return allPassedConditionsPass(condsPairFilters)(valuationItem);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mappes the items to inventory valuation items nodes.
|
||||
* @param {IItem[]} items
|
||||
* @returns {IInventoryValuationItem[]}
|
||||
*/
|
||||
private itemsMapper = (items: IItem[]): IInventoryValuationItem[] => {
|
||||
return this.items.map(this.itemMapper.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters the inventory valuation items nodes.
|
||||
* @param {IInventoryValuationItem[]} nodes -
|
||||
* @returns {IInventoryValuationItem[]}
|
||||
*/
|
||||
private itemsFilter = (
|
||||
nodes: IInventoryValuationItem[]
|
||||
): IInventoryValuationItem[] => {
|
||||
return nodes.filter(this.itemFilter);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether the items post filter is active.
|
||||
*/
|
||||
private isItemsPostFilter = (): boolean => {
|
||||
return isEmpty(this.query.itemsIds);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the inventory valuation items.
|
||||
* @returns {IInventoryValuationItem[]}
|
||||
*/
|
||||
private itemsSection(): IInventoryValuationItem[] {
|
||||
return R.compose(
|
||||
R.when(this.isItemsPostFilter, this.itemsFilter),
|
||||
this.itemsMapper
|
||||
)(this.items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the inventory valuation total.
|
||||
* @param {IInventoryValuationItem[]} items
|
||||
* @returns {IInventoryValuationTotal}
|
||||
*/
|
||||
private totalSection(
|
||||
items: IInventoryValuationItem[]
|
||||
): IInventoryValuationTotal {
|
||||
const valuation = sumBy(items, (item) => item.valuation);
|
||||
const quantity = sumBy(items, (item) => item.quantity);
|
||||
|
||||
return {
|
||||
valuation,
|
||||
quantity,
|
||||
valuationFormatted: this.formatTotalNumber(valuation),
|
||||
quantityFormatted: this.formatTotalNumber(quantity, { money: false }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the inventory valuation report data.
|
||||
* @returns {IInventoryValuationStatement}
|
||||
*/
|
||||
public reportData(): IInventoryValuationStatement {
|
||||
const items = this.itemsSection();
|
||||
const total = this.totalSection(items);
|
||||
|
||||
return items.length > 0 ? { items, total } : {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
IInventoryValuationReportQuery,
|
||||
IInventoryValuationSheetMeta,
|
||||
} from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import InventoryValuationSheet from './InventoryValuationSheet';
|
||||
import InventoryService from '@/services/Inventory/Inventory';
|
||||
import { Tenant } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export default class InventoryValuationSheetService {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
inventoryService: InventoryService;
|
||||
|
||||
/**
|
||||
* Defaults balance sheet filter query.
|
||||
* @return {IBalanceSheetQuery}
|
||||
*/
|
||||
get defaultQuery(): IInventoryValuationReportQuery {
|
||||
return {
|
||||
asDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
itemsIds: [],
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
divideOn1000: false,
|
||||
showZero: false,
|
||||
formatMoney: 'always',
|
||||
negativeFormat: 'mines',
|
||||
},
|
||||
noneTransactions: true,
|
||||
noneZero: false,
|
||||
onlyActive: false,
|
||||
|
||||
warehousesIds: [],
|
||||
branchesIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the balance sheet meta.
|
||||
* @param {number} tenantId -
|
||||
* @returns {IBalanceSheetMeta}
|
||||
*/
|
||||
reportMetadata(tenantId: number): IInventoryValuationSheetMeta {
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
|
||||
const isCostComputeRunning =
|
||||
this.inventoryService.isItemsCostComputeRunning(tenantId);
|
||||
|
||||
const organizationName = settings.get({
|
||||
group: 'organization',
|
||||
key: 'name',
|
||||
});
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
|
||||
return {
|
||||
organizationName,
|
||||
baseCurrency,
|
||||
isCostComputeRunning,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inventory valuation sheet.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {IInventoryValuationReportQuery} query - Valuation query.
|
||||
*/
|
||||
public async inventoryValuationSheet(
|
||||
tenantId: number,
|
||||
query: IInventoryValuationReportQuery
|
||||
) {
|
||||
const { Item, InventoryCostLotTracker } = this.tenancy.models(tenantId);
|
||||
|
||||
const tenant = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
const inventoryItems = await Item.query().onBuild((q) => {
|
||||
q.where('type', 'inventory');
|
||||
|
||||
if (filter.itemsIds.length > 0) {
|
||||
q.whereIn('id', filter.itemsIds);
|
||||
}
|
||||
});
|
||||
const inventoryItemsIds = inventoryItems.map((item) => item.id);
|
||||
|
||||
const commonQuery = (builder) => {
|
||||
builder.whereIn('item_id', inventoryItemsIds);
|
||||
builder.sum('rate as rate');
|
||||
builder.sum('quantity as quantity');
|
||||
builder.sum('cost as cost');
|
||||
builder.select('itemId');
|
||||
builder.groupBy('itemId');
|
||||
|
||||
if (!isEmpty(query.branchesIds)) {
|
||||
builder.modify('filterByBranches', query.branchesIds);
|
||||
}
|
||||
if (!isEmpty(query.warehousesIds)) {
|
||||
builder.modify('filterByWarehouses', query.warehousesIds);
|
||||
}
|
||||
};
|
||||
// Retrieve the inventory cost `IN` transactions.
|
||||
const INTransactions = await InventoryCostLotTracker.query()
|
||||
.onBuild(commonQuery)
|
||||
.where('direction', 'IN');
|
||||
|
||||
// Retrieve the inventory cost `OUT` transactions.
|
||||
const OUTTransactions = await InventoryCostLotTracker.query()
|
||||
.onBuild(commonQuery)
|
||||
.where('direction', 'OUT');
|
||||
|
||||
const inventoryValuationInstance = new InventoryValuationSheet(
|
||||
filter,
|
||||
inventoryItems,
|
||||
INTransactions,
|
||||
OUTTransactions,
|
||||
tenant.metadata.baseCurrency
|
||||
);
|
||||
// Retrieve the inventory valuation report data.
|
||||
const inventoryValuationData = inventoryValuationInstance.reportData();
|
||||
|
||||
return {
|
||||
data: inventoryValuationData,
|
||||
query: filter,
|
||||
meta: this.reportMetadata(tenantId),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { sumBy, chain, get, head } from 'lodash';
|
||||
import {
|
||||
IJournalEntry,
|
||||
IJournalPoster,
|
||||
IJournalReportEntriesGroup,
|
||||
IJournalReportQuery,
|
||||
IJournalReport,
|
||||
IContact,
|
||||
} from '@/interfaces';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
|
||||
export default class JournalSheet extends FinancialSheet {
|
||||
readonly tenantId: number;
|
||||
readonly journal: IJournalPoster;
|
||||
readonly query: IJournalReportQuery;
|
||||
readonly baseCurrency: string;
|
||||
readonly contactsById: Map<number | string, IContact>;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId
|
||||
* @param {IJournalPoster} journal
|
||||
*/
|
||||
constructor(
|
||||
tenantId: number,
|
||||
query: IJournalReportQuery,
|
||||
journal: IJournalPoster,
|
||||
accountsGraph: any,
|
||||
contactsById: Map<number | string, IContact>,
|
||||
baseCurrency: string,
|
||||
i18n
|
||||
) {
|
||||
super();
|
||||
|
||||
this.tenantId = tenantId;
|
||||
this.journal = journal;
|
||||
this.query = query;
|
||||
this.numberFormat = this.query.numberFormat;
|
||||
this.accountsGraph = accountsGraph;
|
||||
this.contactsById = contactsById;
|
||||
this.baseCurrency = baseCurrency;
|
||||
this.i18n = i18n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry mapper.
|
||||
* @param {IJournalEntry} entry
|
||||
*/
|
||||
entryMapper(entry: IJournalEntry) {
|
||||
const account = this.accountsGraph.getNodeData(entry.accountId);
|
||||
const contact = this.contactsById.get(entry.contactId);
|
||||
|
||||
return {
|
||||
entryId: entry.id,
|
||||
index: entry.index,
|
||||
note: entry.note,
|
||||
|
||||
contactName: get(contact, 'displayName'),
|
||||
contactType: get(contact, 'contactService'),
|
||||
|
||||
accountName: account.name,
|
||||
accountCode: account.code,
|
||||
transactionNumber: entry.transactionNumber,
|
||||
|
||||
currencyCode: this.baseCurrency,
|
||||
formattedCredit: this.formatNumber(entry.credit),
|
||||
formattedDebit: this.formatNumber(entry.debit),
|
||||
|
||||
credit: entry.credit,
|
||||
debit: entry.debit,
|
||||
|
||||
createdAt: entry.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappes the journal entries.
|
||||
* @param {IJournalEntry[]} entries -
|
||||
*/
|
||||
entriesMapper(entries: IJournalEntry[]) {
|
||||
return entries.map(this.entryMapper.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping journal entries groups.
|
||||
* @param {IJournalEntry[]} entriesGroup -
|
||||
* @param {string} key -
|
||||
* @return {IJournalReportEntriesGroup}
|
||||
*/
|
||||
entriesGroupsMapper(
|
||||
entriesGroup: IJournalEntry[],
|
||||
groupEntry: IJournalEntry
|
||||
): IJournalReportEntriesGroup {
|
||||
const totalCredit = sumBy(entriesGroup, 'credit');
|
||||
const totalDebit = sumBy(entriesGroup, 'debit');
|
||||
|
||||
return {
|
||||
date: groupEntry.date,
|
||||
referenceType: groupEntry.referenceType,
|
||||
referenceId: groupEntry.referenceId,
|
||||
referenceTypeFormatted: this.i18n.__(groupEntry.referenceTypeFormatted),
|
||||
|
||||
entries: this.entriesMapper(entriesGroup),
|
||||
|
||||
currencyCode: this.baseCurrency,
|
||||
|
||||
credit: totalCredit,
|
||||
debit: totalDebit,
|
||||
|
||||
formattedCredit: this.formatTotalNumber(totalCredit),
|
||||
formattedDebit: this.formatTotalNumber(totalDebit),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping the journal entries to entries groups.
|
||||
* @param {IJournalEntry[]} entries
|
||||
* @return {IJournalReportEntriesGroup[]}
|
||||
*/
|
||||
entriesWalker(entries: IJournalEntry[]): IJournalReportEntriesGroup[] {
|
||||
return chain(entries)
|
||||
.groupBy((entry) => `${entry.referenceId}-${entry.referenceType}`)
|
||||
.map((entriesGroup: IJournalEntry[], key: string) => {
|
||||
const headEntry = head(entriesGroup);
|
||||
return this.entriesGroupsMapper(entriesGroup, headEntry);
|
||||
})
|
||||
.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve journal report.
|
||||
* @return {IJournalReport}
|
||||
*/
|
||||
reportData(): IJournalReport {
|
||||
return this.entriesWalker(this.journal.entries);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import { IJournalReportQuery, IJournalSheetMeta } from '@/interfaces';
|
||||
|
||||
import JournalSheet from './JournalSheet';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import Journal from '@/services/Accounting/JournalPoster';
|
||||
import InventoryService from '@/services/Inventory/Inventory';
|
||||
import { parseBoolean, transformToMap } from 'utils';
|
||||
import { Tenant } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export default class JournalSheetService {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
inventoryService: InventoryService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Default journal sheet filter queyr.
|
||||
*/
|
||||
get defaultQuery() {
|
||||
return {
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
fromRange: null,
|
||||
toRange: null,
|
||||
accountsIds: [],
|
||||
numberFormat: {
|
||||
noCents: false,
|
||||
divideOn1000: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the balance sheet meta.
|
||||
* @param {number} tenantId -
|
||||
* @returns {IBalanceSheetMeta}
|
||||
*/
|
||||
reportMetadata(tenantId: number): IJournalSheetMeta {
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
|
||||
const isCostComputeRunning =
|
||||
this.inventoryService.isItemsCostComputeRunning(tenantId);
|
||||
|
||||
const organizationName = settings.get({
|
||||
group: 'organization',
|
||||
key: 'name',
|
||||
});
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
|
||||
return {
|
||||
isCostComputeRunning: parseBoolean(isCostComputeRunning, false),
|
||||
organizationName,
|
||||
baseCurrency,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Journal sheet.
|
||||
* @param {number} tenantId
|
||||
* @param {IJournalSheetFilterQuery} query
|
||||
*/
|
||||
async journalSheet(tenantId: number, query: IJournalReportQuery) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const { accountRepository, transactionsRepository, contactRepository } =
|
||||
this.tenancy.repositories(tenantId);
|
||||
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
this.logger.info('[journal] trying to calculate the report.', {
|
||||
tenantId,
|
||||
filter,
|
||||
});
|
||||
|
||||
const tenant = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
// Retrieve all accounts on the storage.
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
// Retrieve all contacts on the storage.
|
||||
const contacts = await contactRepository.all();
|
||||
const contactsByIdMap = transformToMap(contacts, 'id');
|
||||
|
||||
// Retrieve all journal transactions based on the given query.
|
||||
const transactions = await AccountTransaction.query().onBuild((query) => {
|
||||
if (filter.fromRange || filter.toRange) {
|
||||
query.modify('filterAmountRange', filter.fromRange, filter.toRange);
|
||||
}
|
||||
query.modify('filterDateRange', filter.fromDate, filter.toDate);
|
||||
query.orderBy(['date', 'createdAt', 'indexGroup', 'index']);
|
||||
|
||||
if (filter.transactionType) {
|
||||
query.where('reference_type', filter.transactionType);
|
||||
}
|
||||
if (filter.transactionType && filter.transactionId) {
|
||||
query.where('reference_id', filter.transactionId);
|
||||
}
|
||||
});
|
||||
// Transform the transactions array to journal collection.
|
||||
const transactionsJournal = Journal.fromTransactions(
|
||||
transactions,
|
||||
tenantId,
|
||||
accountsGraph
|
||||
);
|
||||
// Journal report instance.
|
||||
const journalSheetInstance = new JournalSheet(
|
||||
tenantId,
|
||||
filter,
|
||||
transactionsJournal,
|
||||
accountsGraph,
|
||||
contactsByIdMap,
|
||||
tenant.metadata.baseCurrency,
|
||||
i18n
|
||||
);
|
||||
// Retrieve journal report columns.
|
||||
const journalSheetData = journalSheetInstance.reportData();
|
||||
|
||||
return {
|
||||
data: journalSheetData,
|
||||
query: filter,
|
||||
meta: this.reportMetadata(tenantId),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import * as R from 'ramda';
|
||||
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
|
||||
import {
|
||||
ProfitLossAggregateNodeId,
|
||||
ProfitLossNodeType,
|
||||
IProfitLossSchemaNode,
|
||||
} from '@/interfaces';
|
||||
import { FinancialSchema } from '../FinancialSchema';
|
||||
|
||||
export const ProfitLossShema = (Base) =>
|
||||
class extends R.compose(FinancialSchema)(Base) {
|
||||
/**
|
||||
* Retrieves the report schema.
|
||||
* @returns {IProfitLossSchemaNode[]}
|
||||
*/
|
||||
getSchema = (): IProfitLossSchemaNode[] => {
|
||||
return getProfitLossSheetSchema();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves P&L sheet schema.
|
||||
* @returns {IProfitLossSchemaNode}
|
||||
*/
|
||||
export const getProfitLossSheetSchema = (): IProfitLossSchemaNode[] => [
|
||||
{
|
||||
id: ProfitLossAggregateNodeId.INCOME,
|
||||
name: 'profit_loss_sheet.income',
|
||||
nodeType: ProfitLossNodeType.ACCOUNTS,
|
||||
accountsTypes: [ACCOUNT_TYPE.INCOME],
|
||||
alwaysShow: true,
|
||||
},
|
||||
{
|
||||
id: ProfitLossAggregateNodeId.COS,
|
||||
name: 'profit_loss_sheet.cost_of_sales',
|
||||
nodeType: ProfitLossNodeType.ACCOUNTS,
|
||||
accountsTypes: [ACCOUNT_TYPE.COST_OF_GOODS_SOLD],
|
||||
},
|
||||
{
|
||||
id: ProfitLossAggregateNodeId.GROSS_PROFIT,
|
||||
name: 'profit_loss_sheet.gross_profit',
|
||||
nodeType: ProfitLossNodeType.EQUATION,
|
||||
equation: `${ProfitLossAggregateNodeId.INCOME} - ${ProfitLossAggregateNodeId.COS}`,
|
||||
},
|
||||
{
|
||||
id: ProfitLossAggregateNodeId.EXPENSES,
|
||||
name: 'profit_loss_sheet.expenses',
|
||||
nodeType: ProfitLossNodeType.ACCOUNTS,
|
||||
accountsTypes: [ACCOUNT_TYPE.EXPENSE],
|
||||
alwaysShow: true,
|
||||
},
|
||||
{
|
||||
id: ProfitLossAggregateNodeId.NET_OPERATING_INCOME,
|
||||
name: 'profit_loss_sheet.net_operating_income',
|
||||
nodeType: ProfitLossNodeType.EQUATION,
|
||||
equation: `${ProfitLossAggregateNodeId.GROSS_PROFIT} - ${ProfitLossAggregateNodeId.EXPENSES}`,
|
||||
},
|
||||
{
|
||||
id: ProfitLossAggregateNodeId.OTHER_INCOME,
|
||||
name: 'profit_loss_sheet.other_income',
|
||||
nodeType: ProfitLossNodeType.ACCOUNTS,
|
||||
accountsTypes: [ACCOUNT_TYPE.OTHER_INCOME],
|
||||
},
|
||||
{
|
||||
id: ProfitLossAggregateNodeId.OTHER_EXPENSES,
|
||||
name: 'profit_loss_sheet.other_expenses',
|
||||
nodeType: ProfitLossNodeType.ACCOUNTS,
|
||||
accountsTypes: [ACCOUNT_TYPE.OTHER_EXPENSE],
|
||||
},
|
||||
{
|
||||
id: ProfitLossAggregateNodeId.NET_INCOME,
|
||||
name: 'profit_loss_sheet.net_income',
|
||||
nodeType: ProfitLossNodeType.EQUATION,
|
||||
equation: `${ProfitLossAggregateNodeId.NET_OPERATING_INCOME} + ${ProfitLossAggregateNodeId.OTHER_INCOME} - ${ProfitLossAggregateNodeId.OTHER_EXPENSES}`,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,324 @@
|
||||
import * as R from 'ramda';
|
||||
import { IProfitLossSheetQuery } from '@/interfaces/ProfitLossSheet';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
import { FinancialSheetStructure } from '../FinancialSheetStructure';
|
||||
import {
|
||||
ProfitLossNodeType,
|
||||
IProfitLossSheetEquationNode,
|
||||
IProfitLossEquationSchemaNode,
|
||||
IProfitLossSheetAccountsNode,
|
||||
IProfitLossAccountsSchemaNode,
|
||||
IProfitLossSchemaNode,
|
||||
IProfitLossSheetNode,
|
||||
IAccount,
|
||||
IProfitLossSheetAccountNode,
|
||||
} from '@/interfaces';
|
||||
import { ProfitLossShema } from './ProfitLossSchema';
|
||||
import { ProfitLossSheetPercentage } from './ProfitLossSheetPercentage';
|
||||
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
|
||||
import { ProfitLossSheetRepository } from './ProfitLossSheetRepository';
|
||||
import { ProfitLossSheetBase } from './ProfitLossSheetBase';
|
||||
import { ProfitLossSheetDatePeriods } from './ProfitLossSheetDatePeriods';
|
||||
import { FinancialEvaluateEquation } from '../FinancialEvaluateEquation';
|
||||
import { ProfitLossSheetPreviousYear } from './ProfitLossSheetPreviousYear';
|
||||
import { ProfitLossSheetPreviousPeriod } from './ProfitLossSheetPreviousPeriod';
|
||||
import { FinancialDateRanges } from '../FinancialDateRanges';
|
||||
import { ProfitLossSheetFilter } from './ProfitLossSheetFilter';
|
||||
|
||||
export default class ProfitLossSheet extends R.compose(
|
||||
ProfitLossSheetPreviousYear,
|
||||
ProfitLossSheetPreviousPeriod,
|
||||
ProfitLossSheetPercentage,
|
||||
ProfitLossSheetDatePeriods,
|
||||
ProfitLossSheetFilter,
|
||||
ProfitLossShema,
|
||||
ProfitLossSheetBase,
|
||||
FinancialDateRanges,
|
||||
FinancialEvaluateEquation,
|
||||
FinancialSheetStructure
|
||||
)(FinancialSheet) {
|
||||
/**
|
||||
* Profit/Loss sheet query.
|
||||
* @param {ProfitLossSheetQuery}
|
||||
*/
|
||||
readonly query: ProfitLossSheetQuery;
|
||||
/**
|
||||
* @param {string}
|
||||
*/
|
||||
readonly comparatorDateType: string;
|
||||
|
||||
/**
|
||||
* Organization's base currency.
|
||||
* @param {string}
|
||||
*/
|
||||
readonly baseCurrency: string;
|
||||
|
||||
/**
|
||||
* Profit/Loss repository.
|
||||
* @param {ProfitLossSheetRepository}
|
||||
*/
|
||||
readonly repository: ProfitLossSheetRepository;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {IProfitLossSheetQuery} query -
|
||||
* @param {IAccount[]} accounts -
|
||||
* @param {IJournalPoster} transactionsJournal -
|
||||
*/
|
||||
constructor(
|
||||
repository: ProfitLossSheetRepository,
|
||||
query: IProfitLossSheetQuery,
|
||||
baseCurrency: string,
|
||||
i18n: any
|
||||
) {
|
||||
super();
|
||||
|
||||
this.query = new ProfitLossSheetQuery(query);
|
||||
this.repository = repository;
|
||||
this.numberFormat = this.query.query.numberFormat;
|
||||
this.baseCurrency = baseCurrency;
|
||||
this.i18n = i18n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the sheet account node from the given account.
|
||||
* @param {IAccount} account
|
||||
* @returns {IProfitLossSheetAccountNode}
|
||||
*/
|
||||
private accountNodeMapper = (
|
||||
account: IAccount
|
||||
): IProfitLossSheetAccountNode => {
|
||||
const total = this.repository.totalAccountsLedger
|
||||
.whereAccountId(account.id)
|
||||
.getClosingBalance();
|
||||
|
||||
return {
|
||||
id: account.id,
|
||||
name: account.name,
|
||||
nodeType: ProfitLossNodeType.ACCOUNT,
|
||||
total: this.getAmountMeta(total),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Compose account node.
|
||||
* @param {IAccount} node
|
||||
* @returns {IProfitLossSheetAccountNode}
|
||||
*/
|
||||
private accountNodeCompose = (
|
||||
account: IAccount
|
||||
): IProfitLossSheetAccountNode => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.query.isPreviousPeriodActive,
|
||||
this.previousPeriodAccountNodeCompose
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearActive,
|
||||
this.previousYearAccountNodeCompose
|
||||
),
|
||||
R.when(
|
||||
this.query.isDatePeriodsColumnsType,
|
||||
this.assocAccountNodeDatePeriod
|
||||
),
|
||||
this.accountNodeMapper
|
||||
)(account);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve report accounts nodes by the given accounts types.
|
||||
* @param {string[]} types
|
||||
* @returns {IBalanceSheetAccountNode}
|
||||
*/
|
||||
private getAccountsNodesByTypes = (
|
||||
types: string[]
|
||||
): IProfitLossSheetAccountNode[] => {
|
||||
return R.compose(
|
||||
R.map(this.accountNodeCompose),
|
||||
R.flatten,
|
||||
R.map(this.repository.getAccountsByType)
|
||||
)(types);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mapps the accounts schema node to report node.
|
||||
* @param {IProfitLossSchemaNode} node
|
||||
* @returns {IProfitLossSheetNode}
|
||||
*/
|
||||
private accountsSchemaNodeMapper = (
|
||||
node: IProfitLossAccountsSchemaNode
|
||||
): IProfitLossSheetNode => {
|
||||
// Retrieve accounts node by the given types.
|
||||
const children = this.getAccountsNodesByTypes(node.accountsTypes);
|
||||
|
||||
// Retrieve the total of the given nodes.
|
||||
const total = this.getTotalOfNodes(children);
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
name: this.i18n.__(node.name),
|
||||
nodeType: ProfitLossNodeType.ACCOUNTS,
|
||||
total: this.getTotalAmountMeta(total),
|
||||
children,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Accounts schema node composer.
|
||||
* @param {IProfitLossSchemaNode} node
|
||||
* @returns {IProfitLossSheetAccountsNode}
|
||||
*/
|
||||
private accountsSchemaNodeCompose = (
|
||||
node: IProfitLossSchemaNode
|
||||
): IProfitLossSheetAccountsNode => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.query.isPreviousPeriodActive,
|
||||
this.previousPeriodAggregateNodeCompose
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearActive,
|
||||
this.previousYearAggregateNodeCompose
|
||||
),
|
||||
R.when(
|
||||
this.query.isDatePeriodsColumnsType,
|
||||
this.assocAggregateDatePeriod
|
||||
),
|
||||
this.accountsSchemaNodeMapper
|
||||
)(node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Equation schema node parser.
|
||||
* @param {(IProfitLossSchemaNode | IProfitLossSheetNode)[]} accNodes -
|
||||
* @param {IProfitLossEquationSchemaNode} node -
|
||||
* @param {IProfitLossSheetEquationNode}
|
||||
*/
|
||||
private equationSchemaNodeParser = R.curry(
|
||||
(
|
||||
accNodes: (IProfitLossSchemaNode | IProfitLossSheetNode)[],
|
||||
node: IProfitLossEquationSchemaNode
|
||||
): IProfitLossSheetEquationNode => {
|
||||
const tableNodes = this.getNodesTableForEvaluating(
|
||||
'total.amount',
|
||||
accNodes
|
||||
);
|
||||
// Evaluate the given equation.
|
||||
const total = this.evaluateEquation(node.equation, tableNodes);
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
name: this.i18n.__(node.name),
|
||||
nodeType: ProfitLossNodeType.EQUATION,
|
||||
total: this.getTotalAmountMeta(total),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Equation schema node composer.
|
||||
* @param {(IProfitLossSchemaNode | IProfitLossSheetNode)[]} accNodes -
|
||||
* @param {IProfitLossSchemaNode} node -
|
||||
* @returns {IProfitLossSheetEquationNode}
|
||||
*/
|
||||
private equationSchemaNodeCompose = R.curry(
|
||||
(
|
||||
accNodes: (IProfitLossSchemaNode | IProfitLossSheetNode)[],
|
||||
node: IProfitLossEquationSchemaNode
|
||||
): IProfitLossSheetEquationNode => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.query.isPreviousPeriodActive,
|
||||
this.previousPeriodEquationNodeCompose(accNodes, node.equation)
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearActive,
|
||||
this.previousYearEquationNodeCompose(accNodes, node.equation)
|
||||
),
|
||||
R.when(
|
||||
this.query.isDatePeriodsColumnsType,
|
||||
this.assocEquationNodeDatePeriod(accNodes, node.equation)
|
||||
),
|
||||
this.equationSchemaNodeParser(accNodes)
|
||||
)(node);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses accounts schema node to report node.
|
||||
* @param {IProfitLossSchemaNode} schemaNode
|
||||
* @returns {IProfitLossSheetNode | IProfitLossSchemaNode}
|
||||
*/
|
||||
private accountsSchemaNodeMap = (
|
||||
schemaNode: IProfitLossSchemaNode
|
||||
): IProfitLossSheetNode | IProfitLossSchemaNode => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.isNodeType(ProfitLossNodeType.ACCOUNTS),
|
||||
this.accountsSchemaNodeCompose
|
||||
)
|
||||
)(schemaNode);
|
||||
};
|
||||
|
||||
/**
|
||||
* Composes schema equation node to report node.
|
||||
* @param {IProfitLossSheetNode | IProfitLossSchemaNode} node
|
||||
* @param {number} key
|
||||
* @param {IProfitLossSheetNode | IProfitLossSchemaNode} parentValue
|
||||
* @param {(IProfitLossSheetNode | IProfitLossSchemaNode)[]} accNodes
|
||||
* @param context
|
||||
* @returns {IProfitLossSheetEquationNode}
|
||||
*/
|
||||
private reportSchemaEquationNodeCompose = (
|
||||
node: IProfitLossSheetNode | IProfitLossSchemaNode,
|
||||
key: number,
|
||||
parentValue: IProfitLossSheetNode | IProfitLossSchemaNode,
|
||||
accNodes: (IProfitLossSheetNode | IProfitLossSchemaNode)[],
|
||||
context
|
||||
): IProfitLossSheetEquationNode => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.isNodeType(ProfitLossNodeType.EQUATION),
|
||||
this.equationSchemaNodeCompose(accNodes)
|
||||
)
|
||||
)(node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses schema accounts nodes.
|
||||
* @param {IProfitLossSchemaNode[]}
|
||||
* @returns {(IProfitLossSheetNode | IProfitLossSchemaNode)[]}
|
||||
*/
|
||||
private reportSchemaAccountsNodesCompose = (
|
||||
schemaNodes: IProfitLossSchemaNode[]
|
||||
): (IProfitLossSheetNode | IProfitLossSchemaNode)[] => {
|
||||
return this.mapNodesDeep(schemaNodes, this.accountsSchemaNodeMap);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses schema equation nodes.
|
||||
* @param {(IProfitLossSheetNode | IProfitLossSchemaNode)[]} nodes
|
||||
* @returns {(IProfitLossSheetNode | IProfitLossSchemaNode)[]}
|
||||
*/
|
||||
private reportSchemaEquationNodesCompose = (
|
||||
nodes: (IProfitLossSheetNode | IProfitLossSchemaNode)[]
|
||||
): (IProfitLossSheetNode | IProfitLossSchemaNode)[] => {
|
||||
return this.mapAccNodesDeep(nodes, this.reportSchemaEquationNodeCompose);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve profit/loss report data.
|
||||
* @return {IProfitLossSheetStatement}
|
||||
*/
|
||||
public reportData = (): IProfitLossSheetNode => {
|
||||
const schema = this.getSchema();
|
||||
|
||||
return R.compose(
|
||||
this.reportFilterPlugin,
|
||||
this.reportRowsPercentageCompose,
|
||||
this.reportColumnsPerentageCompose,
|
||||
this.reportSchemaEquationNodesCompose,
|
||||
this.reportSchemaAccountsNodesCompose
|
||||
)(schema);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as R from 'ramda';
|
||||
import { TOTAL_NODE_TYPES } from './constants';
|
||||
|
||||
export const ProfitLossSheetBase = (Base) =>
|
||||
class extends Base {
|
||||
/**
|
||||
*
|
||||
* @param type
|
||||
* @param node
|
||||
* @returns
|
||||
*/
|
||||
public isNodeType = R.curry((type: string, node) => {
|
||||
return node.nodeType === type;
|
||||
});
|
||||
|
||||
protected isNodeTypeIn = R.curry((types: string[], node) => {
|
||||
return types.indexOf(node.nodeType) !== -1;
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
protected findNodeById = R.curry((id, nodes) => {
|
||||
return this.findNodeDeep(nodes, (node) => node.id === id);
|
||||
});
|
||||
|
||||
isNodeTotal = (node) => {
|
||||
return this.isNodeTypeIn(TOTAL_NODE_TYPES, node);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,236 @@
|
||||
import * as R from 'ramda';
|
||||
import { sumBy } from 'lodash';
|
||||
import { FinancialDatePeriods } from '../FinancialDatePeriods';
|
||||
import {
|
||||
IDateRange,
|
||||
IProfitLossHorizontalDatePeriodNode,
|
||||
IProfitLossSheetAccountNode,
|
||||
IProfitLossSheetAccountsNode,
|
||||
IProfitLossSheetCommonNode,
|
||||
IProfitLossSheetNode,
|
||||
} from '@/interfaces';
|
||||
|
||||
export const ProfitLossSheetDatePeriods = (Base) =>
|
||||
class extends R.compose(FinancialDatePeriods)(Base) {
|
||||
/**
|
||||
* Retrieves the date periods based on the report query.
|
||||
* @returns {IDateRange[]}
|
||||
*/
|
||||
get datePeriods(): IDateRange[] {
|
||||
return this.getDateRanges(
|
||||
this.query.fromDate,
|
||||
this.query.toDate,
|
||||
this.query.displayColumnsBy
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the date periods of the given node based on the report query.
|
||||
* @param {IProfitLossSheetCommonNode} node
|
||||
* @param {Function} callback
|
||||
* @returns {}
|
||||
*/
|
||||
protected getReportNodeDatePeriods = (
|
||||
node: IProfitLossSheetCommonNode,
|
||||
callback: (
|
||||
node: IProfitLossSheetCommonNode,
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
index: number
|
||||
) => any
|
||||
) => {
|
||||
return this.getNodeDatePeriods(
|
||||
this.query.fromDate,
|
||||
this.query.toDate,
|
||||
this.query.displayColumnsBy,
|
||||
node,
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
// --------------------------
|
||||
// # Account Nodes.
|
||||
// --------------------------
|
||||
/**
|
||||
* Retrieve account node date period total.
|
||||
* @param {IProfitLossSheetAccount} node
|
||||
* @param {Date} fromDate
|
||||
* @param {Date} toDate
|
||||
* @returns {}
|
||||
*/
|
||||
private getAccountNodeDatePeriodTotal = (
|
||||
node: IProfitLossSheetAccountNode,
|
||||
fromDate: Date,
|
||||
toDate: Date
|
||||
) => {
|
||||
const periodTotal = this.repository.periodsAccountsLedger
|
||||
.whereAccountId(node.id)
|
||||
.whereFromDate(fromDate)
|
||||
.whereToDate(toDate)
|
||||
.getClosingBalance();
|
||||
|
||||
return this.getDatePeriodTotalMeta(periodTotal, fromDate, toDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve account node date period.
|
||||
* @param {IProfitLossSheetAccountNode} node
|
||||
* @returns {IProfitLossSheetAccountNode}
|
||||
*/
|
||||
public getAccountNodeDatePeriod = (node: IProfitLossSheetAccountNode) => {
|
||||
return this.getReportNodeDatePeriods(
|
||||
node,
|
||||
this.getAccountNodeDatePeriodTotal
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Account date periods to the given account node.
|
||||
* @param {IProfitLossSheetAccountNode} node
|
||||
* @returns {IProfitLossSheetAccountNode}
|
||||
*/
|
||||
public assocAccountNodeDatePeriod = (
|
||||
node: IProfitLossSheetAccountNode
|
||||
): IProfitLossSheetAccountNode => {
|
||||
const datePeriods = this.getAccountNodeDatePeriod(node);
|
||||
|
||||
return R.assoc('horizontalTotals', datePeriods, node);
|
||||
};
|
||||
|
||||
// --------------------------
|
||||
// # Aggregate nodes.
|
||||
// --------------------------
|
||||
/**
|
||||
* Retrieves sumation of the given aggregate node children totals.
|
||||
* @param {IProfitLossSheetAccountsNode} node
|
||||
* @param {number} index
|
||||
* @returns {number}
|
||||
*/
|
||||
private getAggregateDatePeriodIndexTotal = (
|
||||
node: IProfitLossSheetAccountsNode,
|
||||
index: number
|
||||
): number => {
|
||||
return sumBy(node.children, `horizontalTotals[${index}].total.amount`);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IProfitLossSheetAccount} node
|
||||
* @param {Date} fromDate
|
||||
* @param {Date} toDate
|
||||
* @param {number} index
|
||||
* @returns {IProfitLossSheetAccount}
|
||||
*/
|
||||
private getAggregateNodeDatePeriodTotal = R.curry(
|
||||
(
|
||||
node: IProfitLossSheetAccountsNode,
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
index: number
|
||||
): IProfitLossHorizontalDatePeriodNode => {
|
||||
const periodTotal = this.getAggregateDatePeriodIndexTotal(node, index);
|
||||
|
||||
return this.getDatePeriodTotalMeta(periodTotal, fromDate, toDate);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieves aggregate horizontal date periods.
|
||||
* @param {IProfitLossSheetAccountsNode} node
|
||||
* @returns {IProfitLossSheetAccountsNode}
|
||||
*/
|
||||
private getAggregateNodeDatePeriod = (
|
||||
node: IProfitLossSheetAccountsNode
|
||||
): IProfitLossHorizontalDatePeriodNode[] => {
|
||||
return this.getReportNodeDatePeriods(
|
||||
node,
|
||||
this.getAggregateNodeDatePeriodTotal
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Assoc horizontal date periods to aggregate node.
|
||||
* @param {IProfitLossSheetAccountsNode} node
|
||||
* @returns {IProfitLossSheetAccountsNode}
|
||||
*/
|
||||
protected assocAggregateDatePeriod = (
|
||||
node: IProfitLossSheetAccountsNode
|
||||
): IProfitLossSheetAccountsNode => {
|
||||
const datePeriods = this.getAggregateNodeDatePeriod(node);
|
||||
|
||||
return R.assoc('horizontalTotals', datePeriods, node);
|
||||
};
|
||||
|
||||
// --------------------------
|
||||
// # Equation nodes.
|
||||
// --------------------------
|
||||
/**
|
||||
* Retrieves equation date period node.
|
||||
* @param {IProfitLossSheetNode[]} accNodes
|
||||
* @param {IProfitLossSheetNode} node
|
||||
* @param {Date} fromDate
|
||||
* @param {Date} toDate
|
||||
* @param {number} index
|
||||
* @returns {IProfitLossHorizontalDatePeriodNode}
|
||||
*/
|
||||
private getEquationNodeDatePeriod = R.curry(
|
||||
(
|
||||
accNodes: IProfitLossSheetNode[],
|
||||
equation: string,
|
||||
node: IProfitLossSheetNode,
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
index: number
|
||||
): IProfitLossHorizontalDatePeriodNode => {
|
||||
const tableNodes = this.getNodesTableForEvaluating(
|
||||
`horizontalTotals[${index}].total.amount`,
|
||||
accNodes
|
||||
);
|
||||
// Evaluate the given equation.
|
||||
const total = this.evaluateEquation(equation, tableNodes);
|
||||
|
||||
return this.getDatePeriodTotalMeta(total, fromDate, toDate);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieves the equation node date periods.
|
||||
* @param {IProfitLossSheetNode[]} node
|
||||
* @param {string} equation
|
||||
* @param {IProfitLossSheetNode} node
|
||||
* @returns {IProfitLossHorizontalDatePeriodNode[]}
|
||||
*/
|
||||
private getEquationNodeDatePeriods = R.curry(
|
||||
(
|
||||
accNodes: IProfitLossSheetNode[],
|
||||
equation: string,
|
||||
node: IProfitLossSheetNode
|
||||
): IProfitLossHorizontalDatePeriodNode[] => {
|
||||
return this.getReportNodeDatePeriods(
|
||||
node,
|
||||
this.getEquationNodeDatePeriod(accNodes, equation)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Assoc equation node date period.
|
||||
* @param {IProfitLossSheetNode[]}
|
||||
* @param {IProfitLossSheetNode} node
|
||||
* @returns {IProfitLossSheetNode}
|
||||
*/
|
||||
protected assocEquationNodeDatePeriod = R.curry(
|
||||
(
|
||||
accNodes: IProfitLossSheetNode[],
|
||||
equation: string,
|
||||
node: IProfitLossSheetNode
|
||||
): IProfitLossSheetNode => {
|
||||
const periods = this.getEquationNodeDatePeriods(
|
||||
accNodes,
|
||||
equation,
|
||||
node
|
||||
);
|
||||
return R.assoc('horizontalTotals', periods, node);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,170 @@
|
||||
import * as R from 'ramda';
|
||||
import { get } from 'lodash';
|
||||
import { IProfitLossSheetNode, ProfitLossNodeType } from '@/interfaces';
|
||||
import { FinancialFilter } from '../FinancialFilter';
|
||||
import { ProfitLossSheetBase } from './ProfitLossSheetBase';
|
||||
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
|
||||
|
||||
export const ProfitLossSheetFilter = (Base) =>
|
||||
class extends R.compose(FinancialFilter, ProfitLossSheetBase)(Base) {
|
||||
query: ProfitLossSheetQuery;
|
||||
|
||||
// ----------------
|
||||
// # Account.
|
||||
// ----------------
|
||||
/**
|
||||
* Filter report node detarmine.
|
||||
* @param {IProfitLossSheetNode} node - Balance sheet node.
|
||||
* @return {boolean}
|
||||
*/
|
||||
private accountNoneZeroNodesFilterDetarminer = (
|
||||
node: IProfitLossSheetNode
|
||||
): boolean => {
|
||||
return R.ifElse(
|
||||
this.isNodeType(ProfitLossNodeType.ACCOUNT),
|
||||
this.isNodeNoneZero,
|
||||
R.always(true)
|
||||
)(node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines account none-transactions node.
|
||||
* @param {IBalanceSheetDataNode} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private accountNoneTransFilterDetarminer = (
|
||||
node: IProfitLossSheetNode
|
||||
): boolean => {
|
||||
return R.ifElse(
|
||||
this.isNodeType(ProfitLossNodeType.ACCOUNT),
|
||||
this.isNodeNoneZero,
|
||||
R.always(true)
|
||||
)(node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Report nodes filter.
|
||||
* @param {IProfitLossSheetNode[]} nodes -
|
||||
* @return {IProfitLossSheetNode[]}
|
||||
*/
|
||||
private accountsNoneZeroNodesFilter = (
|
||||
nodes: IProfitLossSheetNode[]
|
||||
): IProfitLossSheetNode[] => {
|
||||
return this.filterNodesDeep(
|
||||
nodes,
|
||||
this.accountNoneZeroNodesFilterDetarminer
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters the accounts none-transactions nodes.
|
||||
* @param {IProfitLossSheetNode[]} nodes
|
||||
* @returns {IProfitLossSheetNode[]}
|
||||
*/
|
||||
private accountsNoneTransactionsNodesFilter = (
|
||||
nodes: IProfitLossSheetNode[]
|
||||
) => {
|
||||
return this.filterNodesDeep(nodes, this.accountNoneTransFilterDetarminer);
|
||||
};
|
||||
|
||||
// ----------------
|
||||
// # Aggregate.
|
||||
// ----------------
|
||||
/**
|
||||
* Detearmines aggregate none-children filtering.
|
||||
* @param {IProfitLossSheetNode} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private aggregateNoneChildrenFilterDetarminer = (
|
||||
node: IProfitLossSheetNode
|
||||
): boolean => {
|
||||
const schemaNode = this.getSchemaNodeById(node.id);
|
||||
|
||||
// Detarmines whether the given node is aggregate node.
|
||||
const isAggregateNode = this.isNodeType(
|
||||
ProfitLossNodeType.ACCOUNTS,
|
||||
node
|
||||
);
|
||||
// Detarmines if the schema node is always should show.
|
||||
const isSchemaAlwaysShow = get(schemaNode, 'alwaysShow', false);
|
||||
|
||||
// Should node has children if aggregate node or not always show.
|
||||
return isAggregateNode && !isSchemaAlwaysShow
|
||||
? this.isNodeHasChildren(node)
|
||||
: true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters aggregate none-children nodes.
|
||||
* @param {IProfitLossSheetNode[]} nodes
|
||||
* @returns {IProfitLossSheetNode[]}
|
||||
*/
|
||||
private aggregateNoneChildrenFilter = (
|
||||
nodes: IProfitLossSheetNode[]
|
||||
): IProfitLossSheetNode[] => {
|
||||
return this.filterNodesDeep2(
|
||||
this.aggregateNoneChildrenFilterDetarminer,
|
||||
nodes
|
||||
);
|
||||
};
|
||||
|
||||
// ----------------
|
||||
// # Composers.
|
||||
// ----------------
|
||||
/**
|
||||
* Filters none-zero nodes.
|
||||
* @param {IProfitLossSheetNode[]} nodes
|
||||
* @returns {IProfitLossSheetNode[]}
|
||||
*/
|
||||
private filterNoneZeroNodesCompose = (
|
||||
nodes: IProfitLossSheetNode[]
|
||||
): IProfitLossSheetNode[] => {
|
||||
return R.compose(
|
||||
this.aggregateNoneChildrenFilter,
|
||||
this.accountsNoneZeroNodesFilter
|
||||
)(nodes);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters none-transactions nodes.
|
||||
* @param {IProfitLossSheetNode[]} nodes
|
||||
* @returns {IProfitLossSheetNode[]}
|
||||
*/
|
||||
private filterNoneTransNodesCompose = (
|
||||
nodes: IProfitLossSheetNode[]
|
||||
): IProfitLossSheetNode[] => {
|
||||
return R.compose(
|
||||
this.aggregateNoneChildrenFilter,
|
||||
this.accountsNoneTransactionsNodesFilter
|
||||
)(nodes);
|
||||
};
|
||||
|
||||
/**
|
||||
* Supress nodes when total accounts range transactions is empty.
|
||||
* @param {IProfitLossSheetNode[]} nodes
|
||||
* @returns {IProfitLossSheetNode[]}
|
||||
*/
|
||||
private supressNodesWhenRangeTransactionsEmpty = (
|
||||
nodes: IProfitLossSheetNode[]
|
||||
) => {
|
||||
return this.repository.totalAccountsLedger.isEmpty() ? [] : nodes;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compose report nodes filtering.
|
||||
* @param {IProfitLossSheetNode[]} nodes
|
||||
* @returns {IProfitLossSheetNode[]}
|
||||
*/
|
||||
protected reportFilterPlugin = (
|
||||
nodes: IProfitLossSheetNode[]
|
||||
): IProfitLossSheetNode[] => {
|
||||
return R.compose(
|
||||
this.supressNodesWhenRangeTransactionsEmpty,
|
||||
R.when(() => this.query.noneZero, this.filterNoneZeroNodesCompose),
|
||||
R.when(
|
||||
() => this.query.noneTransactions,
|
||||
this.filterNoneTransNodesCompose
|
||||
)
|
||||
)(nodes);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,301 @@
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
IProfitLossSheetNode,
|
||||
IProfitLossSheetTotal,
|
||||
ProfitLossAggregateNodeId,
|
||||
} from '@/interfaces';
|
||||
import { FinancialHorizTotals } from '../FinancialHorizTotals';
|
||||
|
||||
export const ProfitLossSheetPercentage = (Base) =>
|
||||
class extends R.compose(FinancialHorizTotals)(Base) {
|
||||
/**
|
||||
* Assoc column of percentage attribute to the given node.
|
||||
* @param {IProfitLossSheetNode} netIncomeNode -
|
||||
* @param {IProfitLossSheetNode} node -
|
||||
* @return {IProfitLossSheetNode}
|
||||
*/
|
||||
private assocColumnPercentage = R.curry(
|
||||
(
|
||||
propertyPath: string,
|
||||
parentNode: IProfitLossSheetNode,
|
||||
node: IProfitLossSheetNode
|
||||
) => {
|
||||
const percentage = this.getPercentageBasis(
|
||||
parentNode.total.amount,
|
||||
node.total.amount
|
||||
);
|
||||
return R.assoc(
|
||||
propertyPath,
|
||||
this.getPercentageAmountMeta(percentage),
|
||||
node
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Assoc column of percentage attribute to the given node.
|
||||
* @param {IProfitLossSheetNode} netIncomeNode -
|
||||
* @param {IProfitLossSheetNode} node -
|
||||
* @return {IProfitLossSheetNode}
|
||||
*/
|
||||
private assocColumnTotalPercentage = R.curry(
|
||||
(
|
||||
propertyPath: string,
|
||||
parentNode: IProfitLossSheetNode,
|
||||
node: IProfitLossSheetNode
|
||||
) => {
|
||||
const percentage = this.getPercentageBasis(
|
||||
parentNode.total.amount,
|
||||
node.total.amount
|
||||
);
|
||||
return R.assoc(
|
||||
propertyPath,
|
||||
this.getPercentageTotalAmountMeta(percentage),
|
||||
node
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Compose percentage of columns.
|
||||
* @param {IProfitLossSheetNode[]} nodes
|
||||
* @returns {IProfitLossSheetNode[]}
|
||||
*/
|
||||
private columnPercentageCompose = (
|
||||
nodes: IProfitLossSheetNode[]
|
||||
): IProfitLossSheetNode[] => {
|
||||
const netIncomeNode = this.findNodeById(
|
||||
ProfitLossAggregateNodeId.NET_INCOME,
|
||||
nodes
|
||||
);
|
||||
return this.mapNodesDeep(
|
||||
nodes,
|
||||
this.columnPercentageMapper(netIncomeNode)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compose percentage of income.
|
||||
* @param {IProfitLossSheetNode[]} nodes
|
||||
* @returns {IProfitLossSheetNode[]}
|
||||
*/
|
||||
private incomePercetageCompose = (
|
||||
nodes: IProfitLossSheetNode[]
|
||||
): IProfitLossSheetNode[] => {
|
||||
const incomeNode = this.findNodeById(
|
||||
ProfitLossAggregateNodeId.INCOME,
|
||||
nodes
|
||||
);
|
||||
return this.mapNodesDeep(nodes, this.incomePercentageMapper(incomeNode));
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IProfitLossSheetNode[]} nodes
|
||||
* @returns {IProfitLossSheetNode[]}
|
||||
*/
|
||||
private rowPercentageCompose = (
|
||||
nodes: IProfitLossSheetNode[]
|
||||
): IProfitLossSheetNode[] => {
|
||||
return this.mapNodesDeep(nodes, this.rowPercentageMap);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IProfitLossSheetNode} netIncomeNode -
|
||||
* @param {IProfitLossSheetNode} node -
|
||||
* @return {IProfitLossSheetNode}
|
||||
*/
|
||||
private columnPercentageMapper = R.curry(
|
||||
(netIncomeNode: IProfitLossSheetNode, node: IProfitLossSheetNode) => {
|
||||
const path = 'percentageColumn';
|
||||
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.isNodeHasHorizTotals,
|
||||
this.assocColumnPercentageHorizTotals(netIncomeNode)
|
||||
),
|
||||
R.ifElse(
|
||||
this.isNodeTotal,
|
||||
this.assocColumnTotalPercentage(path, netIncomeNode),
|
||||
this.assocColumnPercentage(path, netIncomeNode)
|
||||
)
|
||||
)(node);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IProfitLossSheetNode} node
|
||||
* @returns {IProfitLossSheetNode}
|
||||
*/
|
||||
private rowPercentageMap = (
|
||||
node: IProfitLossSheetNode
|
||||
): IProfitLossSheetNode => {
|
||||
const path = 'percentageRow';
|
||||
|
||||
return R.compose(
|
||||
R.when(this.isNodeHasHorizTotals, this.assocRowPercentageHorizTotals),
|
||||
R.ifElse(
|
||||
this.isNodeTotal,
|
||||
this.assocColumnTotalPercentage(path, node),
|
||||
this.assocColumnPercentage(path, node)
|
||||
)
|
||||
)(node);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IProfitLossSheetNode} incomeNode -
|
||||
* @param {IProfitLossSheetNode} node -
|
||||
* @returns {IProfitLossSheetNode}
|
||||
*/
|
||||
private incomePercentageMapper = R.curry(
|
||||
(incomeNode: IProfitLossSheetNode, node: IProfitLossSheetNode) => {
|
||||
const path = 'percentageIncome';
|
||||
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.isNodeHasHorizTotals,
|
||||
this.assocIncomePercentageHorizTotals(incomeNode)
|
||||
),
|
||||
R.ifElse(
|
||||
this.isNodeTotal,
|
||||
this.assocColumnTotalPercentage(path, incomeNode),
|
||||
this.assocColumnPercentage(path, incomeNode)
|
||||
)
|
||||
)(node);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IProfitLossSheetNode} expenseNode -
|
||||
* @param {IProfitLossSheetNode} node -
|
||||
*/
|
||||
private expensePercentageMapper = R.curry(
|
||||
(expenseNode: IProfitLossSheetNode, node: IProfitLossSheetNode) => {
|
||||
const path = 'percentageExpense';
|
||||
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.isNodeHasHorizTotals,
|
||||
this.assocExpensePercentageHorizTotals(expenseNode)
|
||||
),
|
||||
R.ifElse(
|
||||
this.isNodeTotal,
|
||||
this.assocColumnTotalPercentage(path, expenseNode),
|
||||
this.assocColumnPercentage(path, expenseNode)
|
||||
)
|
||||
)(node);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Compose percentage of expense.
|
||||
* @param {IProfitLossSheetNode[]} nodes
|
||||
* @returns {IProfitLossSheetNode[]}
|
||||
*/
|
||||
private expensesPercentageCompose = (
|
||||
nodes: IProfitLossSheetNode[]
|
||||
): IProfitLossSheetNode[] => {
|
||||
const expenseNode = this.findNodeById(
|
||||
ProfitLossAggregateNodeId.EXPENSES,
|
||||
nodes
|
||||
);
|
||||
return this.mapNodesDeep(
|
||||
nodes,
|
||||
this.expensePercentageMapper(expenseNode)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compose percentage attributes.
|
||||
* @param {IProfitLossSheetNode[]} nodes
|
||||
* @returns {IProfitLossSheetNode[]}
|
||||
*/
|
||||
protected reportColumnsPerentageCompose = (
|
||||
nodes: IProfitLossSheetNode[]
|
||||
): IProfitLossSheetNode[] => {
|
||||
return R.compose(
|
||||
R.when(this.query.isIncomePercentage, this.incomePercetageCompose),
|
||||
R.when(this.query.isColumnPercentage, this.columnPercentageCompose),
|
||||
R.when(this.query.isExpensesPercentage, this.expensesPercentageCompose),
|
||||
R.when(this.query.isRowPercentage, this.rowPercentageCompose)
|
||||
)(nodes);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {} nodes
|
||||
* @returns {}
|
||||
*/
|
||||
protected reportRowsPercentageCompose = (nodes) => {
|
||||
return nodes;
|
||||
};
|
||||
|
||||
// ----------------------------------
|
||||
// # Horizontal Nodes
|
||||
// ----------------------------------
|
||||
/**
|
||||
* Assoc incomer percentage to horizontal totals nodes.
|
||||
* @param {IProfitLossSheetNode} incomeNode -
|
||||
* @param {IProfitLossSheetNode} node -
|
||||
* @returns {IProfitLossSheetNode}
|
||||
*/
|
||||
private assocIncomePercentageHorizTotals = R.curry(
|
||||
(incomeNode: IProfitLossSheetNode, node: IProfitLossSheetNode) => {
|
||||
const horTotalsWithIncomePerc = this.assocPercentageHorizTotals(
|
||||
'percentageIncome',
|
||||
incomeNode,
|
||||
node
|
||||
);
|
||||
return R.assoc('horizontalTotals', horTotalsWithIncomePerc, node);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Assoc expense percentage to horizontal totals nodes.
|
||||
* @param {IProfitLossSheetNode} expenseNode -
|
||||
* @param {IProfitLossSheetNode} node -
|
||||
* @returns {IProfitLossSheetNode}
|
||||
*/
|
||||
private assocExpensePercentageHorizTotals = R.curry(
|
||||
(expenseNode: IProfitLossSheetNode, node: IProfitLossSheetNode) => {
|
||||
const horTotalsWithExpensePerc = this.assocPercentageHorizTotals(
|
||||
'percentageExpense',
|
||||
expenseNode,
|
||||
node
|
||||
);
|
||||
return R.assoc('horizontalTotals', horTotalsWithExpensePerc, node);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Assoc net income percentage to horizontal totals nodes.
|
||||
* @param {IProfitLossSheetNode} expenseNode -
|
||||
* @param {IProfitLossSheetNode} node -
|
||||
* @returns {IProfitLossSheetNode}
|
||||
*/
|
||||
private assocColumnPercentageHorizTotals = R.curry(
|
||||
(netIncomeNode: IProfitLossSheetNode, node: IProfitLossSheetNode) => {
|
||||
const horTotalsWithExpensePerc = this.assocPercentageHorizTotals(
|
||||
'percentageColumn',
|
||||
netIncomeNode,
|
||||
node
|
||||
);
|
||||
return R.assoc('horizontalTotals', horTotalsWithExpensePerc, node);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private assocRowPercentageHorizTotals = R.curry((node) => {
|
||||
const horTotalsWithExpensePerc = this.assocHorizontalPercentageTotals(
|
||||
'percentageRow',
|
||||
node
|
||||
);
|
||||
return R.assoc('horizontalTotals', horTotalsWithExpensePerc, node);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,395 @@
|
||||
import * as R from 'ramda';
|
||||
import { sumBy } from 'lodash';
|
||||
import {
|
||||
IProfitLossHorizontalDatePeriodNode,
|
||||
IProfitLossSchemaNode,
|
||||
IProfitLossSheetAccountNode,
|
||||
IProfitLossSheetAccountsNode,
|
||||
IProfitLossSheetEquationNode,
|
||||
IProfitLossSheetNode,
|
||||
} from '@/interfaces';
|
||||
import { FinancialPreviousPeriod } from '../FinancialPreviousPeriod';
|
||||
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
|
||||
|
||||
export const ProfitLossSheetPreviousPeriod = (Base) =>
|
||||
class extends R.compose(FinancialPreviousPeriod)(Base) {
|
||||
query: ProfitLossSheetQuery;
|
||||
|
||||
// ---------------------------
|
||||
// # Account
|
||||
// ---------------------------
|
||||
/**
|
||||
* Assoc previous period change attribute to account node.
|
||||
* @param {IProfitLossSheetAccountNode} accountNode
|
||||
* @returns {IProfitLossSheetAccountNode}
|
||||
*/
|
||||
protected assocPreviousPeriodTotalAccountNode = (
|
||||
node: IProfitLossSheetAccountNode
|
||||
): IProfitLossSheetAccountNode => {
|
||||
const total = this.repository.PPTotalAccountsLedger.whereAccountId(
|
||||
node.id
|
||||
).getClosingBalance();
|
||||
|
||||
return R.assoc('previousPeriod', this.getAmountMeta(total), node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compose previous period account node.
|
||||
* @param {IProfitLossSheetAccountNode} accountNode
|
||||
* @returns {IProfitLossSheetAccountNode}
|
||||
*/
|
||||
protected previousPeriodAccountNodeCompose = (
|
||||
accountNode: IProfitLossSheetAccountNode
|
||||
): IProfitLossSheetAccountNode => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.isNodeHasHorizTotals,
|
||||
this.assocPreviousPeriodAccountHorizNodeCompose
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodPercentageActive,
|
||||
this.assocPreviousPeriodPercentageNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodChangeActive,
|
||||
this.assocPreviousPeriodChangeNode
|
||||
),
|
||||
this.assocPreviousPeriodTotalAccountNode
|
||||
)(accountNode);
|
||||
};
|
||||
|
||||
// ---------------------------
|
||||
// # Aggregate
|
||||
// ---------------------------
|
||||
/**
|
||||
* Assoc previous period total attribute to aggregate node.
|
||||
* @param {IProfitLossSheetAccountNode} accountNode
|
||||
* @returns {IProfitLossSheetAccountNode}
|
||||
*/
|
||||
private assocPreviousPeriodTotalAggregateNode = (
|
||||
node: IProfitLossSheetAccountNode
|
||||
) => {
|
||||
const total = sumBy(node.children, 'previousPeriod.amount');
|
||||
|
||||
return R.assoc('previousPeriod', this.getTotalAmountMeta(total), node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compose previous period to aggregate node.
|
||||
* @param {IProfitLossSheetAccountNode} accountNode
|
||||
* @returns {IProfitLossSheetAccountNode}
|
||||
*/
|
||||
protected previousPeriodAggregateNodeCompose = (
|
||||
accountNode: IProfitLossSheetAccountNode
|
||||
): IProfitLossSheetAccountNode => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.isNodeHasHorizTotals,
|
||||
this.assocPreviousPeriodAggregateHorizNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodPercentageActive,
|
||||
this.assocPreviousPeriodTotalPercentageNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodChangeActive,
|
||||
this.assocPreviousPeriodTotalChangeNode
|
||||
),
|
||||
this.assocPreviousPeriodTotalAggregateNode
|
||||
)(accountNode);
|
||||
};
|
||||
|
||||
// ---------------------------
|
||||
// # Equation
|
||||
// --------------------------
|
||||
/**
|
||||
*
|
||||
* @param {(IProfitLossSchemaNode | IProfitLossSheetNode)[]} accNodes
|
||||
* @param {string} equation
|
||||
* @param {IProfitLossSheetNode} node
|
||||
* @returns {IProfitLossSheetEquationNode}
|
||||
*/
|
||||
private assocPreviousPeriodTotalEquationNode = R.curry(
|
||||
(
|
||||
accNodes: (IProfitLossSchemaNode | IProfitLossSheetNode)[],
|
||||
equation: string,
|
||||
node: IProfitLossSheetEquationNode
|
||||
): IProfitLossSheetEquationNode => {
|
||||
const previousPeriodNodePath = 'previousPeriod.amount';
|
||||
const tableNodes = this.getNodesTableForEvaluating(
|
||||
previousPeriodNodePath,
|
||||
accNodes
|
||||
);
|
||||
// Evaluate the given equation.
|
||||
const total = this.evaluateEquation(equation, tableNodes);
|
||||
|
||||
return R.assoc('previousPeriod', this.getTotalAmountMeta(total), node);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {(IProfitLossSchemaNode | IProfitLossSheetNode)[]} accNodes -
|
||||
* @param {string} node
|
||||
* @param {IProfitLossSheetEquationNode} node
|
||||
* @returns {IProfitLossSheetEquationNode}
|
||||
*/
|
||||
protected previousPeriodEquationNodeCompose = R.curry(
|
||||
(
|
||||
accNodes: (IProfitLossSchemaNode | IProfitLossSheetNode)[],
|
||||
equation: string,
|
||||
node: IProfitLossSheetEquationNode
|
||||
): IProfitLossSheetEquationNode => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.isNodeHasHorizTotals,
|
||||
this.assocPreviousPeriodEquationHorizNode(accNodes, equation)
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodPercentageActive,
|
||||
this.assocPreviousPeriodTotalPercentageNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodChangeActive,
|
||||
this.assocPreviousPeriodTotalChangeNode
|
||||
),
|
||||
this.assocPreviousPeriodTotalEquationNode(accNodes, equation)
|
||||
)(node);
|
||||
}
|
||||
);
|
||||
|
||||
// ---------------------------
|
||||
// # Horizontal Nodes - Account
|
||||
// --------------------------
|
||||
/**
|
||||
* Assoc previous period to account horizontal node.
|
||||
* @param {IProfitLossSheetAccountNode} node
|
||||
* @param {IProfitLossHorizontalDatePeriodNode} totalNode
|
||||
* @returns {IProfitLossHorizontalDatePeriodNode}
|
||||
*/
|
||||
private assocPerviousPeriodAccountHorizTotal = R.curry(
|
||||
(
|
||||
node: IProfitLossSheetAccountNode,
|
||||
totalNode: IProfitLossHorizontalDatePeriodNode
|
||||
): IProfitLossHorizontalDatePeriodNode => {
|
||||
const total = this.repository.PPPeriodsAccountsLedger.whereAccountId(
|
||||
node.id
|
||||
)
|
||||
.whereFromDate(totalNode.previousPeriodFromDate.date)
|
||||
.whereToDate(totalNode.previousPeriodToDate.date)
|
||||
.getClosingBalance();
|
||||
|
||||
return R.assoc('previousPeriod', this.getAmountMeta(total), totalNode);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @param {IProfitLossSheetAccountNode} node
|
||||
* @param {IProfitLossSheetTotal}
|
||||
*/
|
||||
private previousPeriodAccountHorizNodeCompose = R.curry(
|
||||
(
|
||||
node: IProfitLossSheetAccountNode,
|
||||
horizontalTotalNode: IProfitLossHorizontalDatePeriodNode,
|
||||
index: number
|
||||
): IProfitLossHorizontalDatePeriodNode => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.query.isPreviousPeriodPercentageActive,
|
||||
this.assocPreviousPeriodPercentageNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodChangeActive,
|
||||
this.assocPreviousPeriodChangeNode
|
||||
),
|
||||
this.assocPerviousPeriodAccountHorizTotal(node),
|
||||
this.assocPreviousPeriodHorizNodeFromToDates(
|
||||
this.query.displayColumnsBy
|
||||
)
|
||||
)(horizontalTotalNode);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IProfitLossSheetAccountNode} node
|
||||
* @returns {IProfitLossSheetAccountNode}
|
||||
*/
|
||||
private assocPreviousPeriodAccountHorizNodeCompose = (
|
||||
node: IProfitLossSheetAccountNode
|
||||
): IProfitLossSheetAccountNode => {
|
||||
const horizontalTotals = R.addIndex(R.map)(
|
||||
this.previousPeriodAccountHorizNodeCompose(node),
|
||||
node.horizontalTotals
|
||||
);
|
||||
return R.assoc('horizontalTotals', horizontalTotals, node);
|
||||
};
|
||||
|
||||
// ----------------------------------
|
||||
// # Horizontal Nodes - Aggregate
|
||||
// ----------------------------------
|
||||
/**
|
||||
* Assoc previous period total to aggregate horizontal nodes.
|
||||
* @param {IProfitLossSheetAccountsNode} node
|
||||
* @param {number} index
|
||||
* @param {any} totalNode
|
||||
* @return {}
|
||||
*/
|
||||
private assocPreviousPeriodAggregateHorizTotal = R.curry(
|
||||
(
|
||||
node: IProfitLossSheetAccountsNode,
|
||||
index: number,
|
||||
totalNode: IProfitLossHorizontalDatePeriodNode
|
||||
) => {
|
||||
const total = this.getPPHorizNodesTotalSumation(index, node);
|
||||
|
||||
return R.assoc(
|
||||
'previousPeriod',
|
||||
this.getTotalAmountMeta(total),
|
||||
totalNode
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IProfitLossSheetAccountsNode} node
|
||||
* @param {IProfitLossHorizontalDatePeriodNode} horizontalTotalNode -
|
||||
* @param {number} index
|
||||
* @returns {IProfitLossHorizontalDatePeriodNode}
|
||||
*/
|
||||
private previousPeriodAggregateHorizNodeCompose = R.curry(
|
||||
(
|
||||
node: IProfitLossSheetAccountsNode,
|
||||
horizontalTotalNode: IProfitLossHorizontalDatePeriodNode,
|
||||
index: number
|
||||
): IProfitLossHorizontalDatePeriodNode => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.query.isPreviousPeriodPercentageActive,
|
||||
this.assocPreviousPeriodTotalPercentageNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodChangeActive,
|
||||
this.assocPreviousPeriodTotalChangeNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodActive,
|
||||
this.assocPreviousPeriodAggregateHorizTotal(node, index)
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodActive,
|
||||
this.assocPreviousPeriodHorizNodeFromToDates(
|
||||
this.query.displayColumnsBy
|
||||
)
|
||||
)
|
||||
)(horizontalTotalNode);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Assoc previous period to aggregate horizontal nodes.
|
||||
* @param {IProfitLossSheetAccountsNode} node
|
||||
* @returns
|
||||
*/
|
||||
private assocPreviousPeriodAggregateHorizNode = (
|
||||
node: IProfitLossSheetAccountsNode
|
||||
): IProfitLossSheetAccountsNode => {
|
||||
const horizontalTotals = R.addIndex(R.map)(
|
||||
this.previousPeriodAggregateHorizNodeCompose(node),
|
||||
node.horizontalTotals
|
||||
);
|
||||
return R.assoc('horizontalTotals', horizontalTotals, node);
|
||||
};
|
||||
|
||||
// ----------------------------------
|
||||
// # Horizontal Nodes - Equation
|
||||
// ----------------------------------
|
||||
/**
|
||||
*
|
||||
* @param {IProfitLossSheetNode[]} accNodes -
|
||||
* @param {string} equation
|
||||
* @param {index} number
|
||||
* @param {} totalNode
|
||||
*/
|
||||
private assocPreviousPeriodEquationHorizTotal = R.curry(
|
||||
(
|
||||
accNodes: IProfitLossSheetNode[],
|
||||
equation: string,
|
||||
index: number,
|
||||
totalNode
|
||||
): IProfitLossSheetNode => {
|
||||
const scopes = this.getNodesTableForEvaluating(
|
||||
`horizontalTotals[${index}].previousPeriod.amount`,
|
||||
accNodes
|
||||
);
|
||||
const total = this.evaluateEquation(equation, scopes);
|
||||
|
||||
return R.assoc(
|
||||
'previousPeriod',
|
||||
this.getTotalAmountMeta(total),
|
||||
totalNode
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IProfitLossSheetNode[]} accNodes -
|
||||
* @param {string} equation
|
||||
* @param {} horizontalTotalNode
|
||||
* @param {number} index
|
||||
*/
|
||||
private previousPeriodEquationHorizNodeCompose = R.curry(
|
||||
(
|
||||
accNodes: IProfitLossSheetNode[],
|
||||
equation: string,
|
||||
horizontalTotalNode,
|
||||
index: number
|
||||
) => {
|
||||
const assocHorizTotal = this.assocPreviousPeriodEquationHorizTotal(
|
||||
accNodes,
|
||||
equation,
|
||||
index
|
||||
);
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.query.isPreviousPeriodPercentageActive,
|
||||
this.assocPreviousPeriodTotalPercentageNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodChangeActive,
|
||||
this.assocPreviousPeriodTotalChangeNode
|
||||
),
|
||||
R.when(this.query.isPreviousPeriodActive, assocHorizTotal),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodActive,
|
||||
this.assocPreviousPeriodHorizNodeFromToDates(
|
||||
this.query.displayColumnsBy
|
||||
)
|
||||
)
|
||||
)(horizontalTotalNode);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Assoc previous period equation to horizontal nodes.
|
||||
* @parma {IProfitLossSheetNode[]} accNodes -
|
||||
* @param {string} equation
|
||||
* @param {IProfitLossSheetEquationNode} node
|
||||
* @return {IProfitLossSheetEquationNode}
|
||||
*/
|
||||
private assocPreviousPeriodEquationHorizNode = R.curry(
|
||||
(
|
||||
accNodes: IProfitLossSheetNode[],
|
||||
equation: string,
|
||||
node: IProfitLossSheetEquationNode
|
||||
): IProfitLossSheetEquationNode => {
|
||||
const horizontalTotals = R.addIndex(R.map)(
|
||||
this.previousPeriodEquationHorizNodeCompose(accNodes, equation),
|
||||
node.horizontalTotals
|
||||
);
|
||||
return R.assoc('horizontalTotals', horizontalTotals, node);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,367 @@
|
||||
import * as R from 'ramda';
|
||||
import { sumBy } from 'lodash';
|
||||
import { compose } from 'lodash/fp';
|
||||
import {
|
||||
IProfitLossSheetEquationNode,
|
||||
IProfitLossSheetAccountNode,
|
||||
IProfitLossSchemaNode,
|
||||
IProfitLossSheetNode,
|
||||
IProfitLossSheetTotal,
|
||||
} from '@/interfaces';
|
||||
import { ProfitLossSheetRepository } from './ProfitLossSheetRepository';
|
||||
import { FinancialPreviousYear } from '../FinancialPreviousYear';
|
||||
|
||||
export const ProfitLossSheetPreviousYear = (Base) =>
|
||||
class extends compose(FinancialPreviousYear)(Base) {
|
||||
repository: ProfitLossSheetRepository;
|
||||
|
||||
// ---------------------------
|
||||
// # Account
|
||||
// ---------------------------
|
||||
/**
|
||||
* Assoc previous year total attribute to account node.
|
||||
* @param {IProfitLossSheetAccountNode} accountNode
|
||||
* @returns {IProfitLossSheetAccountNode}
|
||||
*/
|
||||
private assocPreviousYearTotalAccountNode = (
|
||||
accountNode: IProfitLossSheetAccountNode
|
||||
) => {
|
||||
const total = this.repository.PYTotalAccountsLedger.whereAccountId(
|
||||
accountNode.id
|
||||
).getClosingBalance();
|
||||
|
||||
return R.assoc('previousYear', this.getAmountMeta(total), accountNode);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compose previous year account node.
|
||||
* @param {IProfitLossSheetAccountNode} accountNode
|
||||
* @returns {IProfitLossSheetAccountNode}
|
||||
*/
|
||||
protected previousYearAccountNodeCompose = (
|
||||
accountNode: IProfitLossSheetAccountNode
|
||||
): IProfitLossSheetAccountNode => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.isNodeHasHorizTotals,
|
||||
this.assocPreviousYearAccountHorizNodeCompose
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearPercentageActive,
|
||||
this.assocPreviousYearPercentageNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearChangeActive,
|
||||
this.assocPreviousYearChangetNode
|
||||
),
|
||||
this.assocPreviousYearTotalAccountNode
|
||||
)(accountNode);
|
||||
};
|
||||
|
||||
// ---------------------------
|
||||
// # Aggregate
|
||||
// ---------------------------
|
||||
/**
|
||||
* Assoc previous year change attribute to aggregate node.
|
||||
* @param {IProfitLossSheetAccountNode} accountNode
|
||||
* @returns {IProfitLossSheetAccountNode}
|
||||
*/
|
||||
private assocPreviousYearTotalAggregateNode = (
|
||||
node: IProfitLossSheetAccountNode
|
||||
): IProfitLossSheetAccountNode => {
|
||||
const total = sumBy(node.children, 'previousYear.amount');
|
||||
|
||||
return R.assoc('previousYear', this.getTotalAmountMeta(total), node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compose previous year to aggregate node.
|
||||
* @param {IProfitLossSheetAccountNode} accountNode
|
||||
* @returns {IProfitLossSheetAccountNode}
|
||||
*/
|
||||
protected previousYearAggregateNodeCompose = (
|
||||
accountNode: IProfitLossSheetAccountNode
|
||||
): IProfitLossSheetAccountNode => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.isNodeHasHorizTotals,
|
||||
this.assocPreviousYearAggregateHorizNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearPercentageActive,
|
||||
this.assocPreviousYearTotalPercentageNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearChangeActive,
|
||||
this.assocPreviousYearTotalChangeNode
|
||||
),
|
||||
this.assocPreviousYearTotalAggregateNode
|
||||
)(accountNode);
|
||||
};
|
||||
|
||||
// ---------------------------
|
||||
// # Equation
|
||||
// ---------------------------
|
||||
/**
|
||||
* Assoc previous year total to equation node.
|
||||
* @param {(IProfitLossSchemaNode | IProfitLossSheetNode)[]} accNodes
|
||||
* @param {string} equation
|
||||
* @param {IProfitLossSheetNode} node
|
||||
* @returns {IProfitLossSheetEquationNode}
|
||||
*/
|
||||
private assocPreviousYearTotalEquationNode = R.curry(
|
||||
(
|
||||
accNodes: (IProfitLossSchemaNode | IProfitLossSheetNode)[],
|
||||
equation: string,
|
||||
node: IProfitLossSheetNode
|
||||
) => {
|
||||
const previousPeriodNodePath = 'previousYear.amount';
|
||||
const tableNodes = this.getNodesTableForEvaluating(
|
||||
previousPeriodNodePath,
|
||||
accNodes
|
||||
);
|
||||
// Evaluate the given equation.
|
||||
const total = this.evaluateEquation(equation, tableNodes);
|
||||
|
||||
return R.assoc('previousYear', this.getTotalAmountMeta(total), node);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Previous year equation node.
|
||||
* @param {(IProfitLossSchemaNode | IProfitLossSheetNode)[]} accNodes -
|
||||
* @param {string} node
|
||||
* @param {IProfitLossSheetEquationNode} node
|
||||
* @returns {IProfitLossSheetEquationNode}
|
||||
*/
|
||||
protected previousYearEquationNodeCompose = R.curry(
|
||||
(
|
||||
accNodes: (IProfitLossSchemaNode | IProfitLossSheetNode)[],
|
||||
equation: string,
|
||||
node: IProfitLossSheetEquationNode
|
||||
) => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.isNodeHasHorizTotals,
|
||||
this.assocPreviousYearEquationHorizNode(accNodes, equation)
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearPercentageActive,
|
||||
this.assocPreviousYearTotalPercentageNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearChangeActive,
|
||||
this.assocPreviousYearTotalChangeNode
|
||||
),
|
||||
this.assocPreviousYearTotalEquationNode(accNodes, equation)
|
||||
)(node);
|
||||
}
|
||||
);
|
||||
|
||||
// ----------------------------------
|
||||
// # Horizontal Nodes - Account
|
||||
// ----------------------------------
|
||||
/**
|
||||
* Assoc preivous year to account horizontal total node.
|
||||
* @param {IProfitLossSheetAccountNode} node
|
||||
* @returns
|
||||
*/
|
||||
private assocPreviousYearAccountHorizTotal = R.curry(
|
||||
(node: IProfitLossSheetAccountNode, totalNode) => {
|
||||
const total = this.repository.PYPeriodsAccountsLedger.whereAccountId(
|
||||
node.id
|
||||
)
|
||||
.whereFromDate(totalNode.previousYearFromDate.date)
|
||||
.whereToDate(totalNode.previousYearToDate.date)
|
||||
.getClosingBalance();
|
||||
|
||||
return R.assoc('previousYear', this.getAmountMeta(total), totalNode);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Previous year account horizontal node composer.
|
||||
* @param {IProfitLossSheetAccountNode} horizontalTotalNode
|
||||
* @param {IProfitLossSheetTotal} horizontalTotalNode -
|
||||
* @returns {IProfitLossSheetTotal}
|
||||
*/
|
||||
private previousYearAccountHorizNodeCompose = R.curry(
|
||||
(
|
||||
node: IProfitLossSheetAccountNode,
|
||||
horizontalTotalNode: IProfitLossSheetTotal
|
||||
): IProfitLossSheetTotal => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.query.isPreviousYearPercentageActive,
|
||||
this.assocPreviousYearPercentageNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearChangeActive,
|
||||
this.assocPreviousYearChangetNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearActive,
|
||||
this.assocPreviousYearAccountHorizTotal(node)
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearActive,
|
||||
this.assocPreviousYearHorizNodeFromToDates
|
||||
)
|
||||
)(horizontalTotalNode);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IProfitLossSheetAccountNode} node
|
||||
* @returns {IProfitLossSheetAccountNode}
|
||||
*/
|
||||
private assocPreviousYearAccountHorizNodeCompose = (
|
||||
node: IProfitLossSheetAccountNode
|
||||
): IProfitLossSheetAccountNode => {
|
||||
const horizontalTotals = R.map(
|
||||
this.previousYearAccountHorizNodeCompose(node),
|
||||
node.horizontalTotals
|
||||
);
|
||||
return R.assoc('horizontalTotals', horizontalTotals, node);
|
||||
};
|
||||
|
||||
// ----------------------------------
|
||||
// # Horizontal Nodes - Aggregate
|
||||
// ----------------------------------
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private assocPreviousYearAggregateHorizTotal = R.curry(
|
||||
(node, index, totalNode) => {
|
||||
const total = this.getPYHorizNodesTotalSumation(index, node);
|
||||
|
||||
return R.assoc(
|
||||
'previousYear',
|
||||
this.getTotalAmountMeta(total),
|
||||
totalNode
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private previousYearAggregateHorizNodeCompose = R.curry(
|
||||
(node, horizontalTotalNode, index: number) => {
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.query.isPreviousYearPercentageActive,
|
||||
this.assocPreviousYearTotalPercentageNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearChangeActive,
|
||||
this.assocPreviousYearTotalChangeNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearActive,
|
||||
this.assocPreviousYearAggregateHorizTotal(node, index)
|
||||
)
|
||||
)(horizontalTotalNode);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IProfitLossSheetAccountNode} node
|
||||
* @returns {IProfitLossSheetAccountNode}
|
||||
*/
|
||||
private assocPreviousYearAggregateHorizNode = (
|
||||
node: IProfitLossSheetAccountNode
|
||||
): IProfitLossSheetAccountNode => {
|
||||
const horizontalTotals = R.addIndex(R.map)(
|
||||
this.previousYearAggregateHorizNodeCompose(node),
|
||||
node.horizontalTotals
|
||||
);
|
||||
return R.assoc('horizontalTotals', horizontalTotals, node);
|
||||
};
|
||||
|
||||
// ----------------------------------
|
||||
// # Horizontal Nodes - Equation
|
||||
// ----------------------------------
|
||||
/**
|
||||
*
|
||||
* @param {IProfitLossSheetNode[]} accNodes -
|
||||
* @param {string} equation
|
||||
* @param {number} index
|
||||
* @param {} totalNode -
|
||||
*/
|
||||
private assocPreviousYearEquationHorizTotal = R.curry(
|
||||
(
|
||||
accNodes: IProfitLossSheetNode[],
|
||||
equation: string,
|
||||
index: number,
|
||||
totalNode
|
||||
) => {
|
||||
const scopes = this.getNodesTableForEvaluating(
|
||||
`horizontalTotals[${index}].previousYear.amount`,
|
||||
accNodes
|
||||
);
|
||||
const total = this.evaluateEquation(equation, scopes);
|
||||
|
||||
return R.assoc(
|
||||
'previousYear',
|
||||
this.getTotalAmountMeta(total),
|
||||
totalNode
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IProfitLossSheetNode[]} accNodes -
|
||||
* @param {string} equation
|
||||
* @param {} horizontalTotalNode
|
||||
* @param {number} index
|
||||
*/
|
||||
private previousYearEquationHorizNodeCompose = R.curry(
|
||||
(
|
||||
accNodes: IProfitLossSheetNode[],
|
||||
equation: string,
|
||||
horizontalTotalNode,
|
||||
index: number
|
||||
) => {
|
||||
const assocHorizTotal = this.assocPreviousYearEquationHorizTotal(
|
||||
accNodes,
|
||||
equation,
|
||||
index
|
||||
);
|
||||
return R.compose(
|
||||
R.when(
|
||||
this.query.isPreviousYearPercentageActive,
|
||||
this.assocPreviousYearTotalPercentageNode
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearChangeActive,
|
||||
this.assocPreviousYearTotalChangeNode
|
||||
),
|
||||
R.when(this.query.isPreviousYearActive, assocHorizTotal)
|
||||
)(horizontalTotalNode);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IProfitLossSheetNode[]} accNodes
|
||||
* @param {string} equation
|
||||
* @param {IProfitLossSheetEquationNode} node
|
||||
*/
|
||||
private assocPreviousYearEquationHorizNode = R.curry(
|
||||
(
|
||||
accNodes: IProfitLossSheetNode[],
|
||||
equation: string,
|
||||
node: IProfitLossSheetEquationNode
|
||||
) => {
|
||||
const horizontalTotals = R.addIndex(R.map)(
|
||||
this.previousYearEquationHorizNodeCompose(accNodes, equation),
|
||||
node.horizontalTotals
|
||||
);
|
||||
return R.assoc('horizontalTotals', horizontalTotals, node);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,209 @@
|
||||
import { merge } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import { IProfitLossSheetQuery, IFinancialDatePeriodsUnit } from '@/interfaces';
|
||||
import { DISPLAY_COLUMNS_BY } from './constants';
|
||||
import { FinancialDateRanges } from '../FinancialDateRanges';
|
||||
|
||||
export class ProfitLossSheetQuery extends R.compose(FinancialDateRanges)(
|
||||
class {}
|
||||
) {
|
||||
/**
|
||||
* P&L query.
|
||||
* @param {IProfitLossSheetQuery}
|
||||
*/
|
||||
public readonly query: IProfitLossSheetQuery;
|
||||
|
||||
/**
|
||||
* Previous year to date.
|
||||
* @param {Date}
|
||||
*/
|
||||
public readonly PYToDate: Date;
|
||||
/**
|
||||
* Previous year from date.
|
||||
* @param {Date}
|
||||
*/
|
||||
public readonly PYFromDate: Date;
|
||||
/**
|
||||
* Previous period to date.
|
||||
* @param {Date}
|
||||
*/
|
||||
public readonly PPToDate: Date;
|
||||
/**
|
||||
* Previous period from date.
|
||||
* @param {Date}
|
||||
*/
|
||||
public readonly PPFromDate: Date;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {IProfitLossSheetQuery} query
|
||||
*/
|
||||
constructor(query: IProfitLossSheetQuery) {
|
||||
super();
|
||||
this.query = query;
|
||||
|
||||
// Pervious Year (PY) Dates.
|
||||
this.PYToDate = this.getPreviousYearDate(this.query.toDate);
|
||||
this.PYFromDate = this.getPreviousYearDate(this.query.fromDate);
|
||||
|
||||
// Previous Period (PP) Dates for total column..
|
||||
if (this.isTotalColumnType()) {
|
||||
const { fromDate, toDate } = this.getPPTotalDateRange(
|
||||
this.query.fromDate,
|
||||
this.query.toDate
|
||||
);
|
||||
this.PPToDate = toDate;
|
||||
this.PPFromDate = fromDate;
|
||||
|
||||
// Previous period (PP) dates for date periods columns type.
|
||||
} else if (this.isDatePeriodsColumnsType()) {
|
||||
const { fromDate, toDate } = this.getPPDatePeriodDateRange(
|
||||
this.query.fromDate,
|
||||
this.query.toDate,
|
||||
this.query.displayColumnsBy as IFinancialDatePeriodsUnit
|
||||
);
|
||||
this.PPToDate = toDate;
|
||||
this.PPFromDate = fromDate;
|
||||
}
|
||||
return merge(this, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines the given display columns type.
|
||||
* @param {string} displayColumnsBy
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isDisplayColumnsBy = (displayColumnsBy: string): boolean => {
|
||||
return this.query.displayColumnsBy === displayColumnsBy;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines the given display columns by type.
|
||||
* @param {string} displayColumnsBy
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isDisplayColumnsType = (displayColumnsType: string): boolean => {
|
||||
return this.query.displayColumnsType === displayColumnsType;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether the columns type is date periods.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isDatePeriodsColumnsType = (): boolean => {
|
||||
return this.isDisplayColumnsType(DISPLAY_COLUMNS_BY.DATE_PERIODS);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether the columns type is total.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isTotalColumnType = (): boolean => {
|
||||
return this.isDisplayColumnsType(DISPLAY_COLUMNS_BY.TOTAL);
|
||||
};
|
||||
|
||||
// --------------------------------------
|
||||
// # Previous Year (PY)
|
||||
// --------------------------------------
|
||||
/**
|
||||
* Detarmines the report query has previous year enabled.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isPreviousYearActive = (): boolean => {
|
||||
return this.query.previousYear;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines the report query has previous year percentage change active.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isPreviousYearPercentageActive = (): boolean => {
|
||||
return this.query.previousYearPercentageChange;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines the report query has previous year change active.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isPreviousYearChangeActive = (): boolean => {
|
||||
return this.query.previousYearAmountChange;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves PY date based on the current query.
|
||||
* @returns {Date}
|
||||
*/
|
||||
public getTotalPreviousYear = (): Date => {
|
||||
return this.PYFromDate;
|
||||
};
|
||||
|
||||
// --------------------------------------
|
||||
// # Previous Period (PP)
|
||||
// --------------------------------------
|
||||
/**
|
||||
* Detarmines the report query has previous period enabled.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isPreviousPeriodActive = (): boolean => {
|
||||
return this.query.previousPeriod;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines the report query has previous period percentage change active.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isPreviousPeriodPercentageActive = (): boolean => {
|
||||
return this.query.previousPeriodPercentageChange;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines the report query has previous period change active.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isPreviousPeriodChangeActive = (): boolean => {
|
||||
return this.query.previousPeriodAmountChange;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves previous period date based on the current query.
|
||||
* @returns {Date}
|
||||
*/
|
||||
public getTotalPreviousPeriod = (): Date => {
|
||||
return this.PPFromDate;
|
||||
};
|
||||
|
||||
// --------------------------------------
|
||||
// # Percentage vertical/horizontal.
|
||||
// --------------------------------------
|
||||
/**
|
||||
* Detarmines whether percentage of expenses is active.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isExpensesPercentage = (): boolean => {
|
||||
return this.query.percentageExpense;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether percentage of income is active.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isIncomePercentage = (): boolean => {
|
||||
return this.query.percentageIncome;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether percentage of column is active.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isColumnPercentage = (): boolean => {
|
||||
return this.query.percentageColumn;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether percentage of row is active.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isRowPercentage = (): boolean => {
|
||||
return this.query.percentageRow;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
import { defaultTo } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import { Knex } from 'knex';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { transformToMapBy } from 'utils';
|
||||
import {
|
||||
IProfitLossSheetQuery,
|
||||
IAccount,
|
||||
IAccountTransactionsGroupBy,
|
||||
} from '@/interfaces';
|
||||
import Ledger from '@/services/Accounting/Ledger';
|
||||
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
|
||||
import { FinancialDatePeriods } from '../FinancialDatePeriods';
|
||||
|
||||
export class ProfitLossSheetRepository extends R.compose(FinancialDatePeriods)(
|
||||
class {}
|
||||
) {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public models: any;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public accountsByType: any;
|
||||
|
||||
/**
|
||||
* @param {}
|
||||
*/
|
||||
public accounts: IAccount[];
|
||||
|
||||
/**
|
||||
* Transactions group type.
|
||||
* @param {IAccountTransactionsGroupBy}
|
||||
*/
|
||||
public transactionsGroupType: IAccountTransactionsGroupBy =
|
||||
IAccountTransactionsGroupBy.Month;
|
||||
|
||||
/**
|
||||
* @param {IProfitLossSheetQuery}
|
||||
*/
|
||||
public query: ProfitLossSheetQuery;
|
||||
|
||||
/**
|
||||
* Previous year to date.
|
||||
* @param {Date}
|
||||
*/
|
||||
public PYToDate: Date;
|
||||
|
||||
/**
|
||||
* Previous year from date.
|
||||
* @param {Date}
|
||||
*/
|
||||
public PYFromDate: Date;
|
||||
|
||||
/**
|
||||
* Previous year to date.
|
||||
* @param {Date}
|
||||
*/
|
||||
public PPToDate: Date;
|
||||
|
||||
/**
|
||||
* Previous year from date.
|
||||
* @param {Date}
|
||||
*/
|
||||
public PPFromDate: Date;
|
||||
|
||||
// ------------------------
|
||||
// # Total
|
||||
// ------------------------
|
||||
/**
|
||||
* Accounts total.
|
||||
* @param {Ledger}
|
||||
*/
|
||||
public totalAccountsLedger: Ledger;
|
||||
|
||||
// ------------------------
|
||||
// # Date Periods.
|
||||
// ------------------------
|
||||
/**
|
||||
* Accounts date periods.
|
||||
* @param {Ledger}
|
||||
*/
|
||||
public periodsAccountsLedger: Ledger;
|
||||
|
||||
// ------------------------
|
||||
// # Previous Year (PY)
|
||||
// ------------------------
|
||||
/**
|
||||
* @param {Ledger}
|
||||
*/
|
||||
public PYTotalAccountsLedger: Ledger;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Ledger}
|
||||
*/
|
||||
public PYPeriodsAccountsLedger: Ledger;
|
||||
|
||||
// ------------------------
|
||||
// # Previous Period (PP).
|
||||
// ------------------------
|
||||
/**
|
||||
* PP Accounts Periods.
|
||||
* @param {Ledger}
|
||||
*/
|
||||
public PPPeriodsAccountsLedger: Ledger;
|
||||
|
||||
/**
|
||||
* PP Accounts Total.
|
||||
* @param {Ledger}
|
||||
*/
|
||||
public PPTotalAccountsLedger: Ledger;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId
|
||||
* @param {IBalanceSheetQuery} query
|
||||
*/
|
||||
constructor(models: any, query: IProfitLossSheetQuery) {
|
||||
super();
|
||||
|
||||
this.models = models;
|
||||
this.query = new ProfitLossSheetQuery(query);
|
||||
|
||||
this.transactionsGroupType = this.getGroupByFromDisplayColumnsBy(
|
||||
this.query.displayColumnsBy
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Async report repository.
|
||||
*/
|
||||
public asyncInitialize = async () => {
|
||||
await this.initAccounts();
|
||||
await this.initAccountsTotalLedger();
|
||||
|
||||
// Date Periods.
|
||||
if (this.query.isDatePeriodsColumnsType()) {
|
||||
await this.initTotalDatePeriods();
|
||||
}
|
||||
// Previous Period (PP)
|
||||
if (this.query.isPreviousPeriodActive()) {
|
||||
await this.initTotalPreviousPeriod();
|
||||
}
|
||||
if (
|
||||
this.query.isPreviousPeriodActive() &&
|
||||
this.query.isDatePeriodsColumnsType()
|
||||
) {
|
||||
await this.initPeriodsPreviousPeriod();
|
||||
}
|
||||
// Previous Year (PY).
|
||||
if (this.query.isPreviousYearActive()) {
|
||||
await this.initTotalPreviousYear();
|
||||
}
|
||||
if (
|
||||
this.query.isPreviousYearActive() &&
|
||||
this.query.isDatePeriodsColumnsType()
|
||||
) {
|
||||
await this.initPeriodsPreviousYear();
|
||||
}
|
||||
};
|
||||
|
||||
// ----------------------------
|
||||
// # Accounts
|
||||
// ----------------------------
|
||||
/**
|
||||
* Initialize accounts of the report.
|
||||
*/
|
||||
private initAccounts = async () => {
|
||||
const accounts = await this.getAccounts();
|
||||
|
||||
// Inject to the repository.
|
||||
this.accounts = accounts;
|
||||
this.accountsByType = transformToMapBy(accounts, 'accountType');
|
||||
};
|
||||
|
||||
// ----------------------------
|
||||
// # Closing Total.
|
||||
// ----------------------------
|
||||
/**
|
||||
* Initialize accounts closing total based on the given query.
|
||||
*/
|
||||
private initAccountsTotalLedger = async (): Promise<void> => {
|
||||
const totalByAccount = await this.accountsTotal(
|
||||
this.query.fromDate,
|
||||
this.query.toDate
|
||||
);
|
||||
// Inject to the repository.
|
||||
this.totalAccountsLedger = Ledger.fromTransactions(totalByAccount);
|
||||
};
|
||||
|
||||
// ----------------------------
|
||||
// # Date periods.
|
||||
// ----------------------------
|
||||
/**
|
||||
* Initialize date periods total of accounts based on the given query.
|
||||
*/
|
||||
private initTotalDatePeriods = async (): Promise<void> => {
|
||||
// Retrieves grouped transactions by given date group.
|
||||
const periodsByAccount = await this.accountsDatePeriods(
|
||||
this.query.fromDate,
|
||||
this.query.toDate,
|
||||
this.transactionsGroupType
|
||||
);
|
||||
|
||||
// Inject to the repository.
|
||||
this.periodsAccountsLedger = Ledger.fromTransactions(periodsByAccount);
|
||||
};
|
||||
|
||||
// ----------------------------
|
||||
// # Previous Period (PP).
|
||||
// ----------------------------
|
||||
/**
|
||||
* Initialize total of previous period (PP).
|
||||
*/
|
||||
private initTotalPreviousPeriod = async (): Promise<void> => {
|
||||
const PPTotalsByAccounts = await this.accountsTotal(
|
||||
this.query.PPFromDate,
|
||||
this.query.PPToDate
|
||||
);
|
||||
// Inject to the repository.
|
||||
this.PPTotalAccountsLedger = Ledger.fromTransactions(PPTotalsByAccounts);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize date periods of previous period (PP).
|
||||
*/
|
||||
private initPeriodsPreviousPeriod = async (): Promise<void> => {
|
||||
// Retrieves grouped transactions by given date group.
|
||||
const periodsByAccount = await this.accountsDatePeriods(
|
||||
this.query.PPFromDate,
|
||||
this.query.PPToDate,
|
||||
this.transactionsGroupType
|
||||
);
|
||||
// Inject to the repository.
|
||||
this.PPPeriodsAccountsLedger = Ledger.fromTransactions(periodsByAccount);
|
||||
};
|
||||
|
||||
// ----------------------------
|
||||
// # Previous Year (PY).
|
||||
// ----------------------------
|
||||
/**
|
||||
* Initialize total of previous year (PY).
|
||||
*/
|
||||
private initTotalPreviousYear = async (): Promise<void> => {
|
||||
const PYTotalsByAccounts = await this.accountsTotal(
|
||||
this.query.PYFromDate,
|
||||
this.query.PYToDate
|
||||
);
|
||||
// Inject to the repository.
|
||||
this.PYTotalAccountsLedger = Ledger.fromTransactions(PYTotalsByAccounts);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize periods of previous year (PY).
|
||||
*/
|
||||
private initPeriodsPreviousYear = async () => {
|
||||
// Retrieves grouped transactions by given date group.
|
||||
const periodsByAccount = await this.accountsDatePeriods(
|
||||
this.query.PYFromDate,
|
||||
this.query.PYToDate,
|
||||
this.transactionsGroupType
|
||||
);
|
||||
// Inject to the repository.
|
||||
this.PYPeriodsAccountsLedger = Ledger.fromTransactions(periodsByAccount);
|
||||
};
|
||||
|
||||
// ----------------------------
|
||||
// # Utils
|
||||
// ----------------------------
|
||||
/**
|
||||
* Retrieve the opening balance transactions of the report.
|
||||
*/
|
||||
public accountsTotal = async (fromDate: Date, toDate: Date) => {
|
||||
const { AccountTransaction } = this.models;
|
||||
|
||||
return AccountTransaction.query().onBuild((query) => {
|
||||
query.sum('credit as credit');
|
||||
query.sum('debit as debit');
|
||||
query.groupBy('accountId');
|
||||
query.select(['accountId']);
|
||||
|
||||
query.modify('filterDateRange', fromDate, toDate);
|
||||
query.withGraphFetched('account');
|
||||
|
||||
this.commonFilterBranchesQuery(query);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Closing accounts date periods.
|
||||
* @param openingDate
|
||||
* @param datePeriodsType
|
||||
* @returns
|
||||
*/
|
||||
public accountsDatePeriods = async (
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
datePeriodsType
|
||||
) => {
|
||||
const { AccountTransaction } = this.models;
|
||||
|
||||
return AccountTransaction.query().onBuild((query) => {
|
||||
query.sum('credit as credit');
|
||||
query.sum('debit as debit');
|
||||
query.groupBy('accountId');
|
||||
query.select(['accountId']);
|
||||
|
||||
query.modify('groupByDateFormat', datePeriodsType);
|
||||
query.modify('filterDateRange', fromDate, toDate);
|
||||
query.withGraphFetched('account');
|
||||
|
||||
this.commonFilterBranchesQuery(query);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Common branches filter query.
|
||||
* @param {Knex.QueryBuilder} query
|
||||
*/
|
||||
private commonFilterBranchesQuery = (query: Knex.QueryBuilder) => {
|
||||
if (!isEmpty(this.query.branchesIds)) {
|
||||
query.modify('filterByBranches', this.query.branchesIds);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve accounts of the report.
|
||||
* @return {Promise<IAccount[]>}
|
||||
*/
|
||||
private getAccounts = () => {
|
||||
const { Account } = this.models;
|
||||
|
||||
return Account.query();
|
||||
};
|
||||
|
||||
public getAccountsByType = (type: string) => {
|
||||
return defaultTo(this.accountsByType.get(type), []);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import {
|
||||
IProfitLossSheetQuery,
|
||||
IProfitLossSheetMeta,
|
||||
IProfitLossSheetNode,
|
||||
} from '@/interfaces';
|
||||
import ProfitLossSheet from './ProfitLossSheet';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import InventoryService from '@/services/Inventory/Inventory';
|
||||
import { parseBoolean } from 'utils';
|
||||
import { Tenant } from '@/system/models';
|
||||
import { mergeQueryWithDefaults } from './utils';
|
||||
import { ProfitLossSheetRepository } from './ProfitLossSheetRepository';
|
||||
|
||||
// Profit/Loss sheet service.
|
||||
@Service()
|
||||
export default class ProfitLossSheetService {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
inventoryService: InventoryService;
|
||||
|
||||
/**
|
||||
* Retrieve the trial balance sheet meta.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @returns {ITrialBalanceSheetMeta}
|
||||
*/
|
||||
reportMetadata(tenantId: number): IProfitLossSheetMeta {
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
|
||||
const isCostComputeRunning =
|
||||
this.inventoryService.isItemsCostComputeRunning(tenantId);
|
||||
const organizationName = settings.get({
|
||||
group: 'organization',
|
||||
key: 'name',
|
||||
});
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
|
||||
return {
|
||||
isCostComputeRunning: parseBoolean(isCostComputeRunning, false),
|
||||
organizationName,
|
||||
baseCurrency,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve profit/loss sheet statement.
|
||||
* @param {number} tenantId
|
||||
* @param {IProfitLossSheetQuery} query
|
||||
* @return { }
|
||||
*/
|
||||
profitLossSheet = async (
|
||||
tenantId: number,
|
||||
query: IProfitLossSheetQuery
|
||||
): Promise<{
|
||||
data: IProfitLossSheetNode[];
|
||||
query: IProfitLossSheetQuery;
|
||||
meta: IProfitLossSheetMeta;
|
||||
}> => {
|
||||
const models = this.tenancy.models(tenantId);
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
|
||||
// Merges the given query with default filter query.
|
||||
const filter = mergeQueryWithDefaults(query);
|
||||
|
||||
// Get the given accounts or throw not found service error.
|
||||
// if (filter.accountsIds.length > 0) {
|
||||
// await this.accountsService.getAccountsOrThrowError(
|
||||
// tenantId,
|
||||
// filter.accountsIds
|
||||
// );
|
||||
// }
|
||||
const tenant = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
const profitLossRepo = new ProfitLossSheetRepository(models, filter);
|
||||
|
||||
await profitLossRepo.asyncInitialize();
|
||||
|
||||
// Profit/Loss report instance.
|
||||
const profitLossInstance = new ProfitLossSheet(
|
||||
profitLossRepo,
|
||||
filter,
|
||||
tenant.metadata.baseCurrency,
|
||||
i18n
|
||||
);
|
||||
// Profit/loss report data and collumns.
|
||||
const profitLossData = profitLossInstance.reportData();
|
||||
|
||||
return {
|
||||
data: profitLossData,
|
||||
query: filter,
|
||||
meta: this.reportMetadata(tenantId),
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
IProfitLossSheetQuery,
|
||||
ITableColumn,
|
||||
IProfitLossSheetAccountsNode,
|
||||
ITableColumnAccessor,
|
||||
ITableRow,
|
||||
ProfitLossNodeType,
|
||||
ProfitLossSheetRowType,
|
||||
IProfitLossSheetNode,
|
||||
IProfitLossSheetEquationNode,
|
||||
IProfitLossSheetAccountNode,
|
||||
} from '@/interfaces';
|
||||
import { tableRowMapper } from 'utils';
|
||||
import { FinancialTable } from '../FinancialTable';
|
||||
import { ProfitLossSheetBase } from './ProfitLossSheetBase';
|
||||
import { ProfitLossSheetTablePercentage } from './ProfitLossSheetTablePercentage';
|
||||
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
|
||||
import { ProfitLossTablePreviousPeriod } from './ProfitLossTablePreviousPeriod';
|
||||
import { ProfitLossTablePreviousYear } from './ProfitLossTablePreviousYear';
|
||||
import { FinancialSheetStructure } from '../FinancialSheetStructure';
|
||||
import { ProfitLossSheetTableDatePeriods } from './ProfitLossSheetTableDatePeriods';
|
||||
|
||||
export class ProfitLossSheetTable extends R.compose(
|
||||
ProfitLossTablePreviousPeriod,
|
||||
ProfitLossTablePreviousYear,
|
||||
ProfitLossSheetTablePercentage,
|
||||
ProfitLossSheetTableDatePeriods,
|
||||
ProfitLossSheetBase,
|
||||
FinancialSheetStructure,
|
||||
FinancialTable
|
||||
)(class {}) {
|
||||
readonly query: ProfitLossSheetQuery;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {} date
|
||||
* @param {IProfitLossSheetQuery} query
|
||||
*/
|
||||
constructor(data: any, query: IProfitLossSheetQuery, i18n: any) {
|
||||
super();
|
||||
|
||||
this.query = new ProfitLossSheetQuery(query);
|
||||
this.reportData = data;
|
||||
this.i18n = i18n;
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
// # Rows
|
||||
// ----------------------------------
|
||||
/**
|
||||
* Retrieve the total column accessor.
|
||||
* @return {ITableColumnAccessor[]}
|
||||
*/
|
||||
private totalColumnAccessor = (): ITableColumnAccessor[] => {
|
||||
return R.pipe(
|
||||
R.when(
|
||||
this.query.isPreviousPeriodActive,
|
||||
R.concat(this.previousPeriodColumnAccessor())
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearActive,
|
||||
R.concat(this.previousYearColumnAccessor())
|
||||
),
|
||||
R.concat(this.percentageColumnsAccessor()),
|
||||
R.concat([{ key: 'total', accessor: 'total.formattedAmount' }])
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Common columns accessors.
|
||||
* @returns {ITableColumnAccessor}
|
||||
*/
|
||||
private commonColumnsAccessors = (): ITableColumnAccessor[] => {
|
||||
return R.compose(
|
||||
R.concat([{ key: 'name', accessor: 'name' }]),
|
||||
R.ifElse(
|
||||
this.query.isDatePeriodsColumnsType,
|
||||
R.concat(this.datePeriodsColumnsAccessors()),
|
||||
R.concat(this.totalColumnAccessor())
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IProfitLossSheetAccountNode} node
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
private accountNodeToTableRow = (
|
||||
node: IProfitLossSheetAccountNode
|
||||
): ITableRow => {
|
||||
const columns = this.commonColumnsAccessors();
|
||||
const meta = {
|
||||
rowTypes: [ProfitLossSheetRowType.ACCOUNT],
|
||||
id: node.id,
|
||||
};
|
||||
return tableRowMapper(node, columns, meta);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IProfitLossSheetAccountsNode} node
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
private accountsNodeToTableRow = (
|
||||
node: IProfitLossSheetAccountsNode
|
||||
): ITableRow => {
|
||||
const columns = this.commonColumnsAccessors();
|
||||
const meta = {
|
||||
rowTypes: [ProfitLossSheetRowType.ACCOUNTS],
|
||||
id: node.id,
|
||||
};
|
||||
return tableRowMapper(node, columns, meta);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IProfitLossSheetEquationNode} node
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
private equationNodeToTableRow = (
|
||||
node: IProfitLossSheetEquationNode
|
||||
): ITableRow => {
|
||||
const columns = this.commonColumnsAccessors();
|
||||
|
||||
const meta = {
|
||||
rowTypes: [ProfitLossSheetRowType.TOTAL],
|
||||
id: node.id,
|
||||
};
|
||||
return tableRowMapper(node, columns, meta);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IProfitLossSheetNode} node
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
private nodeToTableRowCompose = (node: IProfitLossSheetNode): ITableRow => {
|
||||
return R.cond([
|
||||
[
|
||||
this.isNodeType(ProfitLossNodeType.ACCOUNTS),
|
||||
this.accountsNodeToTableRow,
|
||||
],
|
||||
[
|
||||
this.isNodeType(ProfitLossNodeType.EQUATION),
|
||||
this.equationNodeToTableRow,
|
||||
],
|
||||
[this.isNodeType(ProfitLossNodeType.ACCOUNT), this.accountNodeToTableRow],
|
||||
])(node);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IProfitLossSheetNode[]} nodes
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
private nodesToTableRowsCompose = (
|
||||
nodes: IProfitLossSheetNode[]
|
||||
): ITableRow[] => {
|
||||
return this.mapNodesDeep(nodes, this.nodeToTableRowCompose);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the table rows.
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
public tableRows = (): ITableRow[] => {
|
||||
return R.compose(
|
||||
this.addTotalRows,
|
||||
this.nodesToTableRowsCompose
|
||||
)(this.reportData);
|
||||
};
|
||||
|
||||
// ----------------------------------
|
||||
// # Columns.
|
||||
// ----------------------------------
|
||||
/**
|
||||
* Retrieve total column children columns.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
private tableColumnChildren = (): ITableColumn[] => {
|
||||
return R.compose(
|
||||
R.unless(
|
||||
R.isEmpty,
|
||||
R.concat([
|
||||
{ key: 'total', label: this.i18n.__('profit_loss_sheet.total') },
|
||||
])
|
||||
),
|
||||
R.concat(this.percentageColumns()),
|
||||
R.when(
|
||||
this.query.isPreviousYearActive,
|
||||
R.concat(this.getPreviousYearColumns())
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodActive,
|
||||
R.concat(this.getPreviousPeriodColumns())
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the total column.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
private totalColumn = (): ITableColumn[] => {
|
||||
return [
|
||||
{
|
||||
key: 'total',
|
||||
label: this.i18n.__('profit_loss_sheet.total'),
|
||||
children: this.tableColumnChildren(),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the table columns.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
public tableColumns = (): ITableColumn[] => {
|
||||
return R.compose(
|
||||
this.tableColumnsCellIndexing,
|
||||
R.concat([
|
||||
{ key: 'name', label: this.i18n.__('profit_loss_sheet.account_name') },
|
||||
]),
|
||||
R.ifElse(
|
||||
this.query.isDatePeriodsColumnsType,
|
||||
R.concat(this.datePeriodsColumns()),
|
||||
R.concat(this.totalColumn())
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import * as R from 'ramda';
|
||||
import moment from 'moment';
|
||||
import { ITableColumn, IDateRange, ITableColumnAccessor } from '@/interfaces';
|
||||
import { FinancialDatePeriods } from '../FinancialDatePeriods';
|
||||
import { ProfitLossSheetTablePercentage } from './ProfitLossSheetTablePercentage';
|
||||
import { ProfitLossTablePreviousPeriod } from './ProfitLossTablePreviousPeriod';
|
||||
|
||||
export const ProfitLossSheetTableDatePeriods = (Base) =>
|
||||
class extends R.compose(
|
||||
ProfitLossSheetTablePercentage,
|
||||
ProfitLossTablePreviousPeriod,
|
||||
FinancialDatePeriods
|
||||
)(Base) {
|
||||
/**
|
||||
* Retrieves the date periods based on the report query.
|
||||
* @returns {IDateRange[]}
|
||||
*/
|
||||
get datePeriods() {
|
||||
return this.getDateRanges(
|
||||
this.query.fromDate,
|
||||
this.query.toDate,
|
||||
this.query.displayColumnsBy
|
||||
);
|
||||
}
|
||||
|
||||
// --------------------------------
|
||||
// # Accessors
|
||||
// --------------------------------
|
||||
/**
|
||||
* Date period columns accessor.
|
||||
* @param {IDateRange} dateRange -
|
||||
* @param {number} index -
|
||||
*/
|
||||
private datePeriodColumnsAccessor = R.curry(
|
||||
(dateRange: IDateRange, index: number) => {
|
||||
return R.pipe(
|
||||
R.when(
|
||||
this.query.isPreviousPeriodActive,
|
||||
R.concat(this.previousPeriodHorizontalColumnAccessors(index))
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearActive,
|
||||
R.concat(this.previousYearHorizontalColumnAccessors(index))
|
||||
),
|
||||
R.concat(this.percetangeHorizontalColumnsAccessor(index)),
|
||||
R.concat([
|
||||
{
|
||||
key: `date-range-${index}`,
|
||||
accessor: `horizontalTotals[${index}].total.formattedAmount`,
|
||||
},
|
||||
])
|
||||
)([]);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieve the date periods columns accessors.
|
||||
* @returns {ITableColumnAccessor[]}
|
||||
*/
|
||||
protected datePeriodsColumnsAccessors = (): ITableColumnAccessor[] => {
|
||||
return R.compose(
|
||||
R.flatten,
|
||||
R.addIndex(R.map)(this.datePeriodColumnsAccessor)
|
||||
)(this.datePeriods);
|
||||
};
|
||||
|
||||
// --------------------------------
|
||||
// # Columns
|
||||
// --------------------------------
|
||||
/**
|
||||
* Retrieve the formatted column label from the given date range.
|
||||
* @param {ICashFlowDateRange} dateRange -
|
||||
* @return {string}
|
||||
*/
|
||||
private formatColumnLabel = (dateRange) => {
|
||||
const monthFormat = (range) => moment(range.toDate).format('YYYY-MM');
|
||||
const yearFormat = (range) => moment(range.toDate).format('YYYY');
|
||||
const dayFormat = (range) => moment(range.toDate).format('YYYY-MM-DD');
|
||||
|
||||
const conditions = [
|
||||
['month', monthFormat],
|
||||
['year', yearFormat],
|
||||
['day', dayFormat],
|
||||
['quarter', monthFormat],
|
||||
['week', dayFormat],
|
||||
];
|
||||
const conditionsPairs = R.map(
|
||||
([type, formatFn]) => [
|
||||
R.always(this.query.isDisplayColumnsBy(type)),
|
||||
formatFn,
|
||||
],
|
||||
conditions
|
||||
);
|
||||
return R.compose(R.cond(conditionsPairs))(dateRange);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} index
|
||||
* @param {IDateRange} dateRange
|
||||
* @returns {}
|
||||
*/
|
||||
private datePeriodChildrenColumns = (
|
||||
index: number,
|
||||
dateRange: IDateRange
|
||||
) => {
|
||||
return R.compose(
|
||||
R.unless(
|
||||
R.isEmpty,
|
||||
R.concat([
|
||||
{ key: `total`, label: this.i18n.__('profit_loss_sheet.total') },
|
||||
])
|
||||
),
|
||||
R.concat(this.percentageColumns()),
|
||||
R.when(
|
||||
this.query.isPreviousYearActive,
|
||||
R.concat(this.getPreviousYearDatePeriodColumnPlugin(dateRange))
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodActive,
|
||||
R.concat(this.getPreviousPeriodDatePeriodsPlugin(dateRange))
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IDateRange} dateRange
|
||||
* @param {number} index
|
||||
* @returns {ITableColumn}
|
||||
*/
|
||||
private datePeriodColumn = (
|
||||
dateRange: IDateRange,
|
||||
index: number
|
||||
): ITableColumn => {
|
||||
return {
|
||||
key: `date-range-${index}`,
|
||||
label: this.formatColumnLabel(dateRange),
|
||||
children: this.datePeriodChildrenColumns(index, dateRange),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Date periods columns.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected datePeriodsColumns = (): ITableColumn[] => {
|
||||
return this.datePeriods.map(this.datePeriodColumn);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
import * as R from 'ramda';
|
||||
import { ITableColumn, ITableColumnAccessor } from '@/interfaces';
|
||||
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
|
||||
|
||||
export const ProfitLossSheetTablePercentage = (Base) =>
|
||||
class extends Base {
|
||||
/**
|
||||
* @param {ProfitLossSheetQuery}
|
||||
*/
|
||||
readonly query: ProfitLossSheetQuery;
|
||||
|
||||
// ----------------------------------
|
||||
// # Columns.
|
||||
// ----------------------------------
|
||||
/**
|
||||
* Retrieve percentage of column/row columns.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected percentageColumns = (): ITableColumn[] => {
|
||||
return R.pipe(
|
||||
R.when(
|
||||
this.query.isIncomePercentage,
|
||||
R.append({
|
||||
key: 'percentage_income',
|
||||
label: this.i18n.__('profit_loss_sheet.percentage_of_income'),
|
||||
})
|
||||
),
|
||||
R.when(
|
||||
this.query.isExpensesPercentage,
|
||||
R.append({
|
||||
key: 'percentage_expenses',
|
||||
label: this.i18n.__('profit_loss_sheet.percentage_of_expenses'),
|
||||
})
|
||||
),
|
||||
R.when(
|
||||
this.query.isColumnPercentage,
|
||||
R.append({
|
||||
key: 'percentage_column',
|
||||
label: this.i18n.__('profit_loss_sheet.percentage_of_column'),
|
||||
})
|
||||
),
|
||||
R.when(
|
||||
this.query.isRowPercentage,
|
||||
R.append({
|
||||
key: 'percentage_row',
|
||||
label: this.i18n.__('profit_loss_sheet.percentage_of_row'),
|
||||
})
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
|
||||
// ----------------------------------
|
||||
// # Accessors.
|
||||
// ----------------------------------
|
||||
/**
|
||||
* Retrieves percentage of column/row accessors.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected percentageColumnsAccessor = (): ITableColumnAccessor[] => {
|
||||
return R.pipe(
|
||||
R.when(
|
||||
this.query.isIncomePercentage,
|
||||
R.append({
|
||||
key: 'percentage_income',
|
||||
accessor: 'percentageIncome.formattedAmount',
|
||||
})
|
||||
),
|
||||
R.when(
|
||||
this.query.isExpensesPercentage,
|
||||
R.append({
|
||||
key: 'percentage_expense',
|
||||
accessor: 'percentageExpense.formattedAmount',
|
||||
})
|
||||
),
|
||||
R.when(
|
||||
this.query.isColumnPercentage,
|
||||
R.append({
|
||||
key: 'percentage_column',
|
||||
accessor: 'percentageColumn.formattedAmount',
|
||||
})
|
||||
),
|
||||
R.when(
|
||||
this.query.isRowPercentage,
|
||||
R.append({
|
||||
key: 'percentage_row',
|
||||
accessor: 'percentageRow.formattedAmount',
|
||||
})
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves percentage horizontal columns accessors.
|
||||
* @param {number} index
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected percetangeHorizontalColumnsAccessor = (
|
||||
index: number
|
||||
): ITableColumnAccessor[] => {
|
||||
return R.pipe(
|
||||
R.when(
|
||||
this.query.isIncomePercentage,
|
||||
R.append({
|
||||
key: `percentage_income-${index}`,
|
||||
accessor: `horizontalTotals[${index}].percentageIncome.formattedAmount`,
|
||||
})
|
||||
),
|
||||
R.when(
|
||||
this.query.isExpensesPercentage,
|
||||
R.append({
|
||||
key: `percentage_expense-${index}`,
|
||||
accessor: `horizontalTotals[${index}].percentageExpense.formattedAmount`,
|
||||
})
|
||||
),
|
||||
R.when(
|
||||
this.query.isColumnPercentage,
|
||||
R.append({
|
||||
key: `percentage_of_column-${index}`,
|
||||
accessor: `horizontalTotals[${index}].percentageColumn.formattedAmount`,
|
||||
})
|
||||
),
|
||||
R.when(
|
||||
this.query.isRowPercentage,
|
||||
R.append({
|
||||
key: `percentage_of_row-${index}`,
|
||||
accessor: `horizontalTotals[${index}].percentageRow.formattedAmount`,
|
||||
})
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import * as R from 'ramda';
|
||||
import { IDateRange, ITableColumn, ITableColumnAccessor } from '@/interfaces';
|
||||
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
|
||||
import { FinancialTablePreviousPeriod } from '../FinancialTablePreviousPeriod';
|
||||
|
||||
export const ProfitLossTablePreviousPeriod = (Base) =>
|
||||
class extends R.compose(FinancialTablePreviousPeriod)(Base) {
|
||||
query: ProfitLossSheetQuery;
|
||||
|
||||
// ----------------------------
|
||||
// # Columns
|
||||
// ----------------------------
|
||||
/**
|
||||
* Retrieves pervious period comparison columns.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected getPreviousPeriodColumns = (
|
||||
dateRange?: IDateRange
|
||||
): ITableColumn[] => {
|
||||
return R.pipe(
|
||||
// Previous period columns.
|
||||
R.append(this.getPreviousPeriodTotalColumn(dateRange)),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodChangeActive,
|
||||
R.append(this.getPreviousPeriodChangeColumn())
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodPercentageActive,
|
||||
R.append(this.getPreviousPeriodPercentageColumn())
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Compose the previous period for date periods columns.
|
||||
* @params {IDateRange}
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected getPreviousPeriodDatePeriodsPlugin = (
|
||||
dateRange: IDateRange
|
||||
): ITableColumn[] => {
|
||||
const PPDateRange = this.getPPDatePeriodDateRange(
|
||||
dateRange.fromDate,
|
||||
dateRange.toDate,
|
||||
this.query.displayColumnsBy
|
||||
);
|
||||
return this.getPreviousPeriodColumns(PPDateRange);
|
||||
};
|
||||
|
||||
// ----------------------------
|
||||
// # Accessors
|
||||
// ----------------------------
|
||||
/**
|
||||
* Retrieves previous period columns accessors.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected previousPeriodColumnAccessor = (): ITableColumnAccessor[] => {
|
||||
return R.pipe(
|
||||
// Previous period columns.
|
||||
R.append(this.getPreviousPeriodTotalAccessor()),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodChangeActive,
|
||||
R.append(this.getPreviousPeriodChangeAccessor())
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodPercentageActive,
|
||||
R.append(this.getPreviousPeriodPercentageAccessor())
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Previous period period column accessor.
|
||||
* @param {number} index
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected previousPeriodHorizontalColumnAccessors = (
|
||||
index: number
|
||||
): ITableColumnAccessor[] => {
|
||||
return R.pipe(
|
||||
// Previous period columns.
|
||||
R.append(this.getPreviousPeriodTotalHorizAccessor(index)),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodChangeActive,
|
||||
R.append(this.getPreviousPeriodChangeHorizAccessor(index))
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousPeriodPercentageActive,
|
||||
R.append(this.getPreviousPeriodPercentageHorizAccessor(index))
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
import * as R from 'ramda';
|
||||
import { IDateRange, ITableColumn, ITableColumnAccessor } from '@/interfaces';
|
||||
import { ProfitLossSheetQuery } from './ProfitLossSheetQuery';
|
||||
import { FinancialTablePreviousYear } from '../FinancialTablePreviousYear';
|
||||
import { FinancialDateRanges } from '../FinancialDateRanges';
|
||||
|
||||
export const ProfitLossTablePreviousYear = (Base) =>
|
||||
class extends R.compose(
|
||||
FinancialTablePreviousYear,
|
||||
FinancialDateRanges
|
||||
)(Base) {
|
||||
query: ProfitLossSheetQuery;
|
||||
|
||||
// ------------------------------------
|
||||
// # Columns.
|
||||
// ------------------------------------
|
||||
/**
|
||||
* Retrieves pervious year comparison columns.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected getPreviousYearColumns = (
|
||||
dateRange?: IDateRange
|
||||
): ITableColumn[] => {
|
||||
return R.pipe(
|
||||
// Previous year columns.
|
||||
R.append(this.getPreviousYearTotalColumn(dateRange)),
|
||||
R.when(
|
||||
this.query.isPreviousYearChangeActive,
|
||||
R.append(this.getPreviousYearChangeColumn())
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearPercentageActive,
|
||||
R.append(this.getPreviousYearPercentageColumn())
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IDateRange} dateRange
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
private previousYearDatePeriodColumnCompose = (
|
||||
dateRange: IDateRange
|
||||
): ITableColumn[] => {
|
||||
const PYDateRange = this.getPreviousYearDateRange(
|
||||
dateRange.fromDate,
|
||||
dateRange.toDate
|
||||
);
|
||||
return this.getPreviousYearColumns(PYDateRange);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves previous year date periods columns.
|
||||
* @param {IDateRange} dateRange
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected getPreviousYearDatePeriodColumnPlugin = (
|
||||
dateRange: IDateRange
|
||||
): ITableColumn[] => {
|
||||
return this.previousYearDatePeriodColumnCompose(dateRange);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------
|
||||
// # Accessors.
|
||||
// ---------------------------------------------------
|
||||
/**
|
||||
* Retrieves previous year columns accessors.
|
||||
* @returns {ITableColumnAccessor[]}
|
||||
*/
|
||||
protected previousYearColumnAccessor = (): ITableColumnAccessor[] => {
|
||||
return R.pipe(
|
||||
// Previous year columns.
|
||||
R.append(this.getPreviousYearTotalAccessor()),
|
||||
R.when(
|
||||
this.query.isPreviousYearChangeActive,
|
||||
R.append(this.getPreviousYearChangeAccessor())
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearPercentageActive,
|
||||
R.append(this.getPreviousYearPercentageAccessor())
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Previous year period column accessor.
|
||||
* @param {number} index
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected previousYearHorizontalColumnAccessors = (
|
||||
index: number
|
||||
): ITableColumnAccessor[] => {
|
||||
return R.pipe(
|
||||
// Previous year columns.
|
||||
R.append(this.getPreviousYearTotalHorizAccessor(index)),
|
||||
R.when(
|
||||
this.query.isPreviousYearChangeActive,
|
||||
R.append(this.getPreviousYearChangeHorizAccessor(index))
|
||||
),
|
||||
R.when(
|
||||
this.query.isPreviousYearPercentageActive,
|
||||
R.append(this.getPreviousYearPercentageHorizAccessor(index))
|
||||
)
|
||||
)([]);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ProfitLossNodeType } from '@/interfaces';
|
||||
|
||||
export const MAP_CONFIG = { childrenPath: 'children', pathFormat: 'array' };
|
||||
|
||||
export const DISPLAY_COLUMNS_BY = {
|
||||
DATE_PERIODS: 'date_periods',
|
||||
TOTAL: 'total',
|
||||
};
|
||||
|
||||
export enum IROW_TYPE {
|
||||
AGGREGATE = 'AGGREGATE',
|
||||
ACCOUNTS = 'ACCOUNTS',
|
||||
ACCOUNT = 'ACCOUNT',
|
||||
TOTAL = 'TOTAL',
|
||||
}
|
||||
|
||||
export const TOTAL_NODE_TYPES = [
|
||||
ProfitLossNodeType.ACCOUNTS,
|
||||
ProfitLossNodeType.AGGREGATE,
|
||||
ProfitLossNodeType.EQUATION
|
||||
];
|
||||
@@ -0,0 +1,54 @@
|
||||
import moment from 'moment';
|
||||
import { merge } from 'lodash';
|
||||
import { IProfitLossSheetQuery } from '@/interfaces';
|
||||
|
||||
/**
|
||||
* Default sheet filter query.
|
||||
* @return {IBalanceSheetQuery}
|
||||
*/
|
||||
export const getDefaultPLQuery = (): IProfitLossSheetQuery => ({
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
|
||||
numberFormat: {
|
||||
divideOn1000: false,
|
||||
negativeFormat: 'mines',
|
||||
showZero: false,
|
||||
formatMoney: 'total',
|
||||
precision: 2,
|
||||
},
|
||||
basis: 'accural',
|
||||
|
||||
noneZero: false,
|
||||
noneTransactions: false,
|
||||
|
||||
displayColumnsType: 'total',
|
||||
displayColumnsBy: 'month',
|
||||
|
||||
accountsIds: [],
|
||||
|
||||
percentageColumn: false,
|
||||
percentageRow: false,
|
||||
|
||||
percentageIncome: false,
|
||||
percentageExpense: false,
|
||||
|
||||
previousPeriod: false,
|
||||
previousPeriodAmountChange: false,
|
||||
previousPeriodPercentageChange: false,
|
||||
|
||||
previousYear: false,
|
||||
previousYearAmountChange: false,
|
||||
previousYearPercentageChange: false,
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @param query
|
||||
* @returns
|
||||
*/
|
||||
export const mergeQueryWithDefaults = (
|
||||
query: IProfitLossSheetQuery
|
||||
): IProfitLossSheetQuery => {
|
||||
return merge(getDefaultPLQuery(), query);
|
||||
};
|
||||
@@ -0,0 +1,216 @@
|
||||
import { sumBy } from 'lodash';
|
||||
import { map } from 'lodash/fp';
|
||||
import {
|
||||
IProjectProfitabilitySummaryProjectNode,
|
||||
IProjectProfitabilitySummaryTotal,
|
||||
} from '@/interfaces';
|
||||
import Project from 'models/Project';
|
||||
import { ProjectProfitabilitySummaryRespository } from './ProjectProfitabilitySummaryRepository';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
|
||||
export class ProfitProfitabilitySummary extends FinancialSheet {
|
||||
private readonly repository: ProjectProfitabilitySummaryRespository;
|
||||
private readonly baseCurrency: string;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {ProjectProfitabilitySummaryRespository} repository
|
||||
* @param {string} baseCurrency
|
||||
*/
|
||||
constructor(
|
||||
repository: ProjectProfitabilitySummaryRespository,
|
||||
baseCurrency: string
|
||||
) {
|
||||
super();
|
||||
|
||||
this.repository = repository;
|
||||
this.baseCurrency = baseCurrency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the project income node.
|
||||
* @param {number} projectId
|
||||
* @returns {IProjectProfitabilitySummaryTotal}
|
||||
*/
|
||||
private getProjectIncomeNode = (
|
||||
projectId: number
|
||||
): IProjectProfitabilitySummaryTotal => {
|
||||
const amount = this.repository.incomeLedger
|
||||
.whereProject(projectId)
|
||||
.getClosingBalance();
|
||||
|
||||
const formattedAmount = this.formatNumber(amount);
|
||||
|
||||
return {
|
||||
amount,
|
||||
formattedAmount,
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the project expense node.
|
||||
* @param {number} projectId
|
||||
* @returns {IProjectProfitabilitySummaryTotal}
|
||||
*/
|
||||
private getProjectExpenseNode = (
|
||||
projectId: number
|
||||
): IProjectProfitabilitySummaryTotal => {
|
||||
const amount = this.repository.expenseLedger
|
||||
.whereProject(projectId)
|
||||
.getClosingBalance();
|
||||
|
||||
const formattedAmount = this.formatNumber(amount);
|
||||
|
||||
return {
|
||||
amount,
|
||||
formattedAmount,
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the project profit total node.
|
||||
* @param {number} projectId - Project id.
|
||||
* @returns {number}
|
||||
*/
|
||||
private getProjectProfitTotal = (projectId: number): number => {
|
||||
const incomeTotal = this.repository.incomeLedger
|
||||
.whereProject(projectId)
|
||||
.getClosingBalance();
|
||||
|
||||
const expenseTotal = this.repository.expenseLedger
|
||||
.whereProject(projectId)
|
||||
.getClosingBalance();
|
||||
|
||||
return incomeTotal - expenseTotal;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the project profit node.
|
||||
* @param {number} projectId - Project id.
|
||||
* @returns {IProjectProfitabilitySummaryTotal}
|
||||
*/
|
||||
private getProjectProfitNode = (
|
||||
projectId: number
|
||||
): IProjectProfitabilitySummaryTotal => {
|
||||
const amount = this.getProjectProfitTotal(projectId);
|
||||
const formattedAmount = this.formatNumber(amount);
|
||||
|
||||
return {
|
||||
amount,
|
||||
formattedAmount,
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the project node.
|
||||
* @param {Project} project
|
||||
* @returns {IProjectProfitabilitySummaryProjectNode}
|
||||
*/
|
||||
private getProjectNode = (
|
||||
project: Project
|
||||
): IProjectProfitabilitySummaryProjectNode => {
|
||||
return {
|
||||
projectId: project.id,
|
||||
projectName: project.name,
|
||||
projectStatus: 1,
|
||||
|
||||
customerName: project.contact.displayName,
|
||||
customerId: project.contact.id,
|
||||
|
||||
profit: this.getProjectProfitNode(project.id),
|
||||
income: this.getProjectIncomeNode(project.id),
|
||||
expenses: this.getProjectExpenseNode(project.id),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the projects nodes.
|
||||
* @returns {IProjectProfitabilitySummaryProjectNode[]}
|
||||
*/
|
||||
private getProjectsNode = (): IProjectProfitabilitySummaryProjectNode[] => {
|
||||
return map(this.getProjectNode)(this.repository.projects);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the all projects total income node.
|
||||
* @param {IProjectProfitabilitySummaryProjectNode[]} projects
|
||||
* @returns {IProjectProfitabilitySummaryTotal}
|
||||
*/
|
||||
private getProjectsTotalIncomeNode = (
|
||||
projects: IProjectProfitabilitySummaryProjectNode[]
|
||||
): IProjectProfitabilitySummaryTotal => {
|
||||
const amount = sumBy(projects, 'income.amount');
|
||||
const formattedAmount = this.formatTotalNumber(amount);
|
||||
|
||||
return {
|
||||
amount,
|
||||
formattedAmount,
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the all projects expenses total node.
|
||||
* @param {IProjectProfitabilitySummaryProjectNode[]} projects
|
||||
* @returns {IProjectProfitabilitySummaryTotal}
|
||||
*/
|
||||
private getProjectsTotalExpensesNode = (
|
||||
projects: IProjectProfitabilitySummaryProjectNode[]
|
||||
): IProjectProfitabilitySummaryTotal => {
|
||||
const amount = sumBy(projects, 'expenses.amount');
|
||||
const formattedAmount = this.formatTotalNumber(amount);
|
||||
|
||||
return {
|
||||
amount,
|
||||
formattedAmount,
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retreives the all projects profit total node.
|
||||
* @param {IProjectProfitabilitySummaryProjectNode[]} projects
|
||||
* @returns {IProjectProfitabilitySummaryTotal}
|
||||
*/
|
||||
private getProjectsTotalProfitNode = (
|
||||
projects: IProjectProfitabilitySummaryProjectNode[]
|
||||
) => {
|
||||
const amount = sumBy(projects, 'profit.amount');
|
||||
const formattedAmount = this.formatTotalNumber(amount);
|
||||
|
||||
return {
|
||||
amount,
|
||||
formattedAmount,
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the all projects total node.
|
||||
* @param {IProjectProfitabilitySummaryProjectNode[]} projects
|
||||
* @returns {IProjectProfitabilitySummaryTotal}
|
||||
*/
|
||||
private getProjectsTotalNode = (
|
||||
projects: IProjectProfitabilitySummaryProjectNode[]
|
||||
) => {
|
||||
const income = this.getProjectsTotalIncomeNode(projects);
|
||||
const expenses = this.getProjectsTotalExpensesNode(projects);
|
||||
const profit = this.getProjectsTotalProfitNode(projects);
|
||||
|
||||
return { income, expenses, profit };
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the report data.
|
||||
* @returns {IProjectProfitabilitySummaryTotal}
|
||||
*/
|
||||
public getReportData = () => {
|
||||
const projects = this.getProjectsNode();
|
||||
const total = this.getProjectsTotalNode(projects);
|
||||
|
||||
return { projects, total };
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { ACCOUNT_NORMAL, ACCOUNT_ROOT_TYPE } from '@/data/AccountTypes';
|
||||
import { IAccount, ProjectProfitabilitySummaryQuery } from '@/interfaces';
|
||||
import { isEmpty, map } from 'lodash';
|
||||
import Project from 'models/Project';
|
||||
import Ledger from '@/services/Accounting/Ledger';
|
||||
|
||||
export class ProjectProfitabilitySummaryRespository {
|
||||
/**
|
||||
* Tenant models.
|
||||
*/
|
||||
private readonly models;
|
||||
|
||||
/**
|
||||
* The report query.
|
||||
* @param {ProjectProfitabilitySummaryQuery}
|
||||
*/
|
||||
private readonly query: ProjectProfitabilitySummaryQuery;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Project[]}
|
||||
*/
|
||||
public projects: Project[];
|
||||
|
||||
/**
|
||||
* Income ledger grouped by projects.
|
||||
* @param {Ledger}
|
||||
*/
|
||||
public incomeLedger: Ledger;
|
||||
|
||||
/**
|
||||
* Expenses ledger grouped by projects.
|
||||
* @param {Ledger}
|
||||
*/
|
||||
public expenseLedger: Ledger;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Models} models -
|
||||
* @param {ProjectProfitabilitySummaryQuery} query -
|
||||
*/
|
||||
constructor(models: any, query: ProjectProfitabilitySummaryQuery) {
|
||||
this.query = query;
|
||||
this.models = models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Async initialize all DB queries.
|
||||
*/
|
||||
public asyncInitialize = async () => {
|
||||
await this.initProjects();
|
||||
|
||||
const incomeEntries = await this.getIncomeAccountsGroupedEntries();
|
||||
const expenseEntries = await this.getExpenseAccountsGroupedEntries();
|
||||
|
||||
this.incomeLedger = Ledger.fromTransactions(incomeEntries);
|
||||
this.expenseLedger = Ledger.fromTransactions(expenseEntries);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize projects.
|
||||
*/
|
||||
public initProjects = async () => {
|
||||
const { Project } = this.models;
|
||||
|
||||
const projects = await Project.query().onBuild((query) => {
|
||||
if (this.query.projectsIds) {
|
||||
query.whereIn('id', this.query.projectsIds);
|
||||
}
|
||||
query.withGraphFetched('contact');
|
||||
});
|
||||
this.projects = projects;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the sumation of grouped entries by account and project id.
|
||||
* @param {number[]} accountsIds
|
||||
* @param {string} accountNormal -
|
||||
* @returns {}
|
||||
*/
|
||||
public getAccountsGroupedEntries = async (accountsIds: number[]) => {
|
||||
const { AccountTransaction } = this.models;
|
||||
|
||||
return AccountTransaction.query().onBuild((query) => {
|
||||
query.sum('credit as credit');
|
||||
query.sum('debit as debit');
|
||||
query.select(['accountId', 'projectId']);
|
||||
|
||||
query.groupBy('accountId');
|
||||
query.groupBy('projectId');
|
||||
|
||||
query.whereNotNull('projectId');
|
||||
query.withGraphFetched('account');
|
||||
|
||||
query.whereIn('accountId', accountsIds);
|
||||
|
||||
if (!isEmpty(this.query.projectsIds)) {
|
||||
query.modify('filterByProjects', this.query.projectsIds);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves all income accounts.
|
||||
* @returns {IAccount}
|
||||
*/
|
||||
public getIncomeAccounts = () => {
|
||||
const { Account } = this.models;
|
||||
|
||||
return Account.query().modify('filterByRootType', ACCOUNT_ROOT_TYPE.INCOME);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves all expenses accounts.
|
||||
* @returns
|
||||
*/
|
||||
public getExpensesAccounts = () => {
|
||||
const { Account } = this.models;
|
||||
|
||||
return Account.query().modify(
|
||||
'filterByRootType',
|
||||
ACCOUNT_ROOT_TYPE.EXPENSE
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the sumation of grouped entries by income accounts and projects.
|
||||
* @returns {}
|
||||
*/
|
||||
public getIncomeAccountsGroupedEntries = async () => {
|
||||
const incomeAccounts = await this.getIncomeAccounts();
|
||||
const incomeAcountssIds = map(incomeAccounts, 'id');
|
||||
|
||||
return this.getAccountsGroupedEntries(incomeAcountssIds);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the sumation of grouped entries by expenses accounts and projects.
|
||||
* @returns {}
|
||||
*/
|
||||
public getExpenseAccountsGroupedEntries = async () => {
|
||||
const expenseAccounts = await this.getExpensesAccounts();
|
||||
const expenseAccountsIds = map(expenseAccounts, 'id');
|
||||
|
||||
return this.getAccountsGroupedEntries(expenseAccountsIds);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
IProjectProfitabilitySummaryMeta,
|
||||
IProjectProfitabilitySummaryPOJO,
|
||||
ProjectProfitabilitySummaryQuery,
|
||||
} from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { Tenant } from '@/system/models';
|
||||
import { ProfitProfitabilitySummary } from './ProjectProfitabilitySummary';
|
||||
import { ProjectProfitabilitySummaryRespository } from './ProjectProfitabilitySummaryRepository';
|
||||
|
||||
@Service()
|
||||
export class ProjectProfitabilitySummaryService {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Retrieves the project profitiability summary report.
|
||||
* @param {number} tenantId
|
||||
* @param {ProjectProfitabilitySummaryQuery} query
|
||||
* @returns {Promise<IProjectProfitabilitySummaryPOJO>}
|
||||
*/
|
||||
public projectProfitabilitySummary = async (
|
||||
tenantId: number,
|
||||
query: ProjectProfitabilitySummaryQuery
|
||||
): Promise<IProjectProfitabilitySummaryPOJO> => {
|
||||
const models = this.tenancy.models(tenantId);
|
||||
|
||||
const tenant = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
// Initialize the report repository.
|
||||
const projectProfitabilityRepo = new ProjectProfitabilitySummaryRespository(
|
||||
models,
|
||||
query
|
||||
);
|
||||
await projectProfitabilityRepo.asyncInitialize();
|
||||
|
||||
const projectProfitabilityInstance = new ProfitProfitabilitySummary(
|
||||
projectProfitabilityRepo,
|
||||
tenant.metadata.baseCurrency
|
||||
);
|
||||
const projectProfitData = projectProfitabilityInstance.getReportData();
|
||||
|
||||
return {
|
||||
data: projectProfitData,
|
||||
query,
|
||||
meta: this.reportMetadata(tenantId),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the balance sheet meta.
|
||||
* @param {number} tenantId -
|
||||
* @returns {IBalanceSheetMeta}
|
||||
*/
|
||||
private reportMetadata(tenantId: number): IProjectProfitabilitySummaryMeta {
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
|
||||
const organizationName = settings.get({
|
||||
group: 'organization',
|
||||
key: 'name',
|
||||
});
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
|
||||
return {
|
||||
organizationName,
|
||||
baseCurrency,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { map } from 'lodash/fp';
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
IProjectProfitabilitySummaryData,
|
||||
IProjectProfitabilitySummaryProjectNode,
|
||||
IProjectProfitabilitySummaryRowType,
|
||||
IProjectProfitabilitySummaryTotalNode,
|
||||
ITableColumn,
|
||||
ITableRow,
|
||||
} from '@/interfaces';
|
||||
import { tableRowMapper } from 'utils';
|
||||
|
||||
export class ProjectProfitabilitySummaryTable {
|
||||
/**
|
||||
* Holds the report data.
|
||||
* @var {IProjectProfitabilitySummaryPOJO}
|
||||
*/
|
||||
private readonly reportData: IProjectProfitabilitySummaryData;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {IProjectProfitabilitySummaryData} reportData
|
||||
* @param {} i18n
|
||||
*/
|
||||
constructor(
|
||||
reportData: IProjectProfitabilitySummaryData,
|
||||
i18n: any
|
||||
) {
|
||||
this.reportData = reportData;
|
||||
this.i18n = i18n;
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
// # ROWS.
|
||||
// ----------------------------------
|
||||
/**
|
||||
* Retrieves the project node table row.
|
||||
* @param {IProjectProfitabilitySummaryProjectNode} node
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
private projectNodeData = (
|
||||
node: IProjectProfitabilitySummaryProjectNode
|
||||
): ITableRow => {
|
||||
const meta = {
|
||||
rowTypes: [IProjectProfitabilitySummaryRowType.PROJECT],
|
||||
};
|
||||
const columns = [
|
||||
{ key: 'name', accessor: 'projectName' },
|
||||
{ key: 'customer_name', accessor: 'customerName' },
|
||||
{ key: 'income', accessor: 'income.formattedAmount' },
|
||||
{ key: 'expenses', accessor: 'expenses.formattedAmount' },
|
||||
{ key: 'profit', accessor: 'profit.formattedAmount' },
|
||||
];
|
||||
return tableRowMapper(node, columns, meta);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the projects nodes table rows.
|
||||
* @param {IProjectProfitabilitySummaryProjectNode[]} nodes
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
public projectsNodesData = (
|
||||
nodes: IProjectProfitabilitySummaryProjectNode[]
|
||||
): ITableRow[] => {
|
||||
return map(this.projectNodeData)(nodes);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the projects total table row.
|
||||
* @param {IProjectProfitabilitySummaryTotal} totalNode
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
public projectsTotalRow = (
|
||||
node: IProjectProfitabilitySummaryTotalNode
|
||||
): ITableRow => {
|
||||
const meta = {
|
||||
rowTypes: [IProjectProfitabilitySummaryRowType.TOTAL],
|
||||
};
|
||||
const columns = [
|
||||
{ key: 'name', value: '' },
|
||||
{ key: 'customer_name', value: '' },
|
||||
{ key: 'income', accessor: 'income.formattedAmount' },
|
||||
{ key: 'expenses', accessor: 'expenses.formattedAmount' },
|
||||
{ key: 'profit', accessor: 'profit.formattedAmount' },
|
||||
];
|
||||
return tableRowMapper(node, columns, meta);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the table rows.
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
public tableData = (): ITableRow[] => {
|
||||
return R.pipe(
|
||||
R.concat(this.projectsNodesData(this.reportData.projects)),
|
||||
R.append(this.projectsTotalRow(this.reportData.total))
|
||||
)([]);
|
||||
};
|
||||
|
||||
// ----------------------------------
|
||||
// # Columns.
|
||||
// ----------------------------------
|
||||
/**
|
||||
* Retrievs the table columns
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
public tableColumns = (): ITableColumn[] => {
|
||||
return [
|
||||
{ key: 'name', label: 'Project Name' },
|
||||
{ key: 'customer_name', label: 'Customer Name' },
|
||||
{ key: 'income', label: 'Income' },
|
||||
{ key: 'expenses', label: 'Expenses' },
|
||||
{ key: 'profit', label: 'Profit' },
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import { get, isEmpty, sumBy } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
import { allPassedConditionsPass, transformToMap } from 'utils';
|
||||
import {
|
||||
IAccountTransaction,
|
||||
IInventoryValuationTotal,
|
||||
IInventoryValuationItem,
|
||||
IInventoryValuationReportQuery,
|
||||
IInventoryValuationStatement,
|
||||
IItem,
|
||||
} from '@/interfaces';
|
||||
|
||||
export default class InventoryValuationReport extends FinancialSheet {
|
||||
readonly baseCurrency: string;
|
||||
readonly items: IItem[];
|
||||
readonly itemsTransactions: Map<number, IAccountTransaction>;
|
||||
readonly query: IInventoryValuationReportQuery;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {IInventoryValuationReportQuery} query
|
||||
* @param {IItem[]} items
|
||||
* @param {IAccountTransaction[]} itemsTransactions
|
||||
* @param {string} baseCurrency
|
||||
*/
|
||||
constructor(
|
||||
query: IInventoryValuationReportQuery,
|
||||
items: IItem[],
|
||||
itemsTransactions: IAccountTransaction[],
|
||||
baseCurrency: string
|
||||
) {
|
||||
super();
|
||||
|
||||
this.baseCurrency = baseCurrency;
|
||||
this.items = items;
|
||||
this.itemsTransactions = transformToMap(itemsTransactions, 'itemId');
|
||||
this.query = query;
|
||||
this.numberFormat = this.query.numberFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the item purchase item, cost and average cost price.
|
||||
* @param {number} itemId
|
||||
*/
|
||||
getItemTransaction(itemId: number): {
|
||||
quantity: number;
|
||||
cost: number;
|
||||
average: number;
|
||||
} {
|
||||
const transaction = this.itemsTransactions.get(itemId);
|
||||
|
||||
const quantity = get(transaction, 'quantity', 0);
|
||||
const cost = get(transaction, 'cost', 0);
|
||||
|
||||
const average = cost / quantity;
|
||||
|
||||
return { quantity, cost, average };
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the purchase node is active.
|
||||
* @param {} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private filterPurchaseOnlyActive = (node) => {
|
||||
return node.quantityPurchased !== 0 && node.purchaseCost !== 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines whether the purchase node is not none transactions.
|
||||
* @param node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private filterPurchaseNoneTransaction = (node) => {
|
||||
const anyTransaction = this.itemsTransactions.get(node.id);
|
||||
|
||||
return !isEmpty(anyTransaction);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters sales by items nodes based on the report query.
|
||||
* @param {ISalesByItemsItem} saleItem -
|
||||
* @return {boolean}
|
||||
*/
|
||||
private purchaseByItemFilter = (node): boolean => {
|
||||
const { noneTransactions, onlyActive } = this.query;
|
||||
|
||||
const conditions = [
|
||||
[noneTransactions, this.filterPurchaseNoneTransaction],
|
||||
[onlyActive, this.filterPurchaseOnlyActive],
|
||||
];
|
||||
return allPassedConditionsPass(conditions)(node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mapping the given item section.
|
||||
* @param {IInventoryValuationItem} item
|
||||
* @returns
|
||||
*/
|
||||
private itemSectionMapper = (item: IItem): IInventoryValuationItem => {
|
||||
const meta = this.getItemTransaction(item.id);
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
code: item.code,
|
||||
quantityPurchased: meta.quantity,
|
||||
purchaseCost: meta.cost,
|
||||
averageCostPrice: meta.average,
|
||||
quantityPurchasedFormatted: this.formatNumber(meta.quantity, {
|
||||
money: false,
|
||||
}),
|
||||
purchaseCostFormatted: this.formatNumber(meta.cost),
|
||||
averageCostPriceFormatted: this.formatNumber(meta.average),
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether the items post filter is active.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private isItemsPostFilter = (): boolean => {
|
||||
return isEmpty(this.query.itemsIds);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters purchase by items nodes.
|
||||
* @param {} nodes -
|
||||
* @returns
|
||||
*/
|
||||
private itemsFilter = (nodes) => {
|
||||
return nodes.filter(this.purchaseByItemFilter);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mappes purchase by items nodes.
|
||||
* @param items
|
||||
* @returns
|
||||
*/
|
||||
private itemsMapper = (items) => {
|
||||
return items.map(this.itemSectionMapper);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the items sections.
|
||||
* @returns {IInventoryValuationItem[]}
|
||||
*/
|
||||
private itemsSection = (): IInventoryValuationItem[] => {
|
||||
return R.compose(
|
||||
R.when(this.isItemsPostFilter, this.itemsFilter),
|
||||
this.itemsMapper
|
||||
)(this.items);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the total section of the sheet.
|
||||
* @param {IInventoryValuationItem[]} items
|
||||
* @returns {IInventoryValuationTotal}
|
||||
*/
|
||||
totalSection(items: IInventoryValuationItem[]): IInventoryValuationTotal {
|
||||
const quantityPurchased = sumBy(items, (item) => item.quantityPurchased);
|
||||
const purchaseCost = sumBy(items, (item) => item.purchaseCost);
|
||||
|
||||
return {
|
||||
quantityPurchased,
|
||||
purchaseCost,
|
||||
quantityPurchasedFormatted: this.formatTotalNumber(quantityPurchased, {
|
||||
money: false,
|
||||
}),
|
||||
purchaseCostFormatted: this.formatTotalNumber(purchaseCost),
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the sheet data.
|
||||
* @returns
|
||||
*/
|
||||
reportData(): IInventoryValuationStatement {
|
||||
const items = this.itemsSection();
|
||||
const total = this.totalSection(items);
|
||||
|
||||
return items.length > 0 ? { items, total } : {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
IInventoryValuationReportQuery,
|
||||
IInventoryValuationStatement,
|
||||
IInventoryValuationSheetMeta,
|
||||
} from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import PurchasesByItems from './PurchasesByItems';
|
||||
import { Tenant } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export default class InventoryValuationReportService {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Defaults balance sheet filter query.
|
||||
* @return {IBalanceSheetQuery}
|
||||
*/
|
||||
get defaultQuery(): IInventoryValuationReportQuery {
|
||||
return {
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
itemsIds: [],
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
divideOn1000: false,
|
||||
showZero: false,
|
||||
formatMoney: 'always',
|
||||
negativeFormat: 'mines',
|
||||
},
|
||||
noneTransactions: true,
|
||||
onlyActive: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the balance sheet meta.
|
||||
* @param {number} tenantId -
|
||||
* @returns {IBalanceSheetMeta}
|
||||
*/
|
||||
reportMetadata(tenantId: number): IInventoryValuationSheetMeta {
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
|
||||
const organizationName = settings.get({
|
||||
group: 'organization',
|
||||
key: 'name',
|
||||
});
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
|
||||
return {
|
||||
organizationName,
|
||||
baseCurrency,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve balance sheet statement.
|
||||
* -------------
|
||||
* @param {number} tenantId
|
||||
* @param {IBalanceSheetQuery} query
|
||||
*
|
||||
* @return {IBalanceSheetStatement}
|
||||
*/
|
||||
public async purchasesByItems(
|
||||
tenantId: number,
|
||||
query: IInventoryValuationReportQuery
|
||||
): Promise<{
|
||||
data: IInventoryValuationStatement,
|
||||
query: IInventoryValuationReportQuery,
|
||||
meta: IInventoryValuationSheetMeta,
|
||||
}> {
|
||||
const { Item, InventoryTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
const tenant = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
const inventoryItems = await Item.query().onBuild(q => {
|
||||
q.where('type', 'inventory');
|
||||
|
||||
if (filter.itemsIds.length > 0) {
|
||||
q.whereIn('id', filter.itemsIds);
|
||||
}
|
||||
});
|
||||
const inventoryItemsIds = inventoryItems.map((item) => item.id);
|
||||
|
||||
// Calculates the total inventory total quantity and rate `IN` transactions.
|
||||
const inventoryTransactions = await InventoryTransaction.query().onBuild(
|
||||
(builder: any) => {
|
||||
builder.modify('itemsTotals');
|
||||
builder.modify('INDirection');
|
||||
|
||||
// Filter the inventory items only.
|
||||
builder.whereIn('itemId', inventoryItemsIds);
|
||||
|
||||
// Filter the date range of the sheet.
|
||||
builder.modify('filterDateRange', filter.fromDate, filter.toDate)
|
||||
}
|
||||
);
|
||||
|
||||
const purchasesByItemsInstance = new PurchasesByItems(
|
||||
filter,
|
||||
inventoryItems,
|
||||
inventoryTransactions,
|
||||
tenant.metadata.baseCurrency
|
||||
);
|
||||
const purchasesByItemsData = purchasesByItemsInstance.reportData();
|
||||
|
||||
return {
|
||||
data: purchasesByItemsData,
|
||||
query: filter,
|
||||
meta: this.reportMetadata(tenantId),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { get, sumBy } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
import { allPassedConditionsPass, transformToMap } from 'utils';
|
||||
import {
|
||||
ISalesByItemsReportQuery,
|
||||
IAccountTransaction,
|
||||
ISalesByItemsItem,
|
||||
ISalesByItemsTotal,
|
||||
ISalesByItemsSheetStatement,
|
||||
IItem,
|
||||
} from '@/interfaces';
|
||||
|
||||
export default class SalesByItemsReport extends FinancialSheet {
|
||||
readonly baseCurrency: string;
|
||||
readonly items: IItem[];
|
||||
readonly itemsTransactions: Map<number, IAccountTransaction>;
|
||||
readonly query: ISalesByItemsReportQuery;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {ISalesByItemsReportQuery} query
|
||||
* @param {IItem[]} items
|
||||
* @param {IAccountTransaction[]} itemsTransactions
|
||||
* @param {string} baseCurrency
|
||||
*/
|
||||
constructor(
|
||||
query: ISalesByItemsReportQuery,
|
||||
items: IItem[],
|
||||
itemsTransactions: IAccountTransaction[],
|
||||
baseCurrency: string
|
||||
) {
|
||||
super();
|
||||
|
||||
this.baseCurrency = baseCurrency;
|
||||
this.items = items;
|
||||
this.itemsTransactions = transformToMap(itemsTransactions, 'itemId');
|
||||
this.query = query;
|
||||
this.numberFormat = this.query.numberFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the item purchase item, cost and average cost price.
|
||||
* @param {number} itemId - Item id.
|
||||
*/
|
||||
getItemTransaction(itemId: number): {
|
||||
quantity: number;
|
||||
cost: number;
|
||||
average: number;
|
||||
} {
|
||||
const transaction = this.itemsTransactions.get(itemId);
|
||||
|
||||
const quantity = get(transaction, 'quantity', 0);
|
||||
const cost = get(transaction, 'cost', 0);
|
||||
|
||||
const average = cost / quantity;
|
||||
|
||||
return { quantity, cost, average };
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping the given item section.
|
||||
* @param {ISalesByItemsItem} item
|
||||
* @returns
|
||||
*/
|
||||
private itemSectionMapper = (item: IItem): ISalesByItemsItem => {
|
||||
const meta = this.getItemTransaction(item.id);
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
code: item.code,
|
||||
quantitySold: meta.quantity,
|
||||
soldCost: meta.cost,
|
||||
averageSellPrice: meta.average,
|
||||
quantitySoldFormatted: this.formatNumber(meta.quantity, {
|
||||
money: false,
|
||||
}),
|
||||
soldCostFormatted: this.formatNumber(meta.cost),
|
||||
averageSellPriceFormatted: this.formatNumber(meta.average),
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether the given sale node is has transactions.
|
||||
* @param {ISalesByItemsItem} node -
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private filterSaleNoneTransactions = (node: ISalesByItemsItem) => {
|
||||
return this.itemsTransactions.get(node.id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether the given sale by item node is active.
|
||||
* @param {ISalesByItemsItem} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private filterSaleOnlyActive = (node: ISalesByItemsItem): boolean => {
|
||||
return node.quantitySold !== 0 || node.soldCost !== 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters sales by items nodes based on the report query.
|
||||
* @param {ISalesByItemsItem} saleItem -
|
||||
* @return {boolean}
|
||||
*/
|
||||
private itemSaleFilter = (saleItem: ISalesByItemsItem): boolean => {
|
||||
const { noneTransactions, onlyActive } = this.query;
|
||||
|
||||
const conditions = [
|
||||
[noneTransactions, this.filterSaleNoneTransactions],
|
||||
[onlyActive, this.filterSaleOnlyActive],
|
||||
];
|
||||
return allPassedConditionsPass(conditions)(saleItem);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mappes the given items to sales by items nodes.
|
||||
* @param {IItem[]} items -
|
||||
* @returns {ISalesByItemsItem[]}
|
||||
*/
|
||||
private itemsMapper = (items: IItem[]): ISalesByItemsItem[] => {
|
||||
return items.map(this.itemSectionMapper);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters sales by items sections.
|
||||
* @param items
|
||||
* @returns
|
||||
*/
|
||||
private itemsFilters = (nodes: ISalesByItemsItem[]): ISalesByItemsItem[] => {
|
||||
return nodes.filter(this.itemSaleFilter);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the items sections.
|
||||
* @returns {ISalesByItemsItem[]}
|
||||
*/
|
||||
private itemsSection(): ISalesByItemsItem[] {
|
||||
return R.compose(this.itemsFilters, this.itemsMapper)(this.items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the total section of the sheet.
|
||||
* @param {IInventoryValuationItem[]} items
|
||||
* @returns {IInventoryValuationTotal}
|
||||
*/
|
||||
totalSection(items: ISalesByItemsItem[]): ISalesByItemsTotal {
|
||||
const quantitySold = sumBy(items, (item) => item.quantitySold);
|
||||
const soldCost = sumBy(items, (item) => item.soldCost);
|
||||
|
||||
return {
|
||||
quantitySold,
|
||||
soldCost,
|
||||
quantitySoldFormatted: this.formatTotalNumber(quantitySold, {
|
||||
money: false,
|
||||
}),
|
||||
soldCostFormatted: this.formatTotalNumber(soldCost),
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the sheet data.
|
||||
* @returns {ISalesByItemsSheetStatement}
|
||||
*/
|
||||
reportData(): ISalesByItemsSheetStatement {
|
||||
const items = this.itemsSection();
|
||||
const total = this.totalSection(items);
|
||||
|
||||
return items.length > 0 ? { items, total } : {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
ISalesByItemsReportQuery,
|
||||
ISalesByItemsSheetStatement,
|
||||
ISalesByItemsSheetMeta
|
||||
} from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import SalesByItems from './SalesByItems';
|
||||
import { Tenant } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export default class SalesByItemsReportService {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Defaults balance sheet filter query.
|
||||
* @return {IBalanceSheetQuery}
|
||||
*/
|
||||
get defaultQuery(): ISalesByItemsReportQuery {
|
||||
return {
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
itemsIds: [],
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
divideOn1000: false,
|
||||
showZero: false,
|
||||
formatMoney: 'always',
|
||||
negativeFormat: 'mines',
|
||||
},
|
||||
noneTransactions: true,
|
||||
onlyActive: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the balance sheet meta.
|
||||
* @param {number} tenantId -
|
||||
* @returns {IBalanceSheetMeta}
|
||||
*/
|
||||
reportMetadata(tenantId: number): ISalesByItemsSheetMeta {
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
|
||||
const organizationName = settings.get({
|
||||
group: 'organization',
|
||||
key: 'name',
|
||||
});
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
|
||||
return {
|
||||
organizationName,
|
||||
baseCurrency,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve balance sheet statement.
|
||||
* -------------
|
||||
* @param {number} tenantId
|
||||
* @param {IBalanceSheetQuery} query
|
||||
*
|
||||
* @return {IBalanceSheetStatement}
|
||||
*/
|
||||
public async salesByItems(
|
||||
tenantId: number,
|
||||
query: ISalesByItemsReportQuery
|
||||
): Promise<{
|
||||
data: ISalesByItemsSheetStatement,
|
||||
query: ISalesByItemsReportQuery,
|
||||
meta: ISalesByItemsSheetMeta,
|
||||
}> {
|
||||
const { Item, InventoryTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
const tenant = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
this.logger.info('[sales_by_items] trying to calculate the report.', {
|
||||
filter,
|
||||
tenantId,
|
||||
});
|
||||
// Inventory items for sales report.
|
||||
const inventoryItems = await Item.query().onBuild((q) => {
|
||||
q.where('type', 'inventory');
|
||||
|
||||
if (filter.itemsIds.length > 0) {
|
||||
q.whereIn('id', filter.itemsIds);
|
||||
}
|
||||
});
|
||||
const inventoryItemsIds = inventoryItems.map((item) => item.id);
|
||||
|
||||
// Calculates the total inventory total quantity and rate `IN` transactions.
|
||||
const inventoryTransactions = await InventoryTransaction.query().onBuild(
|
||||
(builder: any) => {
|
||||
builder.modify('itemsTotals');
|
||||
builder.modify('OUTDirection');
|
||||
|
||||
// Filter the inventory items only.
|
||||
builder.whereIn('itemId', inventoryItemsIds);
|
||||
|
||||
// Filter the date range of the sheet.
|
||||
builder.modify('filterDateRange', filter.fromDate, filter.toDate)
|
||||
}
|
||||
);
|
||||
|
||||
const purchasesByItemsInstance = new SalesByItems(
|
||||
filter,
|
||||
inventoryItems,
|
||||
inventoryTransactions,
|
||||
tenant.metadata.baseCurrency,
|
||||
);
|
||||
const purchasesByItemsData = purchasesByItemsInstance.reportData();
|
||||
|
||||
return {
|
||||
data: purchasesByItemsData,
|
||||
query: filter,
|
||||
meta: this.reportMetadata(tenantId),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import { sumBy, defaultTo } from 'lodash';
|
||||
import {
|
||||
ITransactionsByContactsTransaction,
|
||||
ITransactionsByContactsAmount,
|
||||
ITransactionsByContactsFilter,
|
||||
ITransactionsByContactsContact,
|
||||
IContact,
|
||||
ILedger,
|
||||
ILedgerEntry,
|
||||
} from '@/interfaces';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
import { allPassedConditionsPass } from 'utils';
|
||||
|
||||
export default class TransactionsByContact extends FinancialSheet {
|
||||
readonly contacts: IContact[];
|
||||
readonly ledger: ILedger;
|
||||
readonly filter: ITransactionsByContactsFilter;
|
||||
readonly accountsGraph: any;
|
||||
|
||||
/**
|
||||
* Customer transaction mapper.
|
||||
* @param {any} transaction -
|
||||
* @return {Omit<ITransactionsByContactsTransaction, 'runningBalance'>}
|
||||
*/
|
||||
protected contactTransactionMapper(
|
||||
entry: ILedgerEntry
|
||||
): Omit<ITransactionsByContactsTransaction, 'runningBalance'> {
|
||||
const account = this.accountsGraph.getNodeData(entry.accountId);
|
||||
const currencyCode = this.baseCurrency;
|
||||
|
||||
return {
|
||||
credit: this.getContactAmount(entry.credit, currencyCode),
|
||||
debit: this.getContactAmount(entry.debit, currencyCode),
|
||||
accountName: account.name,
|
||||
currencyCode: this.baseCurrency,
|
||||
transactionNumber: entry.transactionNumber,
|
||||
transactionType: this.i18n.__(entry.referenceTypeFormatted),
|
||||
date: entry.date,
|
||||
createdAt: entry.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer transactions mapper with running balance.
|
||||
* @param {number} openingBalance
|
||||
* @param {ITransactionsByContactsTransaction[]} transactions
|
||||
* @returns {ITransactionsByContactsTransaction[]}
|
||||
*/
|
||||
protected contactTransactionRunningBalance(
|
||||
openingBalance: number,
|
||||
accountNormal: 'credit' | 'debit',
|
||||
transactions: Omit<ITransactionsByContactsTransaction, 'runningBalance'>[]
|
||||
): any {
|
||||
let _openingBalance = openingBalance;
|
||||
|
||||
return transactions.map(
|
||||
(transaction: ITransactionsByContactsTransaction) => {
|
||||
_openingBalance +=
|
||||
accountNormal === 'debit'
|
||||
? transaction.debit.amount
|
||||
: -1 * transaction.debit.amount;
|
||||
|
||||
_openingBalance +=
|
||||
accountNormal === 'credit'
|
||||
? transaction.credit.amount
|
||||
: -1 * transaction.credit.amount;
|
||||
|
||||
const runningBalance = this.getTotalAmountMeta(
|
||||
_openingBalance,
|
||||
transaction.currencyCode
|
||||
);
|
||||
return { ...transaction, runningBalance };
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the customer closing balance from the given transactions and opening balance.
|
||||
* @param {number} customerTransactions
|
||||
* @param {number} openingBalance
|
||||
* @returns {number}
|
||||
*/
|
||||
protected getContactClosingBalance(
|
||||
customerTransactions: ITransactionsByContactsTransaction[],
|
||||
contactNormal: 'credit' | 'debit',
|
||||
openingBalance: number
|
||||
): number {
|
||||
const closingBalance = openingBalance;
|
||||
|
||||
const totalCredit = sumBy(customerTransactions, 'credit.amount');
|
||||
const totalDebit = sumBy(customerTransactions, 'debit.amount');
|
||||
|
||||
const total =
|
||||
contactNormal === 'debit'
|
||||
? totalDebit - totalCredit
|
||||
: totalCredit - totalDebit;
|
||||
|
||||
return closingBalance + total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the given customer opening balance from the given customer id.
|
||||
* @param {number} customerId
|
||||
* @returns {number}
|
||||
*/
|
||||
protected getContactOpeningBalance(customerId: number): number {
|
||||
const openingBalanceLedger = this.ledger
|
||||
.whereContactId(customerId)
|
||||
.whereToDate(this.filter.fromDate);
|
||||
|
||||
// Retrieve the closing balance of the ledger.
|
||||
const openingBalance = openingBalanceLedger.getClosingBalance();
|
||||
|
||||
return defaultTo(openingBalance, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the customer amount format meta.
|
||||
* @param {number} amount
|
||||
* @param {string} currencyCode
|
||||
* @returns {ITransactionsByContactsAmount}
|
||||
*/
|
||||
protected getContactAmount(
|
||||
amount: number,
|
||||
currencyCode: string
|
||||
): ITransactionsByContactsAmount {
|
||||
return {
|
||||
amount,
|
||||
formattedAmount: this.formatNumber(amount, { currencyCode }),
|
||||
currencyCode,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the contact total amount format meta.
|
||||
* @param {number} amount - Amount.
|
||||
* @param {string} currencyCode - Currency code./
|
||||
* @returns {ITransactionsByContactsAmount}
|
||||
*/
|
||||
protected getTotalAmountMeta(amount: number, currencyCode: string) {
|
||||
return {
|
||||
amount,
|
||||
formattedAmount: this.formatTotalNumber(amount, { currencyCode }),
|
||||
currencyCode,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter customer section that has no transactions.
|
||||
* @param {ITransactionsByCustomersCustomer} transactionsByCustomer
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private filterContactByNoneTransaction = (
|
||||
transactionsByContact: ITransactionsByContactsContact
|
||||
): boolean => {
|
||||
return transactionsByContact.transactions.length > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters customer section has zero closing balnace.
|
||||
* @param {ITransactionsByCustomersCustomer} transactionsByCustomer
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private filterContactNoneZero = (
|
||||
transactionsByContact: ITransactionsByContactsContact
|
||||
): boolean => {
|
||||
return transactionsByContact.closingBalance.amount !== 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters the given customer node;
|
||||
* @param {ICustomerBalanceSummaryCustomer} customer
|
||||
*/
|
||||
private contactNodeFilter = (node: ITransactionsByContactsContact) => {
|
||||
const { noneTransactions, noneZero } = this.filter;
|
||||
|
||||
// Conditions pair filter detarminer.
|
||||
const condsPairFilters = [
|
||||
[noneTransactions, this.filterContactByNoneTransaction],
|
||||
[noneZero, this.filterContactNoneZero],
|
||||
];
|
||||
return allPassedConditionsPass(condsPairFilters)(node);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters the given customers nodes.
|
||||
* @param {ICustomerBalanceSummaryCustomer[]} nodes
|
||||
* @returns {ICustomerBalanceSummaryCustomer[]}
|
||||
*/
|
||||
protected contactsFilter = (
|
||||
nodes: ITransactionsByContactsContact[]
|
||||
): ITransactionsByContactsContact[] => {
|
||||
return nodes.filter(this.contactNodeFilter);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import moment from 'moment';
|
||||
import * as R from 'ramda';
|
||||
import { tableMapper, tableRowMapper } from 'utils';
|
||||
import { ITransactionsByContactsContact, ITableRow } from '@/interfaces';
|
||||
|
||||
enum ROW_TYPE {
|
||||
OPENING_BALANCE = 'OPENING_BALANCE',
|
||||
CLOSING_BALANCE = 'CLOSING_BALANCE',
|
||||
TRANSACTION = 'TRANSACTION',
|
||||
CUSTOMER = 'CUSTOMER',
|
||||
}
|
||||
|
||||
export default class TransactionsByContactsTableRows {
|
||||
private dateAccessor = (value): string => {
|
||||
return moment(value.date).format('YYYY MMM DD');
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the table rows of contact transactions.
|
||||
* @param {ITransactionsByCustomersCustomer} contact
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
protected contactTransactions = (
|
||||
contact: ITransactionsByContactsContact
|
||||
): ITableRow[] => {
|
||||
const columns = [
|
||||
{ key: 'date', accessor: this.dateAccessor },
|
||||
{ key: 'account', accessor: 'accountName' },
|
||||
{ key: 'transactionType', accessor: 'transactionType' },
|
||||
{ key: 'transactionNumber', accessor: 'transactionNumber' },
|
||||
{ key: 'credit', accessor: 'credit.formattedAmount' },
|
||||
{ key: 'debit', accessor: 'debit.formattedAmount' },
|
||||
{ key: 'runningBalance', accessor: 'runningBalance.formattedAmount' },
|
||||
];
|
||||
return tableMapper(contact.transactions, columns, {
|
||||
rowTypes: [ROW_TYPE.TRANSACTION],
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the table row of contact opening balance.
|
||||
* @param {ITransactionsByCustomersCustomer} contact
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
protected contactOpeningBalance = (
|
||||
contact: ITransactionsByContactsContact
|
||||
): ITableRow => {
|
||||
const columns = [
|
||||
{ key: 'openingBalanceLabel', value: this.i18n.__('Opening balance') },
|
||||
...R.repeat({ key: 'empty', value: '' }, 5),
|
||||
{
|
||||
key: 'openingBalanceValue',
|
||||
accessor: 'openingBalance.formattedAmount',
|
||||
},
|
||||
];
|
||||
return tableRowMapper(contact, columns, {
|
||||
rowTypes: [ROW_TYPE.OPENING_BALANCE],
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the table row of contact closing balance.
|
||||
* @param {ITransactionsByCustomersCustomer} contact -
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
protected contactClosingBalance = (
|
||||
contact: ITransactionsByContactsContact
|
||||
): ITableRow => {
|
||||
const columns = [
|
||||
{ key: 'closingBalanceLabel', value: this.i18n.__('Closing balance') },
|
||||
...R.repeat({ key: 'empty', value: '' }, 5),
|
||||
{
|
||||
key: 'closingBalanceValue',
|
||||
accessor: 'closingBalance.formattedAmount',
|
||||
},
|
||||
];
|
||||
return tableRowMapper(contact, columns, {
|
||||
rowTypes: [ROW_TYPE.CLOSING_BALANCE],
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
ITransactionsByCustomersTransaction,
|
||||
ITransactionsByCustomersFilter,
|
||||
ITransactionsByCustomersCustomer,
|
||||
ITransactionsByCustomersData,
|
||||
INumberFormatQuery,
|
||||
ICustomer,
|
||||
} from '@/interfaces';
|
||||
import TransactionsByContact from '../TransactionsByContact/TransactionsByContact';
|
||||
import Ledger from '@/services/Accounting/Ledger';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
const CUSTOMER_NORMAL = 'debit';
|
||||
|
||||
export default class TransactionsByCustomers extends TransactionsByContact {
|
||||
readonly customers: ICustomer[];
|
||||
readonly ledger: Ledger;
|
||||
readonly filter: ITransactionsByCustomersFilter;
|
||||
readonly baseCurrency: string;
|
||||
readonly numberFormat: INumberFormatQuery;
|
||||
readonly accountsGraph: any;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {ICustomer} customers
|
||||
* @param {Map<number, IAccountTransaction[]>} transactionsLedger
|
||||
* @param {string} baseCurrency
|
||||
*/
|
||||
constructor(
|
||||
customers: ICustomer[],
|
||||
accountsGraph: any,
|
||||
ledger: Ledger,
|
||||
filter: ITransactionsByCustomersFilter,
|
||||
baseCurrency: string,
|
||||
i18n
|
||||
) {
|
||||
super();
|
||||
|
||||
this.customers = customers;
|
||||
this.accountsGraph = accountsGraph;
|
||||
this.ledger = ledger;
|
||||
this.baseCurrency = baseCurrency;
|
||||
this.filter = filter;
|
||||
this.numberFormat = this.filter.numberFormat;
|
||||
this.i18n = i18n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the customer transactions from the given customer id and opening balance.
|
||||
* @param {number} customerId - Customer id.
|
||||
* @param {number} openingBalance - Opening balance amount.
|
||||
* @returns {ITransactionsByCustomersTransaction[]}
|
||||
*/
|
||||
private customerTransactions(
|
||||
customerId: number,
|
||||
openingBalance: number
|
||||
): ITransactionsByCustomersTransaction[] {
|
||||
const ledger = this.ledger
|
||||
.whereContactId(customerId)
|
||||
.whereFromDate(this.filter.fromDate)
|
||||
.whereToDate(this.filter.toDate);
|
||||
|
||||
const ledgerEntries = ledger.getEntries();
|
||||
|
||||
return R.compose(
|
||||
R.curry(this.contactTransactionRunningBalance)(openingBalance, 'debit'),
|
||||
R.map(this.contactTransactionMapper.bind(this))
|
||||
).bind(this)(ledgerEntries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer section mapper.
|
||||
* @param {ICustomer} customer
|
||||
* @returns {ITransactionsByCustomersCustomer}
|
||||
*/
|
||||
private customerMapper(
|
||||
customer: ICustomer
|
||||
): ITransactionsByCustomersCustomer {
|
||||
const openingBalance = this.getContactOpeningBalance(customer.id);
|
||||
const transactions = this.customerTransactions(customer.id, openingBalance);
|
||||
const closingBalance = this.getCustomerClosingBalance(
|
||||
transactions,
|
||||
openingBalance
|
||||
);
|
||||
const currencyCode = this.baseCurrency;
|
||||
|
||||
return {
|
||||
customerName: customer.displayName,
|
||||
openingBalance: this.getTotalAmountMeta(openingBalance, currencyCode),
|
||||
closingBalance: this.getTotalAmountMeta(closingBalance, currencyCode),
|
||||
transactions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the vendor closing balance from the given customer transactions.
|
||||
* @param {ITransactionsByContactsTransaction[]} customerTransactions
|
||||
* @param {number} openingBalance
|
||||
* @returns
|
||||
*/
|
||||
private getCustomerClosingBalance(
|
||||
customerTransactions: ITransactionsByCustomersTransaction[],
|
||||
openingBalance: number
|
||||
): number {
|
||||
return this.getContactClosingBalance(
|
||||
customerTransactions,
|
||||
CUSTOMER_NORMAL,
|
||||
openingBalance
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the customers post filter is active.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private isCustomersPostFilter = () => {
|
||||
return isEmpty(this.filter.customersIds);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the customers sections of the report.
|
||||
* @param {ICustomer[]} customers
|
||||
* @returns {ITransactionsByCustomersCustomer[]}
|
||||
*/
|
||||
private customersMapper(
|
||||
customers: ICustomer[]
|
||||
): ITransactionsByCustomersCustomer[] {
|
||||
return R.compose(
|
||||
R.when(this.isCustomersPostFilter, this.contactsFilter),
|
||||
R.map(this.customerMapper.bind(this))
|
||||
).bind(this)(customers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the report data.
|
||||
* @returns {ITransactionsByCustomersData}
|
||||
*/
|
||||
public reportData(): ITransactionsByCustomersData {
|
||||
return this.customersMapper(this.customers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the report columns.
|
||||
*/
|
||||
public reportColumns() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { isEmpty, 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, customersIds?: number[]) {
|
||||
const { Customer } = this.tenancy.models(tenantId);
|
||||
|
||||
return Customer.query().onBuild((q) => {
|
||||
q.orderBy('displayName');
|
||||
|
||||
if (!isEmpty(customersIds)) {
|
||||
q.whereIn('id', customersIds);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import { Inject } from 'typedi';
|
||||
import * as R from 'ramda';
|
||||
import moment from 'moment';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import {
|
||||
ITransactionsByCustomersService,
|
||||
ITransactionsByCustomersFilter,
|
||||
ITransactionsByCustomersStatement,
|
||||
ILedgerEntry,
|
||||
} from '@/interfaces';
|
||||
import TransactionsByCustomers from './TransactionsByCustomers';
|
||||
import Ledger from '@/services/Accounting/Ledger';
|
||||
import TransactionsByCustomersRepository from './TransactionsByCustomersRepository';
|
||||
import { Tenant } from '@/system/models';
|
||||
|
||||
export default class TransactionsByCustomersService
|
||||
implements ITransactionsByCustomersService
|
||||
{
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
reportRepository: TransactionsByCustomersRepository;
|
||||
|
||||
/**
|
||||
* Defaults balance sheet filter query.
|
||||
* @return {ICustomerBalanceSummaryQuery}
|
||||
*/
|
||||
get defaultQuery(): ITransactionsByCustomersFilter {
|
||||
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',
|
||||
},
|
||||
comparison: {
|
||||
percentageOfColumn: true,
|
||||
},
|
||||
noneZero: false,
|
||||
noneTransactions: true,
|
||||
customersIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the customers opening balance ledger entries.
|
||||
* @param {number} tenantId
|
||||
* @param {Date} openingDate
|
||||
* @param {number[]} customersIds
|
||||
* @returns {Promise<ILedgerEntry[]>}
|
||||
*/
|
||||
private async getCustomersOpeningBalanceEntries(
|
||||
tenantId: number,
|
||||
openingDate: Date,
|
||||
customersIds?: number[]
|
||||
): Promise<ILedgerEntry[]> {
|
||||
const openingTransactions =
|
||||
await this.reportRepository.getCustomersOpeningBalanceTransactions(
|
||||
tenantId,
|
||||
openingDate,
|
||||
customersIds
|
||||
);
|
||||
|
||||
return R.compose(
|
||||
R.map(R.assoc('date', openingDate)),
|
||||
R.map(R.assoc('accountNormal', 'debit'))
|
||||
)(openingTransactions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the customers periods ledger entries.
|
||||
* @param {number} tenantId
|
||||
* @param {Date} fromDate
|
||||
* @param {Date} toDate
|
||||
* @returns {Promise<ILedgerEntry[]>}
|
||||
*/
|
||||
private async getCustomersPeriodsEntries(
|
||||
tenantId: number,
|
||||
fromDate: Date | string,
|
||||
toDate: Date | string
|
||||
): Promise<ILedgerEntry[]> {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve transactions by by the customers.
|
||||
* @param {number} tenantId
|
||||
* @param {ITransactionsByCustomersFilter} query
|
||||
* @return {Promise<ITransactionsByCustomersStatement>}
|
||||
*/
|
||||
public async transactionsByCustomers(
|
||||
tenantId: number,
|
||||
query: ITransactionsByCustomersFilter
|
||||
): Promise<ITransactionsByCustomersStatement> {
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
|
||||
// Retrieve tenant information.
|
||||
const tenant = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
// Retrieve the report customers.
|
||||
const customers = await this.reportRepository.getCustomers(
|
||||
tenantId,
|
||||
filter.customersIds
|
||||
);
|
||||
|
||||
const openingBalanceDate = moment(filter.fromDate)
|
||||
.subtract(1, 'days')
|
||||
.toDate();
|
||||
|
||||
// Retrieve all ledger transactions of the opening balance of.
|
||||
const openingBalanceEntries = await this.getCustomersOpeningBalanceEntries(
|
||||
tenantId,
|
||||
openingBalanceDate
|
||||
);
|
||||
// Retrieve all ledger transactions between opeing and closing period.
|
||||
const customersTransactions = await this.getCustomersPeriodsEntries(
|
||||
tenantId,
|
||||
query.fromDate,
|
||||
query.toDate
|
||||
);
|
||||
// Concats the opening balance and period customer ledger transactions.
|
||||
const journalTransactions = [
|
||||
...openingBalanceEntries,
|
||||
...customersTransactions,
|
||||
];
|
||||
const journal = new Ledger(journalTransactions);
|
||||
|
||||
// Transactions by customers data mapper.
|
||||
const reportInstance = new TransactionsByCustomers(
|
||||
customers,
|
||||
accountsGraph,
|
||||
journal,
|
||||
filter,
|
||||
tenant.metadata.baseCurrency,
|
||||
i18n
|
||||
);
|
||||
|
||||
return {
|
||||
data: reportInstance.reportData(),
|
||||
columns: reportInstance.reportColumns(),
|
||||
query: filter,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import * as R from 'ramda';
|
||||
import { tableRowMapper, tableMapper } from 'utils';
|
||||
import { ITransactionsByCustomersCustomer, ITableRow } from '@/interfaces';
|
||||
import TransactionsByContactsTableRows from '../TransactionsByContact/TransactionsByContactTableRows';
|
||||
|
||||
enum ROW_TYPE {
|
||||
OPENING_BALANCE = 'OPENING_BALANCE',
|
||||
CLOSING_BALANCE = 'CLOSING_BALANCE',
|
||||
TRANSACTION = 'TRANSACTION',
|
||||
CUSTOMER = 'CUSTOMER',
|
||||
}
|
||||
|
||||
export default class TransactionsByCustomersTableRows extends TransactionsByContactsTableRows {
|
||||
private customersTransactions: ITransactionsByCustomersCustomer[];
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {ITransactionsByCustomersCustomer[]} customersTransactions - Customers transactions.
|
||||
*/
|
||||
constructor(
|
||||
customersTransactions: ITransactionsByCustomersCustomer[],
|
||||
i18n
|
||||
) {
|
||||
super();
|
||||
this.customersTransactions = customersTransactions;
|
||||
this.i18n = i18n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the table row of customer details.
|
||||
* @param {ITransactionsByCustomersCustomer} customer -
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
private customerDetails = (customer: ITransactionsByCustomersCustomer) => {
|
||||
const columns = [
|
||||
{ key: 'customerName', accessor: 'customerName' },
|
||||
...R.repeat({ key: 'empty', value: '' }, 5),
|
||||
{
|
||||
key: 'closingBalanceValue',
|
||||
accessor: 'closingBalance.formattedAmount',
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
...tableRowMapper(customer, columns, { rowTypes: [ROW_TYPE.CUSTOMER] }),
|
||||
children: R.pipe(
|
||||
R.when(
|
||||
R.always(customer.transactions.length > 0),
|
||||
R.pipe(
|
||||
R.concat(this.contactTransactions(customer)),
|
||||
R.prepend(this.contactOpeningBalance(customer))
|
||||
)
|
||||
),
|
||||
R.append(this.contactClosingBalance(customer))
|
||||
)([]),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the table rows of the customer section.
|
||||
* @param {ITransactionsByCustomersCustomer} customer
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
private customerRowsMapper = (customer: ITransactionsByCustomersCustomer) => {
|
||||
return R.pipe(this.customerDetails)(customer);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the table rows of transactions by customers report.
|
||||
* @param {ITransactionsByCustomersCustomer[]} customers
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
public tableRows = (): ITableRow[] => {
|
||||
return R.map(this.customerRowsMapper.bind(this))(
|
||||
this.customersTransactions
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
IAccount,
|
||||
IAccountTransaction,
|
||||
INumberFormatQuery,
|
||||
ITransactionsByReferenceQuery,
|
||||
ITransactionsByReferenceTransaction,
|
||||
} from '@/interfaces';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
|
||||
export default class TransactionsByReference extends FinancialSheet {
|
||||
readonly transactions: IAccountTransaction[];
|
||||
readonly query: ITransactionsByReferenceQuery;
|
||||
readonly baseCurrency: string;
|
||||
readonly numberFormat: INumberFormatQuery;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {IAccountTransaction[]} transactions
|
||||
* @param {ITransactionsByReferenceQuery} query
|
||||
* @param {string} baseCurrency
|
||||
*/
|
||||
constructor(
|
||||
transactions: (IAccountTransaction & { account: IAccount }) [],
|
||||
query: ITransactionsByReferenceQuery,
|
||||
baseCurrency: string
|
||||
) {
|
||||
super();
|
||||
|
||||
this.transactions = transactions;
|
||||
this.query = query;
|
||||
this.baseCurrency = baseCurrency;
|
||||
this.numberFormat = this.query.numberFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappes the given account transaction to report transaction.
|
||||
* @param {IAccountTransaction} transaction
|
||||
* @returns {ITransactionsByReferenceTransaction}
|
||||
*/
|
||||
private transactionMapper = (
|
||||
transaction: IAccountTransaction
|
||||
): ITransactionsByReferenceTransaction => {
|
||||
return {
|
||||
date: this.getDateMeta(transaction.date),
|
||||
|
||||
credit: this.getAmountMeta(transaction.credit, { money: false }),
|
||||
debit: this.getAmountMeta(transaction.debit, { money: false }),
|
||||
|
||||
referenceTypeFormatted: transaction.referenceTypeFormatted,
|
||||
referenceType: transaction.referenceType,
|
||||
referenceId: transaction.referenceId,
|
||||
|
||||
contactId: transaction.contactId,
|
||||
contactType: transaction.contactType,
|
||||
contactTypeFormatted: transaction.contactType,
|
||||
|
||||
accountName: transaction.account.name,
|
||||
accountCode: transaction.account.code,
|
||||
accountId: transaction.accountId,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Mappes the given accounts transactions to report transactions.
|
||||
* @param {IAccountTransaction} transaction
|
||||
* @returns {ITransactionsByReferenceTransaction}
|
||||
*/
|
||||
private transactionsMapper = (
|
||||
transactions: IAccountTransaction[]
|
||||
): ITransactionsByReferenceTransaction[] => {
|
||||
return transactions.map(this.transactionMapper);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the report data.
|
||||
* @returns {ITransactionsByReferenceTransaction}
|
||||
*/
|
||||
public reportData(): ITransactionsByReferenceTransaction[] {
|
||||
return this.transactionsMapper(this.transactions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { IAccount, IAccountTransaction, ITransactionsByReferenceQuery } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class TransactionsByReferenceRepository {
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Retrieve the accounts transactions of the givne reference id and type.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} referenceId - Reference id.
|
||||
* @param {string} referenceType - Reference type.
|
||||
* @return {Promise<IAccountTransaction[]>}
|
||||
*/
|
||||
public getTransactions(
|
||||
tenantId: number,
|
||||
referenceId: number,
|
||||
referenceType: string,
|
||||
): Promise<(IAccountTransaction & { account: IAccount }) []> {
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
return AccountTransaction.query()
|
||||
.where('reference_id', referenceId)
|
||||
.where('reference_type', referenceType)
|
||||
.withGraphFetched('account');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import {
|
||||
ITransactionsByReferenceQuery,
|
||||
ITransactionsByReferenceTransaction,
|
||||
} from '@/interfaces';
|
||||
import TransactionsByReferenceRepository from './TransactionsByReferenceRepository';
|
||||
import TransactionsByReferenceReport from './TransactionsByReferenceReport';
|
||||
|
||||
@Service()
|
||||
export default class TransactionsByReferenceService {
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
reportRepository: TransactionsByReferenceRepository;
|
||||
|
||||
/**
|
||||
* Default query of transactions by reference report.
|
||||
*/
|
||||
get defaultQuery(): ITransactionsByReferenceQuery {
|
||||
return {
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
divideOn1000: false,
|
||||
showZero: false,
|
||||
formatMoney: 'total',
|
||||
negativeFormat: 'mines',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve accounts transactions by given reference id and type.
|
||||
* @param {number} tenantId
|
||||
* @param {ITransactionsByReferenceQuery} filter
|
||||
*/
|
||||
public async getTransactionsByReference(
|
||||
tenantId: number,
|
||||
query: ITransactionsByReferenceQuery
|
||||
): Promise<{
|
||||
transactions: ITransactionsByReferenceTransaction[];
|
||||
}> {
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
|
||||
// Retrieve the accounts transactions of the given reference.
|
||||
const transactions = await this.reportRepository.getTransactions(
|
||||
tenantId,
|
||||
filter.referenceId,
|
||||
filter.referenceType
|
||||
);
|
||||
|
||||
// Settings tenant service.
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
|
||||
// Transactions by reference report.
|
||||
const report = new TransactionsByReferenceReport(
|
||||
transactions,
|
||||
filter,
|
||||
baseCurrency
|
||||
);
|
||||
|
||||
return {
|
||||
transactions: report.reportData(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user