mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 05:10: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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user