mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
draft: AR and AP aging summary report.
This commit is contained in:
@@ -6,8 +6,8 @@ import TrialBalanceSheetController from './FinancialStatements/TrialBalanceSheet
|
||||
import GeneralLedgerController from './FinancialStatements/GeneralLedger';
|
||||
import JournalSheetController from './FinancialStatements/JournalSheet';
|
||||
import ProfitLossController from './FinancialStatements/ProfitLossSheet';
|
||||
import ReceivableAgingSummary from './FinancialStatements/ARAgingSummary';
|
||||
// import PayableAgingSummary from './FinancialStatements/PayableAgingSummary';
|
||||
import ARAgingSummary from './FinancialStatements/ARAgingSummary';
|
||||
import APAgingSummary from './FinancialStatements/APAgingSummary';
|
||||
|
||||
@Service()
|
||||
export default class FinancialStatementsService {
|
||||
@@ -22,8 +22,8 @@ export default class FinancialStatementsService {
|
||||
router.use('/general_ledger', Container.get(GeneralLedgerController).router());
|
||||
router.use('/trial_balance_sheet', Container.get(TrialBalanceSheetController).router());
|
||||
router.use('/journal', Container.get(JournalSheetController).router());
|
||||
router.use('/receivable_aging_summary', Container.get(ReceivableAgingSummary).router());
|
||||
// router.use('/payable_aging_summary', PayableAgingSummary.router());
|
||||
router.use('/receivable_aging_summary', Container.get(ARAgingSummary).router());
|
||||
router.use('/payable_aging_summary', Container.get(APAgingSummary).router());
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { query } from 'express-validator';
|
||||
import { Inject } from 'typedi';
|
||||
import BaseController from 'api/controllers/BaseController';
|
||||
import asyncMiddleware from 'api/middleware/asyncMiddleware';
|
||||
import APAgingSummaryReportService from 'services/FinancialStatements/AgingSummary/APAgingSummaryService';
|
||||
import { findPhoneNumbersInText } from 'libphonenumber-js';
|
||||
|
||||
export default class APAgingSummaryReportController extends BaseController {
|
||||
@Inject()
|
||||
APAgingSummaryService: APAgingSummaryReportService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
this.validationSchema,
|
||||
asyncMiddleware(this.payableAgingSummary.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema() {
|
||||
return [
|
||||
query('as_date').optional().isISO8601(),
|
||||
query('aging_days_before').optional().isNumeric().toInt(),
|
||||
query('aging_periods').optional().isNumeric().toInt(),
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.1000_divide').optional().isBoolean().toBoolean(),
|
||||
query('vendors_ids.*').isNumeric().toInt(),
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve payable aging summary report.
|
||||
*/
|
||||
async payableAgingSummary(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
const organizationName = settings.get({
|
||||
group: 'organization',
|
||||
key: 'name',
|
||||
});
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
|
||||
try {
|
||||
const {
|
||||
data,
|
||||
columns,
|
||||
query,
|
||||
} = await this.APAgingSummaryService.APAgingSummary(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
organization_name: organizationName,
|
||||
base_currency: baseCurrency,
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,8 +30,8 @@ export default class ARAgingSummaryReportController extends BaseController {
|
||||
get validationSchema() {
|
||||
return [
|
||||
query('as_date').optional().isISO8601(),
|
||||
query('aging_days_before').optional().isNumeric().toInt(),
|
||||
query('aging_periods').optional().isNumeric().toInt(),
|
||||
query('aging_days_before').optional().isInt({ max: 500 }).toInt(),
|
||||
query('aging_periods').optional().isInt({ max: 12 }).toInt(),
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.1000_divide').optional().isBoolean().toBoolean(),
|
||||
oneOf(
|
||||
@@ -49,18 +49,31 @@ export default class ARAgingSummaryReportController extends BaseController {
|
||||
* Retrieve receivable aging summary report.
|
||||
*/
|
||||
async receivableAgingSummary(req: Request, res: Response) {
|
||||
const { tenantId } = req;
|
||||
const { tenantId, settings } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
const organizationName = settings.get({
|
||||
group: 'organization',
|
||||
key: 'name',
|
||||
});
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
|
||||
try {
|
||||
const {
|
||||
data,
|
||||
columns
|
||||
columns,
|
||||
query,
|
||||
} = await this.ARAgingSummaryService.ARAgingSummary(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
organization_name: organizationName,
|
||||
base_currency: baseCurrency,
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import moment from 'moment';
|
||||
import { validationResult } from 'express-validator';
|
||||
import { omit, reverse } from 'lodash';
|
||||
import BaseController from 'api/controllers/BaseController';
|
||||
|
||||
export default class AgingReport extends BaseController{
|
||||
|
||||
/**
|
||||
* Express validator middleware.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static validateResults(req, res, next) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Array} agingPeriods
|
||||
* @param {Numeric} customerBalance
|
||||
*/
|
||||
static contactAgingBalance(agingPeriods, receivableTotalCredit) {
|
||||
let prevAging = 0;
|
||||
let receivableCredit = receivableTotalCredit;
|
||||
let diff = receivableCredit;
|
||||
|
||||
const periods = reverse(agingPeriods).map((agingPeriod) => {
|
||||
const agingAmount = (agingPeriod.closingBalance - prevAging);
|
||||
const subtract = Math.min(diff, agingAmount);
|
||||
diff -= Math.min(agingAmount, diff);
|
||||
|
||||
const total = Math.max(agingAmount - subtract, 0);
|
||||
|
||||
const output = {
|
||||
...omit(agingPeriod, ['closingBalance']),
|
||||
total,
|
||||
};
|
||||
prevAging = agingPeriod.closingBalance;
|
||||
return output;
|
||||
});
|
||||
return reverse(periods);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} asDay
|
||||
* @param {*} agingDaysBefore
|
||||
* @param {*} agingPeriodsFreq
|
||||
*/
|
||||
static agingRangePeriods(asDay, agingDaysBefore, agingPeriodsFreq) {
|
||||
const totalAgingDays = agingDaysBefore * agingPeriodsFreq;
|
||||
const startAging = moment(asDay).startOf('day');
|
||||
const endAging = startAging.clone().subtract('days', totalAgingDays).endOf('day');
|
||||
|
||||
const agingPeriods = [];
|
||||
const startingAging = startAging.clone();
|
||||
|
||||
let beforeDays = 1;
|
||||
let toDays = 0;
|
||||
|
||||
while (startingAging > endAging) {
|
||||
const currentAging = startingAging.clone();
|
||||
startingAging.subtract('days', agingDaysBefore).endOf('day');
|
||||
toDays += agingDaysBefore;
|
||||
|
||||
agingPeriods.push({
|
||||
from_period: moment(currentAging).toDate(),
|
||||
to_period: moment(startingAging).toDate(),
|
||||
before_days: beforeDays === 1 ? 0 : beforeDays,
|
||||
to_days: toDays,
|
||||
...(startingAging.valueOf() === endAging.valueOf()) ? {
|
||||
to_period: null,
|
||||
to_days: null,
|
||||
} : {},
|
||||
});
|
||||
beforeDays += agingDaysBefore;
|
||||
}
|
||||
return agingPeriods;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} filter
|
||||
*/
|
||||
static formatNumberClosure(filter) {
|
||||
return (balance) => {
|
||||
let formattedBalance = parseFloat(balance);
|
||||
|
||||
if (filter.no_cents) {
|
||||
formattedBalance = parseInt(formattedBalance, 10);
|
||||
}
|
||||
if (filter.divide_1000) {
|
||||
formattedBalance /= 1000;
|
||||
}
|
||||
return formattedBalance;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
|
||||
|
||||
export default class InventoryValuationSummary {
|
||||
|
||||
static router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/inventory_valuation_summary',
|
||||
asyncMiddleware(this.inventoryValuationSummary),
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
static inventoryValuationSummary(req, res) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
import express from 'express';
|
||||
import { query } from 'express-validator';
|
||||
import { difference } from 'lodash';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
import asyncMiddleware from 'api/middleware/asyncMiddleware';
|
||||
import AgingReport from 'api/controllers/FinancialStatements/AgingReport';
|
||||
import moment from 'moment';
|
||||
|
||||
export default class PayableAgingSummary extends AgingReport {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
static router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
this.payableAgingSummaryRoles(),
|
||||
this.validateResults,
|
||||
asyncMiddleware(this.validateVendorsIds.bind(this)),
|
||||
asyncMiddleware(this.payableAgingSummary.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the report vendors ids query.
|
||||
*/
|
||||
static async validateVendorsIds(req, res, next) {
|
||||
const { Vendor } = req.models;
|
||||
|
||||
const filter = {
|
||||
vendors_ids: [],
|
||||
...req.query,
|
||||
};
|
||||
if (!Array.isArray(filter.vendors_ids)) {
|
||||
filter.vendors_ids = [filter.vendors_ids];
|
||||
}
|
||||
if (filter.vendors_ids.length > 0) {
|
||||
const storedCustomers = await Vendor.query().whereIn(
|
||||
'id',
|
||||
filter.vendors_ids
|
||||
);
|
||||
const storedCustomersIds = storedCustomers.map((c) => c.id);
|
||||
const notStoredCustomersIds = difference(
|
||||
storedCustomersIds,
|
||||
filter,
|
||||
vendors_ids
|
||||
);
|
||||
if (notStoredCustomersIds.length) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'VENDORS.IDS.NOT.FOUND', code: 300 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Receivable aging summary validation roles.
|
||||
*/
|
||||
static payableAgingSummaryRoles() {
|
||||
return [
|
||||
query('as_date').optional().isISO8601(),
|
||||
query('aging_days_before').optional().isNumeric().toInt(),
|
||||
query('aging_periods').optional().isNumeric().toInt(),
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.1000_divide').optional().isBoolean().toBoolean(),
|
||||
query('vendors_ids.*').isNumeric().toInt(),
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve payable aging summary report.
|
||||
*/
|
||||
static async payableAgingSummary(req, res) {
|
||||
const { Customer, Account, AccountTransaction, AccountType } = req.models;
|
||||
const storedVendors = await Customer.query();
|
||||
|
||||
const filter = {
|
||||
as_date: moment().format('YYYY-MM-DD'),
|
||||
aging_days_before: 30,
|
||||
aging_periods: 3,
|
||||
number_format: {
|
||||
no_cents: false,
|
||||
divide_1000: false,
|
||||
},
|
||||
...req.query,
|
||||
};
|
||||
const accountsReceivableType = await AccountType.query()
|
||||
.where('key', 'accounts_payable')
|
||||
.first();
|
||||
|
||||
const accountsReceivable = await Account.query()
|
||||
.where('account_type_id', accountsReceivableType.id)
|
||||
.remember()
|
||||
.first();
|
||||
|
||||
const transactions = await AccountTransaction.query()
|
||||
.modify('filterDateRange', null, filter.as_date)
|
||||
.where('account_id', accountsReceivable.id)
|
||||
.remember();
|
||||
|
||||
const journalPoster = new JournalPoster();
|
||||
journalPoster.loadEntries(transactions);
|
||||
|
||||
const agingPeriods = this.agingRangePeriods(
|
||||
filter.as_date,
|
||||
filter.aging_days_before,
|
||||
filter.aging_periods
|
||||
);
|
||||
// Total amount formmatter based on the given query.
|
||||
const totalFormatter = formatNumberClosure(filter.number_format);
|
||||
|
||||
const vendors = storedVendors.map((vendor) => {
|
||||
// Calculate the trial balance total of the given vendor.
|
||||
const vendorBalance = journalPoster.getContactTrialBalance(
|
||||
accountsReceivable.id,
|
||||
vendor.id,
|
||||
'vendor'
|
||||
);
|
||||
const agingClosingBalance = agingPeriods.map((agingPeriod) => {
|
||||
// Calculate the trial balance between the given date period.
|
||||
const agingTrialBalance = journalPoster.getContactTrialBalance(
|
||||
accountsReceivable.id,
|
||||
vendor.id,
|
||||
'vendor',
|
||||
agingPeriod.from_period
|
||||
);
|
||||
return {
|
||||
...agingPeriod,
|
||||
closingBalance: agingTrialBalance.debit,
|
||||
};
|
||||
});
|
||||
const aging = this.contactAgingBalance(
|
||||
agingClosingBalance,
|
||||
vendorBalance.credit
|
||||
);
|
||||
return {
|
||||
vendor_name: vendor.displayName,
|
||||
aging: aging.map((item) => ({
|
||||
...item,
|
||||
formatted_total: totalFormatter(item.total),
|
||||
})),
|
||||
total: vendorBalance.balance,
|
||||
formatted_total: totalFormatted(vendorBalance.balance),
|
||||
};
|
||||
});
|
||||
|
||||
const agingClosingBalance = agingPeriods.map((agingPeriod) => {
|
||||
const closingTrialBalance = journalPoster.getContactTrialBalance(
|
||||
accountsReceivable.id,
|
||||
null,
|
||||
'vendor',
|
||||
agingPeriod.from_period
|
||||
);
|
||||
return {
|
||||
...agingPeriod,
|
||||
closingBalance: closingTrialBalance.balance,
|
||||
};
|
||||
});
|
||||
|
||||
const totalClosingBalance = journalPoster.getContactTrialBalance(
|
||||
accountsReceivable.id,
|
||||
null,
|
||||
'vendor'
|
||||
);
|
||||
const agingTotal = this.contactAgingBalance(
|
||||
agingClosingBalance,
|
||||
totalClosingBalance.credit
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
columns: [ ...agingPeriods ],
|
||||
aging: {
|
||||
vendors,
|
||||
total: [
|
||||
...agingTotal.map((item) => ({
|
||||
...item,
|
||||
formatted_total: totalFormatter(item.total),
|
||||
})),
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
29
server/src/interfaces/APAgingSummaryReport.ts
Normal file
29
server/src/interfaces/APAgingSummaryReport.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
IAgingPeriod,
|
||||
IAgingPeriodTotal
|
||||
} from './AgingReport';
|
||||
|
||||
export interface IAPAgingSummaryQuery {
|
||||
asDate: Date | string;
|
||||
agingDaysBefore: number;
|
||||
agingPeriods: number;
|
||||
numberFormat: {
|
||||
noCents: boolean;
|
||||
divideOn1000: boolean;
|
||||
};
|
||||
vendorsIds: number[];
|
||||
noneZero: boolean;
|
||||
}
|
||||
|
||||
export interface IAPAgingSummaryVendor {
|
||||
vendorName: string,
|
||||
aging: (IAgingPeriod & IAgingPeriodTotal)[],
|
||||
total: IAgingPeriodTotal,
|
||||
}
|
||||
|
||||
export interface IAPAgingSummaryData {
|
||||
vendors: IAPAgingSummaryVendor[],
|
||||
total: (IAgingPeriod & IAgingPeriodTotal)[],
|
||||
}
|
||||
|
||||
export type IAPAgingSummaryColumns = IAgingPeriod[];
|
||||
@@ -1,45 +1,29 @@
|
||||
|
||||
|
||||
import {
|
||||
IAgingPeriod,
|
||||
IAgingPeriodTotal
|
||||
} from './AgingReport';
|
||||
|
||||
export interface IARAgingSummaryQuery {
|
||||
asDate: Date | string,
|
||||
agingDaysBefore: number,
|
||||
agingPeriods: number,
|
||||
asDate: Date | string;
|
||||
agingDaysBefore: number;
|
||||
agingPeriods: number;
|
||||
numberFormat: {
|
||||
noCents: number,
|
||||
divideOn1000: number,
|
||||
},
|
||||
customersIds: number[],
|
||||
noneZero: boolean,
|
||||
noCents: number;
|
||||
divideOn1000: number;
|
||||
};
|
||||
customersIds: number[];
|
||||
noneZero: boolean;
|
||||
}
|
||||
|
||||
export interface IAgingPeriod {
|
||||
fromPeriod: Date,
|
||||
toPeriod: Date,
|
||||
beforeDays: number,
|
||||
toDays: number,
|
||||
};
|
||||
|
||||
export interface IAgingPeriodClosingBalance extends IAgingPeriod {
|
||||
closingBalance: number,
|
||||
};
|
||||
|
||||
export interface IAgingPeriodTotal extends IAgingPeriod {
|
||||
total: number,
|
||||
};
|
||||
|
||||
export interface ARAgingSummaryCustomerPeriod {
|
||||
|
||||
export interface IARAgingSummaryCustomer {
|
||||
customerName: string;
|
||||
aging: (IAgingPeriodTotal & IAgingPeriod)[];
|
||||
total: IAgingPeriodTotal;
|
||||
}
|
||||
|
||||
export interface ARAgingSummaryCustomerTotal {
|
||||
amount: number,
|
||||
formattedAmount: string,
|
||||
currencyCode: string,
|
||||
export interface IARAgingSummaryData {
|
||||
customers: IARAgingSummaryCustomer[],
|
||||
total: (IAgingPeriodTotal & IAgingPeriod)[]
|
||||
}
|
||||
|
||||
export interface ARAgingSummaryCustomer {
|
||||
customerName: string,
|
||||
aging: IAgingPeriodTotal[],
|
||||
total: ARAgingSummaryCustomerTotal,
|
||||
};
|
||||
export type IARAgingSummaryColumns = IAgingPeriod[];
|
||||
12
server/src/interfaces/AgingReport.ts
Normal file
12
server/src/interfaces/AgingReport.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export interface IAgingPeriodTotal {
|
||||
total: number;
|
||||
formattedTotal: string;
|
||||
currencyCode: string;
|
||||
}
|
||||
|
||||
export interface IAgingPeriod {
|
||||
fromPeriod: Date|string;
|
||||
toPeriod: Date|string;
|
||||
beforeDays: number;
|
||||
toDays: number;
|
||||
}
|
||||
@@ -1,6 +1,75 @@
|
||||
import moment from 'moment';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import APAgingSummarySheet from './APAgingSummarySheet';
|
||||
|
||||
|
||||
|
||||
@Service()
|
||||
export default class PayableAgingSummaryService {
|
||||
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Default report query.
|
||||
*/
|
||||
get defaultQuery() {
|
||||
return {
|
||||
asDate: moment().format('YYYY-MM-DD'),
|
||||
agingDaysBefore: 30,
|
||||
agingPeriods: 3,
|
||||
numberFormat: {
|
||||
noCents: false,
|
||||
divideOn1000: false,
|
||||
},
|
||||
vendorsIds: [],
|
||||
noneZero: false,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param query
|
||||
*/
|
||||
async APAgingSummary(tenantId: number, query) {
|
||||
const {
|
||||
vendorRepository,
|
||||
} = this.tenancy.repositories(tenantId);
|
||||
const { Bill } = this.tenancy.models(tenantId);
|
||||
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
this.logger.info('[AR_Aging_Summary] trying to prepairing the report.', {
|
||||
tenantId, filter,
|
||||
});
|
||||
// Settings tenant service.
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
// Retrieve all vendors from the storage.
|
||||
const vendors = await vendorRepository.all();
|
||||
|
||||
// Retrieve all unpaid vendors bills.
|
||||
const unpaidBills = await Bill.query().modify('unpaid');
|
||||
|
||||
// A/P aging summary report instance.
|
||||
const APAgingSummaryReport = new APAgingSummarySheet(
|
||||
tenantId,
|
||||
filter,
|
||||
vendors,
|
||||
unpaidBills,
|
||||
baseCurrency,
|
||||
);
|
||||
// A/P aging summary report data and columns.
|
||||
const data = APAgingSummaryReport.reportData();
|
||||
const columns = APAgingSummaryReport.reportColumns();
|
||||
|
||||
return { data, columns, query: filter };
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,97 @@
|
||||
import FinancialSheet from "../FinancialSheet";
|
||||
import { groupBy, sumBy } from 'lodash';
|
||||
import AgingSummaryReport from './AgingSummary';
|
||||
import {
|
||||
IAPAgingSummaryQuery,
|
||||
IAgingPeriod,
|
||||
IBill,
|
||||
IVendor,
|
||||
IAPAgingSummaryData,
|
||||
IAPAgingSummaryVendor,
|
||||
IAPAgingSummaryColumns
|
||||
} from 'interfaces';
|
||||
import { Dictionary } from 'tsyringe/dist/typings/types';
|
||||
export default class APAgingSummarySheet extends AgingSummaryReport {
|
||||
readonly tenantId: number;
|
||||
readonly query: IAPAgingSummaryQuery;
|
||||
readonly contacts: IVendor[];
|
||||
readonly unpaidBills: IBill[];
|
||||
readonly baseCurrency: string;
|
||||
|
||||
readonly unpaidInvoicesByContactId: Dictionary<IBill[]>;
|
||||
readonly agingPeriods: IAgingPeriod[];
|
||||
|
||||
constructor(
|
||||
tenantId: number,
|
||||
query: IAPAgingSummaryQuery,
|
||||
vendors: IVendor[],
|
||||
unpaidBills: IBill[],
|
||||
baseCurrency: string
|
||||
) {
|
||||
super();
|
||||
|
||||
export default class APAgingSummarySheet extends FinancialSheet {
|
||||
this.tenantId = tenantId;
|
||||
this.query = query;
|
||||
this.numberFormat = this.query.numberFormat;
|
||||
this.contacts = vendors;
|
||||
this.unpaidBills = unpaidBills;
|
||||
this.baseCurrency = baseCurrency;
|
||||
|
||||
|
||||
this.unpaidInvoicesByContactId = groupBy(unpaidBills, 'vendorId');
|
||||
|
||||
reportData() {
|
||||
// Initializes the aging periods.
|
||||
this.agingPeriods = this.agingRangePeriods(
|
||||
this.query.asDate,
|
||||
this.query.agingDaysBefore,
|
||||
this.query.agingPeriods
|
||||
);
|
||||
this.initContactsAgingPeriods();
|
||||
this.calcUnpaidInvoicesAgingPeriods();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the vendor section data.
|
||||
* @param {IVendor} vendor
|
||||
* @return {IAPAgingSummaryData}
|
||||
*/
|
||||
protected vendorData(vendor: IVendor): IAPAgingSummaryVendor {
|
||||
const agingPeriods = this.getContactAgingPeriods(vendor.id);
|
||||
const amount = sumBy(agingPeriods, 'total');
|
||||
|
||||
return {
|
||||
vendorName: vendor.displayName,
|
||||
aging: agingPeriods,
|
||||
total: this.formatTotalAmount(amount),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve vendors aging periods.
|
||||
* @return {IAPAgingSummaryVendor[]}
|
||||
*/
|
||||
private vendorsWalker(): IAPAgingSummaryVendor[] {
|
||||
return this.contacts
|
||||
.map((vendor) => this.vendorData(vendor))
|
||||
.filter(
|
||||
(vendor: IAPAgingSummaryVendor) =>
|
||||
!(vendor.total.total === 0 && this.query.noneZero)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the A/P aging summary report data.
|
||||
* @return {IAPAgingSummaryData}
|
||||
*/
|
||||
public reportData(): IAPAgingSummaryData {
|
||||
return {
|
||||
vendors: this.vendorsWalker(),
|
||||
total: this.getTotalAgingPeriods(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the A/P aging summary report columns.
|
||||
*/
|
||||
reportColumns(): IAPAgingSummaryColumns {
|
||||
return this.agingPeriods;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import moment from 'moment';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { IARAgingSummaryQuery } from 'interfaces';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import Journal from 'services/Accounting/JournalPoster';
|
||||
import ARAgingSummarySheet from './ARAgingSummarySheet';
|
||||
|
||||
@Service()
|
||||
@@ -31,63 +30,48 @@ export default class ARAgingSummaryService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retreive th accounts receivable aging summary data and columns.
|
||||
* @param {number} tenantId
|
||||
* @param query
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param query
|
||||
*/
|
||||
async ARAgingSummary(tenantId: number, query: IARAgingSummaryQuery) {
|
||||
const {
|
||||
customerRepository,
|
||||
accountRepository,
|
||||
transactionsRepository,
|
||||
accountTypeRepository
|
||||
saleInvoiceRepository
|
||||
} = this.tenancy.repositories(tenantId);
|
||||
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
this.logger.info('[AR_Aging_Summary] try to calculate the report.', { tenantId, filter });
|
||||
|
||||
this.logger.info('[AR_Aging_Summary] try to calculate the report.', {
|
||||
tenantId,
|
||||
filter,
|
||||
});
|
||||
// Settings tenant service.
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' });
|
||||
|
||||
// Retrieve all accounts graph on the storage.
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
// Retrieve all customers from the storage.
|
||||
const customers = await customerRepository.all();
|
||||
|
||||
// Retrieve AR account type.
|
||||
const ARType = await accountTypeRepository.getByKey('accounts_receivable');
|
||||
// Retrieve all due sale invoices.
|
||||
const dueSaleInvoices = await saleInvoiceRepository.dueInvoices();
|
||||
|
||||
// Retreive AR account.
|
||||
const ARAccount = await Account.query().findOne('account_type_id', ARType.id);
|
||||
|
||||
// Retrieve journal transactions based on the given query.
|
||||
const transactions = await transactionsRepository.journal({
|
||||
toDate: filter.asDate,
|
||||
contactType: 'customer',
|
||||
contactsIds: customers.map(customer => customer.id),
|
||||
});
|
||||
// Converts transactions array to journal collection.
|
||||
const journal = Journal.fromTransactions(transactions, tenantId, accountsGraph);
|
||||
|
||||
// AR aging summary report instnace.
|
||||
// AR aging summary report instance.
|
||||
const ARAgingSummaryReport = new ARAgingSummarySheet(
|
||||
tenantId,
|
||||
filter,
|
||||
customers,
|
||||
journal,
|
||||
ARAccount,
|
||||
dueSaleInvoices,
|
||||
baseCurrency
|
||||
);
|
||||
// AR aging summary report data and columns.
|
||||
const data = ARAgingSummaryReport.reportData();
|
||||
const columns = ARAgingSummaryReport.reportColumns();
|
||||
|
||||
return { data, columns };
|
||||
return { data, columns, query: filter };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,154 +1,107 @@
|
||||
import { groupBy, sumBy, defaultTo } from 'lodash';
|
||||
import {
|
||||
ICustomer,
|
||||
IARAgingSummaryQuery,
|
||||
ARAgingSummaryCustomer,
|
||||
IAgingPeriodClosingBalance,
|
||||
IARAgingSummaryCustomer,
|
||||
IAgingPeriodTotal,
|
||||
IJournalPoster,
|
||||
IAccount,
|
||||
IAgingPeriod
|
||||
} from "interfaces";
|
||||
IAgingPeriod,
|
||||
ISaleInvoice,
|
||||
IARAgingSummaryData,
|
||||
IARAgingSummaryColumns,
|
||||
} from 'interfaces';
|
||||
import AgingSummaryReport from './AgingSummary';
|
||||
|
||||
import { Dictionary } from 'tsyringe/dist/typings/types';
|
||||
|
||||
export default class ARAgingSummarySheet extends AgingSummaryReport {
|
||||
tenantId: number;
|
||||
query: IARAgingSummaryQuery;
|
||||
customers: ICustomer[];
|
||||
journal: IJournalPoster;
|
||||
ARAccount: IAccount;
|
||||
agingPeriods: IAgingPeriod[];
|
||||
baseCurrency: string;
|
||||
readonly tenantId: number;
|
||||
readonly query: IARAgingSummaryQuery;
|
||||
readonly contacts: ICustomer[];
|
||||
readonly agingPeriods: IAgingPeriod[];
|
||||
readonly baseCurrency: string;
|
||||
readonly dueInvoices: ISaleInvoice[];
|
||||
readonly unpaidInvoicesByContactId: Dictionary<ISaleInvoice[]>;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId
|
||||
* @param {IARAgingSummaryQuery} query
|
||||
* @param {ICustomer[]} customers
|
||||
* @param {IJournalPoster} journal
|
||||
* @param {number} tenantId
|
||||
* @param {IARAgingSummaryQuery} query
|
||||
* @param {ICustomer[]} customers
|
||||
* @param {IJournalPoster} journal
|
||||
*/
|
||||
constructor(
|
||||
tenantId: number,
|
||||
query: IARAgingSummaryQuery,
|
||||
customers: ICustomer[],
|
||||
journal: IJournalPoster,
|
||||
ARAccount: IAccount,
|
||||
baseCurrency: string,
|
||||
unpaidSaleInvoices: ISaleInvoice[],
|
||||
baseCurrency: string
|
||||
) {
|
||||
super();
|
||||
|
||||
this.tenantId = tenantId;
|
||||
this.customers = customers;
|
||||
this.contacts = customers;
|
||||
this.query = query;
|
||||
this.numberFormat = this.query.numberFormat;
|
||||
this.journal = journal;
|
||||
this.ARAccount = ARAccount;
|
||||
this.baseCurrency = baseCurrency;
|
||||
this.numberFormat = this.query.numberFormat;
|
||||
this.unpaidInvoicesByContactId = groupBy(unpaidSaleInvoices, 'customerId');
|
||||
this.dueInvoices = unpaidSaleInvoices;
|
||||
this.periodsByContactId = {};
|
||||
|
||||
this.initAgingPeriod();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the aging periods.
|
||||
*/
|
||||
private initAgingPeriod() {
|
||||
// Initializes the aging periods.
|
||||
this.agingPeriods = this.agingRangePeriods(
|
||||
this.query.asDate,
|
||||
this.query.agingDaysBefore,
|
||||
this.query.agingPeriods
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ICustomer} customer
|
||||
* @param {IAgingPeriod} agingPeriod
|
||||
*/
|
||||
private agingPeriodCloser(
|
||||
customer: ICustomer,
|
||||
agingPeriod: IAgingPeriod,
|
||||
): IAgingPeriodClosingBalance {
|
||||
// Calculate the trial balance between the given date period.
|
||||
const agingTrialBalance = this.journal.getContactTrialBalance(
|
||||
this.ARAccount.id,
|
||||
customer.id,
|
||||
'customer',
|
||||
agingPeriod.fromPeriod,
|
||||
);
|
||||
return {
|
||||
...agingPeriod,
|
||||
closingBalance: agingTrialBalance.debit,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ICustomer} customer
|
||||
*/
|
||||
private getCustomerAging(customer: ICustomer, totalReceivable: number): IAgingPeriodTotal[] {
|
||||
const agingClosingBalance = this.agingPeriods
|
||||
.map((agingPeriod: IAgingPeriod) => this.agingPeriodCloser(customer, agingPeriod));
|
||||
|
||||
const aging = this.contactAgingBalance(
|
||||
agingClosingBalance,
|
||||
totalReceivable
|
||||
);
|
||||
return aging;
|
||||
this.initContactsAgingPeriods();
|
||||
this.calcUnpaidInvoicesAgingPeriods();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping aging customer.
|
||||
* @param {ICustomer} customer -
|
||||
* @return {ARAgingSummaryCustomer[]}
|
||||
* @return {IARAgingSummaryCustomer[]}
|
||||
*/
|
||||
private customerMapper(customer: ICustomer): ARAgingSummaryCustomer {
|
||||
// Calculate the trial balance total of the given customer.
|
||||
const trialBalance = this.journal.getContactTrialBalance(
|
||||
this.ARAccount.id,
|
||||
customer.id,
|
||||
'customer'
|
||||
);
|
||||
const amount = trialBalance.balance;
|
||||
const formattedAmount = this.formatNumber(amount);
|
||||
const currencyCode = this.baseCurrency;
|
||||
private customerData(customer: ICustomer): IARAgingSummaryCustomer {
|
||||
const agingPeriods = this.getContactAgingPeriods(customer.id);
|
||||
const amount = sumBy(agingPeriods, 'total');
|
||||
|
||||
return {
|
||||
customerName: customer.displayName,
|
||||
aging: this.getCustomerAging(customer, trialBalance.balance),
|
||||
total: {
|
||||
amount,
|
||||
formattedAmount,
|
||||
currencyCode,
|
||||
},
|
||||
aging: agingPeriods,
|
||||
total: this.formatTotalAmount(amount),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve customers walker.
|
||||
* @param {ICustomer[]} customers
|
||||
* @return {ARAgingSummaryCustomer[]}
|
||||
* Retrieve customers report.
|
||||
* @param {ICustomer[]} customers
|
||||
* @return {IARAgingSummaryCustomer[]}
|
||||
*/
|
||||
private customersWalker(customers: ICustomer[]): ARAgingSummaryCustomer[] {
|
||||
return customers
|
||||
.map((customer: ICustomer) => this.customerMapper(customer))
|
||||
|
||||
// Filter customers that have zero total amount when `noneZero` is on.
|
||||
.filter((customer: ARAgingSummaryCustomer) =>
|
||||
!(customer.total.amount === 0 && this.query.noneZero),
|
||||
private customersWalker(): IARAgingSummaryCustomer[] {
|
||||
return this.contacts
|
||||
.map((customer) => this.customerData(customer))
|
||||
.filter(
|
||||
(customer: IARAgingSummaryCustomer) =>
|
||||
!(customer.total.total === 0 && this.query.noneZero)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve AR. aging summary report data.
|
||||
* Retrieve A/R aging summary report data.
|
||||
* @return {IARAgingSummaryData}
|
||||
*/
|
||||
public reportData() {
|
||||
return this.customersWalker(this.customers);
|
||||
public reportData(): IARAgingSummaryData {
|
||||
return {
|
||||
customers: this.customersWalker(),
|
||||
total: this.getTotalAgingPeriods(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve AR aging summary report columns.
|
||||
* @return {IARAgingSummaryColumns}
|
||||
*/
|
||||
reportColumns() {
|
||||
return []
|
||||
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: 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;
|
||||
}
|
||||
}
|
||||
@@ -1,75 +1,158 @@
|
||||
import moment from 'moment';
|
||||
import { omit, reverse } from 'lodash';
|
||||
import { IAgingPeriod, IAgingPeriodClosingBalance, IAgingPeriodTotal } from 'interfaces';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
import { defaultTo } from 'lodash';
|
||||
import {
|
||||
IAgingPeriod,
|
||||
ISaleInvoice,
|
||||
IBill,
|
||||
IAgingPeriodTotal,
|
||||
IContact,
|
||||
} from 'interfaces';
|
||||
import AgingReport from './AgingReport';
|
||||
import { Dictionary } from 'tsyringe/dist/typings/types';
|
||||
|
||||
export default class AgingSummaryReport extends FinancialSheet{
|
||||
export default abstract class AgingSummaryReport extends AgingReport {
|
||||
protected readonly contacts: IContact[];
|
||||
protected readonly agingPeriods: IAgingPeriod[] = [];
|
||||
protected readonly baseCurrency: string;
|
||||
protected readonly unpaidInvoices: (ISaleInvoice | IBill)[];
|
||||
readonly unpaidInvoicesByContactId: Dictionary<
|
||||
(ISaleInvoice | IBill)[]
|
||||
>;
|
||||
protected periodsByContactId: {
|
||||
[key: number]: (IAgingPeriod & IAgingPeriodTotal)[];
|
||||
} = {};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Array} agingPeriods
|
||||
* @param {Numeric} customerBalance
|
||||
* Setes initial aging periods to the given customer id.
|
||||
* @param {number} customerId - Customer id.
|
||||
*/
|
||||
contactAgingBalance(
|
||||
agingPeriods: IAgingPeriodClosingBalance[],
|
||||
receivableTotalCredit: number,
|
||||
): IAgingPeriodTotal[] {
|
||||
let prevAging = 0;
|
||||
let receivableCredit = receivableTotalCredit;
|
||||
let diff = receivableCredit;
|
||||
|
||||
const periods = reverse(agingPeriods).map((agingPeriod) => {
|
||||
const agingAmount = (agingPeriod.closingBalance - prevAging);
|
||||
const subtract = Math.min(diff, agingAmount);
|
||||
diff -= Math.min(agingAmount, diff);
|
||||
|
||||
const total = Math.max(agingAmount - subtract, 0);
|
||||
|
||||
const output = {
|
||||
...omit(agingPeriod, ['closingBalance']),
|
||||
total,
|
||||
};
|
||||
prevAging = agingPeriod.closingBalance;
|
||||
return output;
|
||||
});
|
||||
return reverse(periods);
|
||||
protected setInitialAgingPeriods(contactId: number): void {
|
||||
this.periodsByContactId[contactId] = this.agingPeriods.map(
|
||||
(agingPeriod) => ({
|
||||
...agingPeriod,
|
||||
...this.formatTotalAmount(0),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} asDay
|
||||
* @param {*} agingDaysBefore
|
||||
* @param {*} agingPeriodsFreq
|
||||
* Calculates the given contact aging periods.
|
||||
* @param {ICustomer} customer
|
||||
* @return {(IAgingPeriod & IAgingPeriodTotal)[]}
|
||||
*/
|
||||
agingRangePeriods(asDay, agingDaysBefore, agingPeriodsFreq): IAgingPeriod[] {
|
||||
const totalAgingDays = agingDaysBefore * agingPeriodsFreq;
|
||||
const startAging = moment(asDay).startOf('day');
|
||||
const endAging = startAging.clone().subtract('days', totalAgingDays).endOf('day');
|
||||
protected getContactAgingPeriods(
|
||||
contactId: number
|
||||
): (IAgingPeriod & IAgingPeriodTotal)[] {
|
||||
return defaultTo(this.periodsByContactId[contactId], []);
|
||||
}
|
||||
|
||||
const agingPeriods: IAgingPeriod[] = [];
|
||||
const startingAging = startAging.clone();
|
||||
|
||||
let beforeDays = 1;
|
||||
let toDays = 0;
|
||||
|
||||
while (startingAging > endAging) {
|
||||
const currentAging = startingAging.clone();
|
||||
startingAging.subtract('days', agingDaysBefore).endOf('day');
|
||||
toDays += agingDaysBefore;
|
||||
|
||||
agingPeriods.push({
|
||||
fromPeriod: moment(currentAging).toDate(),
|
||||
toPeriod: moment(startingAging).toDate(),
|
||||
beforeDays: beforeDays === 1 ? 0 : beforeDays,
|
||||
toDays: toDays,
|
||||
...(startingAging.valueOf() === endAging.valueOf()) ? {
|
||||
toPeriod: null,
|
||||
toDays: null,
|
||||
} : {},
|
||||
});
|
||||
beforeDays += agingDaysBefore;
|
||||
/**
|
||||
* Sets the customer aging due amount to the table.
|
||||
* @param {number} customerId - Customer id.
|
||||
* @param {number} dueAmount - Due amount.
|
||||
* @param {number} overdueDays - Overdue days.
|
||||
*/
|
||||
protected setContactAgingDueAmount(
|
||||
customerId: number,
|
||||
dueAmount: number,
|
||||
overdueDays: number
|
||||
): void {
|
||||
if (!this.periodsByContactId[customerId]) {
|
||||
this.setInitialAgingPeriods(customerId);
|
||||
}
|
||||
return agingPeriods;
|
||||
const agingPeriods = this.periodsByContactId[customerId];
|
||||
|
||||
const newAgingPeriods = agingPeriods.map((agingPeriod) => {
|
||||
const isInAgingPeriod =
|
||||
agingPeriod.beforeDays < overdueDays &&
|
||||
agingPeriod.toDays > overdueDays;
|
||||
|
||||
return {
|
||||
...agingPeriod,
|
||||
total: isInAgingPeriod
|
||||
? agingPeriod.total + dueAmount
|
||||
: agingPeriod.total,
|
||||
};
|
||||
});
|
||||
this.periodsByContactId[customerId] = newAgingPeriods;
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Retrieve the aging period total object.
|
||||
* @param {number} amount
|
||||
* @return {IAgingPeriodTotal}
|
||||
*/
|
||||
protected formatTotalAmount(amount: number): IAgingPeriodTotal {
|
||||
return {
|
||||
total: amount,
|
||||
formattedTotal: this.formatNumber(amount),
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the total of the aging period by the given index.
|
||||
* @param {number} index
|
||||
* @return {number}
|
||||
*/
|
||||
protected getTotalAgingPeriodByIndex(index: number): number {
|
||||
return this.contacts.reduce((acc, customer) => {
|
||||
const periods = this.getContactAgingPeriods(customer.id);
|
||||
const totalPeriod = periods[index] ? periods[index].total : 0;
|
||||
|
||||
return acc + totalPeriod;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the initial aging periods to the all customers.
|
||||
*/
|
||||
protected initContactsAgingPeriods(): void {
|
||||
this.contacts.forEach((contact) => {
|
||||
this.setInitialAgingPeriods(contact.id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the due invoices by the given customer id.
|
||||
* @param {number} customerId -
|
||||
* @return {ISaleInvoice[]}
|
||||
*/
|
||||
protected getUnpaidInvoicesByContactId(
|
||||
contactId: number
|
||||
): (ISaleInvoice | IBill)[] {
|
||||
return defaultTo(this.unpaidInvoicesByContactId[contactId], []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve total aging periods of the report.
|
||||
* @return {(IAgingPeriodTotal & IAgingPeriod)[]}
|
||||
*/
|
||||
protected getTotalAgingPeriods(): (IAgingPeriodTotal & IAgingPeriod)[] {
|
||||
return this.agingPeriods.map((agingPeriod, index) => {
|
||||
const total = this.getTotalAgingPeriodByIndex(index);
|
||||
|
||||
return {
|
||||
...agingPeriod,
|
||||
...this.formatTotalAmount(total),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets customers invoices to aging periods.
|
||||
*/
|
||||
protected calcUnpaidInvoicesAgingPeriods(): void {
|
||||
this.contacts.forEach((contact) => {
|
||||
const unpaidInvoices = this.getUnpaidInvoicesByContactId(contact.id);
|
||||
|
||||
unpaidInvoices.forEach((unpaidInvoice) => {
|
||||
this.setContactAgingDueAmount(
|
||||
contact.id,
|
||||
unpaidInvoice.dueAmount,
|
||||
unpaidInvoice.overdueDays
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,13 @@ import BalanceSheetStructure from 'data/BalanceSheetStructure';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
|
||||
export default class BalanceSheetStatement extends FinancialSheet {
|
||||
query: IBalanceSheetQuery;
|
||||
tenantId: number;
|
||||
accounts: IAccount & { type: IAccountType }[];
|
||||
journalFinancial: IJournalPoster;
|
||||
comparatorDateType: string;
|
||||
dateRangeSet: string[];
|
||||
baseCurrency: string;
|
||||
readonly query: IBalanceSheetQuery;
|
||||
readonly tenantId: number;
|
||||
readonly accounts: IAccount & { type: IAccountType }[];
|
||||
readonly journalFinancial: IJournalPoster;
|
||||
readonly comparatorDateType: string;
|
||||
readonly dateRangeSet: string[];
|
||||
readonly baseCurrency: string;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Service, Inject } from "typedi";
|
||||
import { Service, Inject } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import { ServiceError } from "exceptions";
|
||||
import { ServiceError } from 'exceptions';
|
||||
import { difference } from 'lodash';
|
||||
import { IGeneralLedgerSheetQuery } from 'interfaces';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import Journal from "services/Accounting/JournalPoster";
|
||||
import Journal from 'services/Accounting/JournalPoster';
|
||||
import GeneralLedgerSheet from 'services/FinancialStatements/GeneralLedger/GeneralLedger';
|
||||
|
||||
const ERRORS = {
|
||||
@@ -39,8 +39,8 @@ export default class GeneralLedgerService {
|
||||
|
||||
/**
|
||||
* Validates accounts existance on the storage.
|
||||
* @param {number} tenantId
|
||||
* @param {number[]} accountsIds
|
||||
* @param {number} tenantId
|
||||
* @param {number[]} accountsIds
|
||||
*/
|
||||
async validateAccountsExistance(tenantId: number, accountsIds: number[]) {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
@@ -49,35 +49,42 @@ export default class GeneralLedgerService {
|
||||
const storedAccountsIds = storedAccounts.map((a) => a.id);
|
||||
|
||||
if (difference(accountsIds, storedAccountsIds).length > 0) {
|
||||
throw new ServiceError(ERRORS.ACCOUNTS_NOT_FOUND)
|
||||
throw new ServiceError(ERRORS.ACCOUNTS_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve general ledger report statement.
|
||||
* ----------
|
||||
* @param {number} tenantId
|
||||
* @param {IGeneralLedgerSheetQuery} query
|
||||
* @param {number} tenantId
|
||||
* @param {IGeneralLedgerSheetQuery} query
|
||||
* @return {IGeneralLedgerStatement}
|
||||
*/
|
||||
async generalLedger(tenantId: number, query: IGeneralLedgerSheetQuery):
|
||||
Promise<{
|
||||
data: any,
|
||||
query: IGeneralLedgerSheetQuery,
|
||||
}> {
|
||||
async generalLedger(
|
||||
tenantId: number,
|
||||
query: IGeneralLedgerSheetQuery
|
||||
): Promise<{
|
||||
data: any;
|
||||
query: IGeneralLedgerSheetQuery;
|
||||
}> {
|
||||
const {
|
||||
accountRepository,
|
||||
transactionsRepository,
|
||||
} = this.tenancy.repositories(tenantId);
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
this.logger.info('[general_ledger] trying to calculate the report.', { tenantId, filter })
|
||||
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' });
|
||||
this.logger.info('[general_ledger] trying to calculate the report.', {
|
||||
tenantId,
|
||||
filter,
|
||||
});
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
|
||||
// Retrieve all accounts from the storage.
|
||||
const accounts = await accountRepository.all('type');
|
||||
@@ -98,12 +105,22 @@ export default class GeneralLedgerService {
|
||||
toDate: filter.toDate,
|
||||
sumationCreditDebit: true,
|
||||
});
|
||||
|
||||
// Transform array transactions to journal collection.
|
||||
const transactionsJournal = Journal.fromTransactions(transactions, tenantId, accountsGraph);
|
||||
const openingTransJournal = Journal.fromTransactions(openingBalanceTrans, tenantId, accountsGraph);
|
||||
const closingTransJournal = Journal.fromTransactions(closingBalanceTrans, tenantId, accountsGraph);
|
||||
|
||||
const transactionsJournal = Journal.fromTransactions(
|
||||
transactions,
|
||||
tenantId,
|
||||
accountsGraph
|
||||
);
|
||||
const openingTransJournal = Journal.fromTransactions(
|
||||
openingBalanceTrans,
|
||||
tenantId,
|
||||
accountsGraph
|
||||
);
|
||||
const closingTransJournal = Journal.fromTransactions(
|
||||
closingBalanceTrans,
|
||||
tenantId,
|
||||
accountsGraph
|
||||
);
|
||||
// General ledger report instance.
|
||||
const generalLedgerInstance = new GeneralLedgerSheet(
|
||||
tenantId,
|
||||
@@ -120,6 +137,6 @@ export default class GeneralLedgerService {
|
||||
return {
|
||||
data: reportData,
|
||||
query: filter,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user