mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 12:50:38 +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),
|
||||
})),
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user