mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 14:20:31 +00:00
Merge branch 'master' of https://github.com/abouolia/Bigcapital
This commit is contained in:
@@ -6,8 +6,8 @@ import TrialBalanceSheetController from './FinancialStatements/TrialBalanceSheet
|
|||||||
import GeneralLedgerController from './FinancialStatements/GeneralLedger';
|
import GeneralLedgerController from './FinancialStatements/GeneralLedger';
|
||||||
import JournalSheetController from './FinancialStatements/JournalSheet';
|
import JournalSheetController from './FinancialStatements/JournalSheet';
|
||||||
import ProfitLossController from './FinancialStatements/ProfitLossSheet';
|
import ProfitLossController from './FinancialStatements/ProfitLossSheet';
|
||||||
import ReceivableAgingSummary from './FinancialStatements/ARAgingSummary';
|
import ARAgingSummary from './FinancialStatements/ARAgingSummary';
|
||||||
// import PayableAgingSummary from './FinancialStatements/PayableAgingSummary';
|
import APAgingSummary from './FinancialStatements/APAgingSummary';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class FinancialStatementsService {
|
export default class FinancialStatementsService {
|
||||||
@@ -22,8 +22,8 @@ export default class FinancialStatementsService {
|
|||||||
router.use('/general_ledger', Container.get(GeneralLedgerController).router());
|
router.use('/general_ledger', Container.get(GeneralLedgerController).router());
|
||||||
router.use('/trial_balance_sheet', Container.get(TrialBalanceSheetController).router());
|
router.use('/trial_balance_sheet', Container.get(TrialBalanceSheetController).router());
|
||||||
router.use('/journal', Container.get(JournalSheetController).router());
|
router.use('/journal', Container.get(JournalSheetController).router());
|
||||||
router.use('/receivable_aging_summary', Container.get(ReceivableAgingSummary).router());
|
router.use('/receivable_aging_summary', Container.get(ARAgingSummary).router());
|
||||||
// router.use('/payable_aging_summary', PayableAgingSummary.router());
|
router.use('/payable_aging_summary', Container.get(APAgingSummary).router());
|
||||||
|
|
||||||
return 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() {
|
get validationSchema() {
|
||||||
return [
|
return [
|
||||||
query('as_date').optional().isISO8601(),
|
query('as_date').optional().isISO8601(),
|
||||||
query('aging_days_before').optional().isNumeric().toInt(),
|
query('aging_days_before').optional().isInt({ max: 500 }).toInt(),
|
||||||
query('aging_periods').optional().isNumeric().toInt(),
|
query('aging_periods').optional().isInt({ max: 12 }).toInt(),
|
||||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||||
query('number_format.1000_divide').optional().isBoolean().toBoolean(),
|
query('number_format.1000_divide').optional().isBoolean().toBoolean(),
|
||||||
oneOf(
|
oneOf(
|
||||||
@@ -41,7 +41,7 @@ export default class ARAgingSummaryReportController extends BaseController {
|
|||||||
],
|
],
|
||||||
[query('customer_ids').optional().isNumeric().toInt()]
|
[query('customer_ids').optional().isNumeric().toInt()]
|
||||||
),
|
),
|
||||||
query('none_zero').optional().isBoolean().toBoolean(),
|
query('none_zero').default(true).isBoolean().toBoolean(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,18 +49,31 @@ export default class ARAgingSummaryReportController extends BaseController {
|
|||||||
* Retrieve receivable aging summary report.
|
* Retrieve receivable aging summary report.
|
||||||
*/
|
*/
|
||||||
async receivableAgingSummary(req: Request, res: Response) {
|
async receivableAgingSummary(req: Request, res: Response) {
|
||||||
const { tenantId } = req;
|
const { tenantId, settings } = req;
|
||||||
const filter = this.matchedQueryData(req);
|
const filter = this.matchedQueryData(req);
|
||||||
|
|
||||||
|
const organizationName = settings.get({
|
||||||
|
group: 'organization',
|
||||||
|
key: 'name',
|
||||||
|
});
|
||||||
|
const baseCurrency = settings.get({
|
||||||
|
group: 'organization',
|
||||||
|
key: 'base_currency',
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
columns
|
columns,
|
||||||
|
query,
|
||||||
} = await this.ARAgingSummaryService.ARAgingSummary(tenantId, filter);
|
} = await this.ARAgingSummaryService.ARAgingSummary(tenantId, filter);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
|
organization_name: organizationName,
|
||||||
|
base_currency: baseCurrency,
|
||||||
data: this.transfromToResponse(data),
|
data: this.transfromToResponse(data),
|
||||||
columns: this.transfromToResponse(columns),
|
columns: this.transfromToResponse(columns),
|
||||||
|
query: this.transfromToResponse(query),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(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),
|
|
||||||
})),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -321,7 +321,6 @@ export default class SaleInvoicesController extends BaseController {
|
|||||||
tenantId,
|
tenantId,
|
||||||
customerId
|
customerId
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
sales_invoices: this.transfromToResponse(salesInvoices),
|
sales_invoices: this.transfromToResponse(salesInvoices),
|
||||||
});
|
});
|
||||||
|
|||||||
35
server/src/interfaces/APAgingSummaryReport.ts
Normal file
35
server/src/interfaces/APAgingSummaryReport.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
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,
|
||||||
|
current: IAgingPeriodTotal,
|
||||||
|
aging: (IAgingPeriod & IAgingPeriodTotal)[],
|
||||||
|
total: IAgingPeriodTotal,
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IAPAgingSummaryTotal {
|
||||||
|
current: IAgingPeriodTotal,
|
||||||
|
aging: (IAgingPeriodTotal & IAgingPeriod)[],
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IAPAgingSummaryData {
|
||||||
|
vendors: IAPAgingSummaryVendor[],
|
||||||
|
total: IAPAgingSummaryTotal,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IAPAgingSummaryColumns = IAgingPeriod[];
|
||||||
@@ -1,45 +1,34 @@
|
|||||||
|
import {
|
||||||
|
IAgingPeriod,
|
||||||
|
IAgingPeriodTotal
|
||||||
|
} from './AgingReport';
|
||||||
|
|
||||||
export interface IARAgingSummaryQuery {
|
export interface IARAgingSummaryQuery {
|
||||||
asDate: Date | string,
|
asDate: Date | string;
|
||||||
agingDaysBefore: number,
|
agingDaysBefore: number;
|
||||||
agingPeriods: number,
|
agingPeriods: number;
|
||||||
numberFormat: {
|
numberFormat: {
|
||||||
noCents: number,
|
noCents: boolean;
|
||||||
divideOn1000: number,
|
divideOn1000: boolean;
|
||||||
},
|
};
|
||||||
customersIds: number[],
|
customersIds: number[];
|
||||||
noneZero: boolean,
|
noneZero: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAgingPeriod {
|
export interface IARAgingSummaryCustomer {
|
||||||
fromPeriod: Date,
|
customerName: string;
|
||||||
toPeriod: Date,
|
current: IAgingPeriodTotal,
|
||||||
beforeDays: number,
|
aging: (IAgingPeriodTotal & IAgingPeriod)[];
|
||||||
toDays: number,
|
total: IAgingPeriodTotal;
|
||||||
};
|
|
||||||
|
|
||||||
export interface IAgingPeriodClosingBalance extends IAgingPeriod {
|
|
||||||
closingBalance: number,
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface IAgingPeriodTotal extends IAgingPeriod {
|
|
||||||
total: number,
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ARAgingSummaryCustomerPeriod {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ARAgingSummaryCustomerTotal {
|
export interface IARAgingSummaryTotal {
|
||||||
amount: number,
|
current: IAgingPeriodTotal,
|
||||||
formattedAmount: string,
|
aging: (IAgingPeriodTotal & IAgingPeriod)[],
|
||||||
currencyCode: string,
|
};
|
||||||
|
export interface IARAgingSummaryData {
|
||||||
|
customers: IARAgingSummaryCustomer[],
|
||||||
|
total: IARAgingSummaryTotal,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ARAgingSummaryCustomer {
|
export type IARAgingSummaryColumns = IAgingPeriod[];
|
||||||
customerName: string,
|
|
||||||
aging: IAgingPeriodTotal[],
|
|
||||||
total: ARAgingSummaryCustomerTotal,
|
|
||||||
};
|
|
||||||
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;
|
||||||
|
}
|
||||||
@@ -42,6 +42,9 @@ export interface IBill {
|
|||||||
amount: number,
|
amount: number,
|
||||||
paymentAmount: number,
|
paymentAmount: number,
|
||||||
|
|
||||||
|
dueAmount: number,
|
||||||
|
overdueDays: number,
|
||||||
|
|
||||||
invLotNumber: string,
|
invLotNumber: string,
|
||||||
openedAt: Date | string,
|
openedAt: Date | string,
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface ISaleInvoice {
|
|||||||
invoiceDate: Date,
|
invoiceDate: Date,
|
||||||
dueDate: Date,
|
dueDate: Date,
|
||||||
dueAmount: number,
|
dueAmount: number,
|
||||||
|
overdueDays: number,
|
||||||
customerId: number,
|
customerId: number,
|
||||||
entries: IItemEntry[],
|
entries: IItemEntry[],
|
||||||
deliveredAt: string | Date,
|
deliveredAt: string | Date,
|
||||||
|
|||||||
@@ -35,5 +35,7 @@ export * from './TrialBalanceSheet';
|
|||||||
export * from './GeneralLedgerSheet'
|
export * from './GeneralLedgerSheet'
|
||||||
export * from './ProfitLossSheet';
|
export * from './ProfitLossSheet';
|
||||||
export * from './JournalReport';
|
export * from './JournalReport';
|
||||||
|
export * from './AgingReport';
|
||||||
export * from './ARAgingSummaryReport';
|
export * from './ARAgingSummaryReport';
|
||||||
|
export * from './APAgingSummaryReport';
|
||||||
export * from './Mailable';
|
export * from './Mailable';
|
||||||
@@ -48,6 +48,12 @@ export default class Bill extends TenantModel {
|
|||||||
overdue(query) {
|
overdue(query) {
|
||||||
query.where('due_date', '<', moment().format('YYYY-MM-DD'));
|
query.where('due_date', '<', moment().format('YYYY-MM-DD'));
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Filters the not overdue invoices.
|
||||||
|
*/
|
||||||
|
notOverdue(query, asDate = moment().format('YYYY-MM-DD')) {
|
||||||
|
query.where('due_date', '>=', asDate);
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* Filters the partially paid bills.
|
* Filters the partially paid bills.
|
||||||
*/
|
*/
|
||||||
@@ -61,7 +67,13 @@ export default class Bill extends TenantModel {
|
|||||||
paid(query) {
|
paid(query) {
|
||||||
query.where(raw('`PAYMENT_AMOUNT` = `AMOUNT`'));
|
query.where(raw('`PAYMENT_AMOUNT` = `AMOUNT`'));
|
||||||
},
|
},
|
||||||
}
|
/**
|
||||||
|
* Filters the bills from the given date.
|
||||||
|
*/
|
||||||
|
fromDate(query, fromDate) {
|
||||||
|
query.where('bill_date', '<=', fromDate)
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,7 +83,7 @@ export default class Bill extends TenantModel {
|
|||||||
return ['createdAt', 'updatedAt'];
|
return ['createdAt', 'updatedAt'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Virtual attributes.
|
* Virtual attributes.
|
||||||
*/
|
*/
|
||||||
static get virtualAttributes() {
|
static get virtualAttributes() {
|
||||||
@@ -117,7 +129,7 @@ export default class Bill extends TenantModel {
|
|||||||
*/
|
*/
|
||||||
get isFullyPaid() {
|
get isFullyPaid() {
|
||||||
return this.dueAmount === 0;
|
return this.dueAmount === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detarmines whether the bill paid fully or partially.
|
* Detarmines whether the bill paid fully or partially.
|
||||||
@@ -133,7 +145,9 @@ export default class Bill extends TenantModel {
|
|||||||
*/
|
*/
|
||||||
get remainingDays() {
|
get remainingDays() {
|
||||||
// Can't continue in case due date not defined.
|
// Can't continue in case due date not defined.
|
||||||
if (!this.dueDate) { return null; }
|
if (!this.dueDate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const date = moment();
|
const date = moment();
|
||||||
const dueDate = moment(this.dueDate);
|
const dueDate = moment(this.dueDate);
|
||||||
@@ -146,13 +160,7 @@ export default class Bill extends TenantModel {
|
|||||||
* @return {number|null}
|
* @return {number|null}
|
||||||
*/
|
*/
|
||||||
get overdueDays() {
|
get overdueDays() {
|
||||||
// Can't continue in case due date not defined.
|
return this.getOverdueDays();
|
||||||
if (!this.dueDate) { return null; }
|
|
||||||
|
|
||||||
const date = moment();
|
|
||||||
const dueDate = moment(this.dueDate);
|
|
||||||
|
|
||||||
return Math.max(date.diff(dueDate, 'days'), 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -163,6 +171,17 @@ export default class Bill extends TenantModel {
|
|||||||
return this.overdueDays > 0;
|
return this.overdueDays > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOverdueDays(asDate = moment().format('YYYY-MM-DD')) {
|
||||||
|
// Can't continue in case due date not defined.
|
||||||
|
if (!this.dueDate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const date = moment(asDate);
|
||||||
|
const dueDate = moment(this.dueDate);
|
||||||
|
|
||||||
|
return Math.max(date.diff(dueDate, 'days'), 0);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relationship mapping.
|
* Relationship mapping.
|
||||||
*/
|
*/
|
||||||
@@ -180,7 +199,7 @@ export default class Bill extends TenantModel {
|
|||||||
},
|
},
|
||||||
filter(query) {
|
filter(query) {
|
||||||
query.where('contact_service', 'vendor');
|
query.where('contact_service', 'vendor');
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
entries: {
|
entries: {
|
||||||
@@ -199,26 +218,22 @@ export default class Bill extends TenantModel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the not found bills ids as array that associated to the given vendor.
|
* Retrieve the not found bills ids as array that associated to the given vendor.
|
||||||
* @param {Array} billsIds
|
* @param {Array} billsIds
|
||||||
* @param {number} vendorId -
|
* @param {number} vendorId -
|
||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
static async getNotFoundBills(billsIds, vendorId) {
|
static async getNotFoundBills(billsIds, vendorId) {
|
||||||
const storedBills = await this.query()
|
const storedBills = await this.query().onBuild((builder) => {
|
||||||
.onBuild((builder) => {
|
builder.whereIn('id', billsIds);
|
||||||
builder.whereIn('id', billsIds);
|
|
||||||
|
if (vendorId) {
|
||||||
|
builder.where('vendor_id', vendorId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (vendorId) {
|
|
||||||
builder.where('vendor_id', vendorId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const storedBillsIds = storedBills.map((t) => t.id);
|
const storedBillsIds = storedBills.map((t) => t.id);
|
||||||
|
|
||||||
const notFoundBillsIds = difference(
|
const notFoundBillsIds = difference(billsIds, storedBillsIds);
|
||||||
billsIds,
|
|
||||||
storedBillsIds,
|
|
||||||
);
|
|
||||||
return notFoundBillsIds;
|
return notFoundBillsIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,19 +278,25 @@ export default class Bill extends TenantModel {
|
|||||||
label: 'Status',
|
label: 'Status',
|
||||||
options: [],
|
options: [],
|
||||||
query: (query, role) => {
|
query: (query, role) => {
|
||||||
switch(role.value) {
|
switch (role.value) {
|
||||||
case 'draft':
|
case 'draft':
|
||||||
query.modify('draft'); break;
|
query.modify('draft');
|
||||||
|
break;
|
||||||
case 'opened':
|
case 'opened':
|
||||||
query.modify('opened'); break;
|
query.modify('opened');
|
||||||
|
break;
|
||||||
case 'unpaid':
|
case 'unpaid':
|
||||||
query.modify('unpaid'); break;
|
query.modify('unpaid');
|
||||||
|
break;
|
||||||
case 'overdue':
|
case 'overdue':
|
||||||
query.modify('overdue'); break;
|
query.modify('overdue');
|
||||||
|
break;
|
||||||
case 'partially-paid':
|
case 'partially-paid':
|
||||||
query.modify('partiallyPaid'); break;
|
query.modify('partiallyPaid');
|
||||||
|
break;
|
||||||
case 'paid':
|
case 'paid':
|
||||||
query.modify('paid'); break;
|
query.modify('paid');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -295,14 +316,12 @@ export default class Bill extends TenantModel {
|
|||||||
label: 'Note',
|
label: 'Note',
|
||||||
column: 'note',
|
column: 'note',
|
||||||
},
|
},
|
||||||
user: {
|
user: {},
|
||||||
|
|
||||||
},
|
|
||||||
created_at: {
|
created_at: {
|
||||||
label: 'Created at',
|
label: 'Created at',
|
||||||
column: 'created_at',
|
column: 'created_at',
|
||||||
columnType: 'date',
|
columnType: 'date',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,19 +103,27 @@ export default class SaleInvoice extends TenantModel {
|
|||||||
* @return {number|null}
|
* @return {number|null}
|
||||||
*/
|
*/
|
||||||
get overdueDays() {
|
get overdueDays() {
|
||||||
// Can't continue in case due date not defined.
|
return this.getOverdueDays();
|
||||||
if (!this.dueDate) { return null; }
|
|
||||||
|
|
||||||
const date = moment();
|
|
||||||
const dueDate = moment(this.dueDate);
|
|
||||||
|
|
||||||
return Math.max(date.diff(dueDate, 'days'), 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get resourceable() {
|
static get resourceable() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} asDate
|
||||||
|
*/
|
||||||
|
getOverdueDays(asDate = moment().format('YYYY-MM-DD')) {
|
||||||
|
// Can't continue in case due date not defined.
|
||||||
|
if (!this.dueDate) { return null; }
|
||||||
|
|
||||||
|
const date = moment(asDate);
|
||||||
|
const dueDate = moment(this.dueDate);
|
||||||
|
|
||||||
|
return Math.max(date.diff(dueDate, 'days'), 0);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model modifiers.
|
* Model modifiers.
|
||||||
*/
|
*/
|
||||||
@@ -163,8 +171,14 @@ export default class SaleInvoice extends TenantModel {
|
|||||||
/**
|
/**
|
||||||
* Filters the overdue invoices.
|
* Filters the overdue invoices.
|
||||||
*/
|
*/
|
||||||
overdue(query) {
|
overdue(query, asDate = moment().format('YYYY-MM-DD')) {
|
||||||
query.where('due_date', '<', moment().format('YYYY-MM-DD'));
|
query.where('due_date', '<', asDate);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Filters the not overdue invoices.
|
||||||
|
*/
|
||||||
|
notOverdue(query, asDate = moment().format('YYYY-MM-DD')) {
|
||||||
|
query.where('due_date', '>=', asDate);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Filters the partially invoices.
|
* Filters the partially invoices.
|
||||||
@@ -178,6 +192,12 @@ export default class SaleInvoice extends TenantModel {
|
|||||||
*/
|
*/
|
||||||
paid(query) {
|
paid(query) {
|
||||||
query.where(raw('PAYMENT_AMOUNT = BALANCE'));
|
query.where(raw('PAYMENT_AMOUNT = BALANCE'));
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Filters the sale invoices from the given date.
|
||||||
|
*/
|
||||||
|
fromDate(query, fromDate) {
|
||||||
|
query.where('invoice_date', '<=', fromDate)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import moment from 'moment';
|
||||||
import { Bill } from 'models';
|
import { Bill } from 'models';
|
||||||
import TenantRepository from 'repositories/TenantRepository';
|
import TenantRepository from 'repositories/TenantRepository';
|
||||||
|
|
||||||
@@ -8,4 +9,30 @@ export default class BillRepository extends TenantRepository {
|
|||||||
get model() {
|
get model() {
|
||||||
return Bill.bindKnex(this.knex);
|
return Bill.bindKnex(this.knex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dueBills(asDate = moment().format('YYYY-MM-DD'), withRelations) {
|
||||||
|
const cacheKey = this.getCacheKey('dueInvoices', asDate, withRelations);
|
||||||
|
|
||||||
|
return this.cache.get(cacheKey, () => {
|
||||||
|
return this.model
|
||||||
|
.query()
|
||||||
|
.modify('dueBills')
|
||||||
|
.modify('notOverdue')
|
||||||
|
.modify('fromDate', asDate)
|
||||||
|
.withGraphFetched(withRelations);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
overdueBills(asDate = moment().format('YYYY-MM-DD'), withRelations) {
|
||||||
|
const cacheKey = this.getCacheKey('overdueInvoices', asDate, withRelations);
|
||||||
|
|
||||||
|
return this.cache.get(cacheKey, () => {
|
||||||
|
return this.model
|
||||||
|
.query()
|
||||||
|
.modify('dueBills')
|
||||||
|
.modify('overdue', asDate)
|
||||||
|
.modify('fromDate', asDate)
|
||||||
|
.withGraphFetched(withRelations);
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import moment from 'moment';
|
||||||
import { SaleInvoice } from 'models';
|
import { SaleInvoice } from 'models';
|
||||||
import TenantRepository from 'repositories/TenantRepository';
|
import TenantRepository from 'repositories/TenantRepository';
|
||||||
|
|
||||||
@@ -8,4 +9,30 @@ export default class SaleInvoiceRepository extends TenantRepository {
|
|||||||
get model() {
|
get model() {
|
||||||
return SaleInvoice.bindKnex(this.knex);
|
return SaleInvoice.bindKnex(this.knex);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
dueInvoices(asDate = moment().format('YYYY-MM-DD'), withRelations) {
|
||||||
|
const cacheKey = this.getCacheKey('dueInvoices', asDate, withRelations);
|
||||||
|
|
||||||
|
return this.cache.get(cacheKey, async () => {
|
||||||
|
return this.model
|
||||||
|
.query()
|
||||||
|
.modify('dueInvoices')
|
||||||
|
.modify('notOverdue', asDate)
|
||||||
|
.modify('fromDate', asDate)
|
||||||
|
.withGraphFetched(withRelations);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
overdueInvoices(asDate = moment().format('YYYY-MM-DD'), withRelations) {
|
||||||
|
const cacheKey = this.getCacheKey('overdueInvoices', asDate, withRelations);
|
||||||
|
|
||||||
|
return this.cache.get(cacheKey, () => {
|
||||||
|
return this.model
|
||||||
|
.query()
|
||||||
|
.modify('dueInvoices')
|
||||||
|
.modify('overdue', asDate)
|
||||||
|
.modify('fromDate', asDate)
|
||||||
|
.withGraphFetched(withRelations);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -291,8 +291,6 @@ export default class JournalCommands {
|
|||||||
referenceType: ['SaleInvoice'],
|
referenceType: ['SaleInvoice'],
|
||||||
index: [3, 4],
|
index: [3, 4],
|
||||||
});
|
});
|
||||||
console.log(transactions);
|
|
||||||
|
|
||||||
this.journal.fromTransactions(transactions);
|
this.journal.fromTransactions(transactions);
|
||||||
this.journal.removeEntries();
|
this.journal.removeEntries();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,80 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import TenancyService from 'services/Tenancy/TenancyService';
|
||||||
|
import APAgingSummarySheet from './APAgingSummarySheet';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
|
||||||
export default class PayableAgingSummaryService {
|
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,
|
||||||
|
billRepository
|
||||||
|
} = 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 overdue vendors bills.
|
||||||
|
const overdueBills = await billRepository.overdueBills(
|
||||||
|
filter.asDate,
|
||||||
|
);
|
||||||
|
const dueBills = await billRepository.dueBills(filter.asDate);
|
||||||
|
|
||||||
|
// A/P aging summary report instance.
|
||||||
|
const APAgingSummaryReport = new APAgingSummarySheet(
|
||||||
|
tenantId,
|
||||||
|
filter,
|
||||||
|
vendors,
|
||||||
|
overdueBills,
|
||||||
|
dueBills,
|
||||||
|
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,116 @@
|
|||||||
import FinancialSheet from "../FinancialSheet";
|
import { groupBy, sum } 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 overdueInvoicesByContactId: Dictionary<IBill[]>;
|
||||||
|
readonly currentInvoicesByContactId: Dictionary<IBill[]>;
|
||||||
|
|
||||||
|
readonly agingPeriods: IAgingPeriod[];
|
||||||
|
|
||||||
export default class APAgingSummarySheet extends FinancialSheet {
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
reportData() {
|
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 vendor section data.
|
||||||
|
* @param {IVendor} vendor
|
||||||
|
* @return {IAPAgingSummaryVendor}
|
||||||
|
*/
|
||||||
|
private vendorData(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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve vendors aging periods.
|
||||||
|
* @return {IAPAgingSummaryVendor[]}
|
||||||
|
*/
|
||||||
|
private vendorsWalker(vendors: IVendor[]): IAPAgingSummaryVendor[] {
|
||||||
|
return vendors
|
||||||
|
.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 {
|
||||||
|
const vendorsAgingPeriods = this.vendorsWalker(this.contacts);
|
||||||
|
const totalAgingPeriods = this.getTotalAgingPeriods(vendorsAgingPeriods);
|
||||||
|
const totalCurrent = this.getTotalCurrent(vendorsAgingPeriods);
|
||||||
|
|
||||||
|
return {
|
||||||
|
vendors: vendorsAgingPeriods,
|
||||||
|
total: {
|
||||||
|
current: this.formatTotalAmount(totalCurrent),
|
||||||
|
aging: totalAgingPeriods,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the A/P aging summary report columns.
|
||||||
|
*/
|
||||||
|
public reportColumns(): IAPAgingSummaryColumns {
|
||||||
|
return this.agingPeriods;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,8 @@ import moment from 'moment';
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { IARAgingSummaryQuery } from 'interfaces';
|
import { IARAgingSummaryQuery } from 'interfaces';
|
||||||
import TenancyService from 'services/Tenancy/TenancyService';
|
import TenancyService from 'services/Tenancy/TenancyService';
|
||||||
import Journal from 'services/Accounting/JournalPoster';
|
|
||||||
import ARAgingSummarySheet from './ARAgingSummarySheet';
|
import ARAgingSummarySheet from './ARAgingSummarySheet';
|
||||||
|
import SaleInvoiceRepository from 'repositories/SaleInvoiceRepository';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class ARAgingSummaryService {
|
export default class ARAgingSummaryService {
|
||||||
@@ -31,63 +31,54 @@ export default class ARAgingSummaryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retreive th accounts receivable aging summary data and columns.
|
*
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param query
|
* @param query
|
||||||
*/
|
*/
|
||||||
async ARAgingSummary(tenantId: number, query: IARAgingSummaryQuery) {
|
async ARAgingSummary(tenantId: number, query: IARAgingSummaryQuery) {
|
||||||
const {
|
const {
|
||||||
customerRepository,
|
customerRepository,
|
||||||
accountRepository,
|
saleInvoiceRepository,
|
||||||
transactionsRepository,
|
|
||||||
accountTypeRepository
|
|
||||||
} = this.tenancy.repositories(tenantId);
|
} = this.tenancy.repositories(tenantId);
|
||||||
|
|
||||||
const { Account } = this.tenancy.models(tenantId);
|
|
||||||
const filter = {
|
const filter = {
|
||||||
...this.defaultQuery,
|
...this.defaultQuery,
|
||||||
...query,
|
...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.
|
// Settings tenant service.
|
||||||
const settings = this.tenancy.settings(tenantId);
|
const settings = this.tenancy.settings(tenantId);
|
||||||
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' });
|
const baseCurrency = settings.get({
|
||||||
|
group: 'organization',
|
||||||
// Retrieve all accounts graph on the storage.
|
key: 'base_currency',
|
||||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
});
|
||||||
|
|
||||||
// Retrieve all customers from the storage.
|
// Retrieve all customers from the storage.
|
||||||
const customers = await customerRepository.all();
|
const customers = await customerRepository.all();
|
||||||
|
|
||||||
// Retrieve AR account type.
|
// Retrieve all overdue sale invoices.
|
||||||
const ARType = await accountTypeRepository.getByKey('accounts_receivable');
|
const overdueSaleInvoices = await saleInvoiceRepository.overdueInvoices(
|
||||||
|
filter.asDate
|
||||||
// Retreive AR account.
|
);
|
||||||
const ARAccount = await Account.query().findOne('account_type_id', ARType.id);
|
// Retrieve all due sale invoices.
|
||||||
|
const currentInvoices = await saleInvoiceRepository.dueInvoices(
|
||||||
// Retrieve journal transactions based on the given query.
|
filter.asDate
|
||||||
const transactions = await transactionsRepository.journal({
|
);
|
||||||
toDate: filter.asDate,
|
// AR aging summary report instance.
|
||||||
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.
|
|
||||||
const ARAgingSummaryReport = new ARAgingSummarySheet(
|
const ARAgingSummaryReport = new ARAgingSummarySheet(
|
||||||
tenantId,
|
tenantId,
|
||||||
filter,
|
filter,
|
||||||
customers,
|
customers,
|
||||||
journal,
|
overdueSaleInvoices,
|
||||||
ARAccount,
|
currentInvoices,
|
||||||
baseCurrency
|
baseCurrency
|
||||||
);
|
);
|
||||||
// AR aging summary report data and columns.
|
// AR aging summary report data and columns.
|
||||||
const data = ARAgingSummaryReport.reportData();
|
const data = ARAgingSummaryReport.reportData();
|
||||||
const columns = ARAgingSummaryReport.reportColumns();
|
const columns = ARAgingSummaryReport.reportColumns();
|
||||||
|
|
||||||
return { data, columns };
|
return { data, columns, query: filter };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,53 @@
|
|||||||
|
import { groupBy, sum } from 'lodash';
|
||||||
import {
|
import {
|
||||||
ICustomer,
|
ICustomer,
|
||||||
IARAgingSummaryQuery,
|
IARAgingSummaryQuery,
|
||||||
ARAgingSummaryCustomer,
|
IARAgingSummaryCustomer,
|
||||||
IAgingPeriodClosingBalance,
|
IAgingPeriod,
|
||||||
IAgingPeriodTotal,
|
ISaleInvoice,
|
||||||
IJournalPoster,
|
IARAgingSummaryData,
|
||||||
IAccount,
|
IARAgingSummaryColumns,
|
||||||
IAgingPeriod
|
} from 'interfaces';
|
||||||
} from "interfaces";
|
|
||||||
import AgingSummaryReport from './AgingSummary';
|
import AgingSummaryReport from './AgingSummary';
|
||||||
|
import { Dictionary } from 'tsyringe/dist/typings/types';
|
||||||
|
|
||||||
export default class ARAgingSummarySheet extends AgingSummaryReport {
|
export default class ARAgingSummarySheet extends AgingSummaryReport {
|
||||||
tenantId: number;
|
readonly tenantId: number;
|
||||||
query: IARAgingSummaryQuery;
|
readonly query: IARAgingSummaryQuery;
|
||||||
customers: ICustomer[];
|
readonly contacts: ICustomer[];
|
||||||
journal: IJournalPoster;
|
readonly agingPeriods: IAgingPeriod[];
|
||||||
ARAccount: IAccount;
|
readonly baseCurrency: string;
|
||||||
agingPeriods: IAgingPeriod[];
|
|
||||||
baseCurrency: string;
|
readonly overdueInvoicesByContactId: Dictionary<ISaleInvoice[]>;
|
||||||
|
readonly currentInvoicesByContactId: Dictionary<ISaleInvoice[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor method.
|
* Constructor method.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {IARAgingSummaryQuery} query
|
* @param {IARAgingSummaryQuery} query
|
||||||
* @param {ICustomer[]} customers
|
* @param {ICustomer[]} customers
|
||||||
* @param {IJournalPoster} journal
|
* @param {IJournalPoster} journal
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
query: IARAgingSummaryQuery,
|
query: IARAgingSummaryQuery,
|
||||||
customers: ICustomer[],
|
customers: ICustomer[],
|
||||||
journal: IJournalPoster,
|
overdueSaleInvoices: ISaleInvoice[],
|
||||||
ARAccount: IAccount,
|
currentSaleInvoices: ISaleInvoice[],
|
||||||
baseCurrency: string,
|
baseCurrency: string
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.tenantId = tenantId;
|
this.tenantId = tenantId;
|
||||||
this.customers = customers;
|
this.contacts = customers;
|
||||||
this.query = query;
|
this.query = query;
|
||||||
this.numberFormat = this.query.numberFormat;
|
|
||||||
this.journal = journal;
|
|
||||||
this.ARAccount = ARAccount;
|
|
||||||
this.baseCurrency = baseCurrency;
|
this.baseCurrency = baseCurrency;
|
||||||
|
this.numberFormat = this.query.numberFormat;
|
||||||
|
|
||||||
this.initAgingPeriod();
|
this.overdueInvoicesByContactId = groupBy(overdueSaleInvoices, 'customerId');
|
||||||
}
|
this.currentInvoicesByContactId = groupBy(currentSaleInvoices, 'customerId');
|
||||||
|
|
||||||
/**
|
// Initializes the aging periods.
|
||||||
* Initializes the aging periods.
|
|
||||||
*/
|
|
||||||
private initAgingPeriod() {
|
|
||||||
this.agingPeriods = this.agingRangePeriods(
|
this.agingPeriods = this.agingRangePeriods(
|
||||||
this.query.asDate,
|
this.query.asDate,
|
||||||
this.query.agingDaysBefore,
|
this.query.agingDaysBefore,
|
||||||
@@ -59,96 +55,62 @@ export default class ARAgingSummarySheet extends AgingSummaryReport {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapping aging customer.
|
* Mapping aging customer.
|
||||||
* @param {ICustomer} customer -
|
* @param {ICustomer} customer -
|
||||||
* @return {ARAgingSummaryCustomer[]}
|
* @return {IARAgingSummaryCustomer[]}
|
||||||
*/
|
*/
|
||||||
private customerMapper(customer: ICustomer): ARAgingSummaryCustomer {
|
private customerData(customer: ICustomer): IARAgingSummaryCustomer {
|
||||||
// Calculate the trial balance total of the given customer.
|
const agingPeriods = this.getContactAgingPeriods(customer.id);
|
||||||
const trialBalance = this.journal.getContactTrialBalance(
|
const currentTotal = this.getContactCurrentTotal(customer.id);
|
||||||
this.ARAccount.id,
|
const agingPeriodsTotal = this.getAgingPeriodsTotal(agingPeriods);
|
||||||
customer.id,
|
const amount = sum([agingPeriodsTotal, currentTotal]);
|
||||||
'customer'
|
|
||||||
);
|
|
||||||
const amount = trialBalance.balance;
|
|
||||||
const formattedAmount = this.formatNumber(amount);
|
|
||||||
const currencyCode = this.baseCurrency;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
customerName: customer.displayName,
|
customerName: customer.displayName,
|
||||||
aging: this.getCustomerAging(customer, trialBalance.balance),
|
current: this.formatTotalAmount(currentTotal),
|
||||||
total: {
|
aging: agingPeriods,
|
||||||
amount,
|
total: this.formatTotalAmount(amount),
|
||||||
formattedAmount,
|
|
||||||
currencyCode,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve customers walker.
|
* Retrieve customers report.
|
||||||
* @param {ICustomer[]} customers
|
* @param {ICustomer[]} customers
|
||||||
* @return {ARAgingSummaryCustomer[]}
|
* @return {IARAgingSummaryCustomer[]}
|
||||||
*/
|
*/
|
||||||
private customersWalker(customers: ICustomer[]): ARAgingSummaryCustomer[] {
|
private customersWalker(customers: ICustomer[]): IARAgingSummaryCustomer[] {
|
||||||
return customers
|
return customers
|
||||||
.map((customer: ICustomer) => this.customerMapper(customer))
|
.map((customer) => this.customerData(customer))
|
||||||
|
.filter(
|
||||||
// Filter customers that have zero total amount when `noneZero` is on.
|
(customer: IARAgingSummaryCustomer) =>
|
||||||
.filter((customer: ARAgingSummaryCustomer) =>
|
!(customer.total.total === 0 && this.query.noneZero)
|
||||||
!(customer.total.amount === 0 && this.query.noneZero),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve AR. aging summary report data.
|
* Retrieve A/R aging summary report data.
|
||||||
|
* @return {IARAgingSummaryData}
|
||||||
*/
|
*/
|
||||||
public reportData() {
|
public reportData(): IARAgingSummaryData {
|
||||||
return this.customersWalker(this.customers);
|
const customersAgingPeriods = this.customersWalker(this.contacts);
|
||||||
|
const totalAgingPeriods = this.getTotalAgingPeriods(customersAgingPeriods);
|
||||||
|
const totalCurrent = this.getTotalCurrent(customersAgingPeriods);
|
||||||
|
|
||||||
|
return {
|
||||||
|
customers: customersAgingPeriods,
|
||||||
|
total: {
|
||||||
|
current: this.formatTotalAmount(totalCurrent),
|
||||||
|
aging: totalAgingPeriods,
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve AR aging summary report columns.
|
* Retrieve AR aging summary report columns.
|
||||||
|
* @return {IARAgingSummaryColumns}
|
||||||
*/
|
*/
|
||||||
reportColumns() {
|
public reportColumns(): IARAgingSummaryColumns {
|
||||||
return []
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,75 +1,185 @@
|
|||||||
import moment from 'moment';
|
import { defaultTo, sumBy, get } from 'lodash';
|
||||||
import { omit, reverse } from 'lodash';
|
import {
|
||||||
import { IAgingPeriod, IAgingPeriodClosingBalance, IAgingPeriodTotal } from 'interfaces';
|
IAgingPeriod,
|
||||||
import FinancialSheet from '../FinancialSheet';
|
ISaleInvoice,
|
||||||
|
IBill,
|
||||||
|
IAgingPeriodTotal,
|
||||||
|
IARAgingSummaryCustomer,
|
||||||
|
IContact,
|
||||||
|
IARAgingSummaryQuery,
|
||||||
|
} 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 query: IARAgingSummaryQuery;
|
||||||
|
protected readonly overdueInvoicesByContactId: Dictionary<
|
||||||
|
(ISaleInvoice | IBill)[]
|
||||||
|
>;
|
||||||
|
protected readonly currentInvoicesByContactId: Dictionary<
|
||||||
|
(ISaleInvoice | IBill)[]
|
||||||
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Setes initial aging periods to the given customer id.
|
||||||
* @param {Array} agingPeriods
|
* @param {number} customerId - Customer id.
|
||||||
* @param {Numeric} customerBalance
|
|
||||||
*/
|
*/
|
||||||
contactAgingBalance(
|
protected getInitialAgingPeriodsTotal() {
|
||||||
agingPeriods: IAgingPeriodClosingBalance[],
|
return this.agingPeriods.map((agingPeriod) => ({
|
||||||
receivableTotalCredit: number,
|
...agingPeriod,
|
||||||
): IAgingPeriodTotal[] {
|
...this.formatTotalAmount(0),
|
||||||
let prevAging = 0;
|
}));
|
||||||
let receivableCredit = receivableTotalCredit;
|
}
|
||||||
let diff = receivableCredit;
|
|
||||||
|
|
||||||
const periods = reverse(agingPeriods).map((agingPeriod) => {
|
/**
|
||||||
const agingAmount = (agingPeriod.closingBalance - prevAging);
|
* Calculates the given contact aging periods.
|
||||||
const subtract = Math.min(diff, agingAmount);
|
* @param {ICustomer} customer
|
||||||
diff -= Math.min(agingAmount, diff);
|
* @return {(IAgingPeriod & IAgingPeriodTotal)[]}
|
||||||
|
*/
|
||||||
|
protected getContactAgingPeriods(
|
||||||
|
contactId: number
|
||||||
|
): (IAgingPeriod & IAgingPeriodTotal)[] {
|
||||||
|
const unpaidInvoices = this.getUnpaidInvoicesByContactId(contactId);
|
||||||
|
const initialAgingPeriods = this.getInitialAgingPeriodsTotal();
|
||||||
|
|
||||||
const total = Math.max(agingAmount - subtract, 0);
|
return unpaidInvoices.reduce((agingPeriods, unpaidInvoice) => {
|
||||||
|
const newAgingPeriods = this.getContactAgingDueAmount(
|
||||||
|
agingPeriods,
|
||||||
|
unpaidInvoice.dueAmount,
|
||||||
|
unpaidInvoice.getOverdueDays(this.query.asDate)
|
||||||
|
);
|
||||||
|
return newAgingPeriods;
|
||||||
|
}, initialAgingPeriods);
|
||||||
|
}
|
||||||
|
|
||||||
const output = {
|
/**
|
||||||
...omit(agingPeriod, ['closingBalance']),
|
* Sets the customer aging due amount to the table. (Xx)
|
||||||
total,
|
* @param {number} customerId - Customer id.
|
||||||
|
* @param {number} dueAmount - Due amount.
|
||||||
|
* @param {number} overdueDays - Overdue days.
|
||||||
|
*/
|
||||||
|
protected getContactAgingDueAmount(
|
||||||
|
agingPeriods: any,
|
||||||
|
dueAmount: number,
|
||||||
|
overdueDays: number
|
||||||
|
): (IAgingPeriod & IAgingPeriodTotal)[] {
|
||||||
|
const newAgingPeriods = agingPeriods.map((agingPeriod) => {
|
||||||
|
const isInAgingPeriod =
|
||||||
|
agingPeriod.beforeDays <= overdueDays &&
|
||||||
|
(agingPeriod.toDays > overdueDays || !agingPeriod.toDays);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...agingPeriod,
|
||||||
|
total: isInAgingPeriod
|
||||||
|
? agingPeriod.total + dueAmount
|
||||||
|
: agingPeriod.total,
|
||||||
};
|
};
|
||||||
prevAging = agingPeriod.closingBalance;
|
|
||||||
return output;
|
|
||||||
});
|
});
|
||||||
return reverse(periods);
|
return newAgingPeriods;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Retrieve the aging period total object.
|
||||||
* @param {*} asDay
|
* @param {number} amount
|
||||||
* @param {*} agingDaysBefore
|
* @return {IAgingPeriodTotal}
|
||||||
* @param {*} agingPeriodsFreq
|
|
||||||
*/
|
*/
|
||||||
agingRangePeriods(asDay, agingDaysBefore, agingPeriodsFreq): IAgingPeriod[] {
|
protected formatTotalAmount(amount: number): IAgingPeriodTotal {
|
||||||
const totalAgingDays = agingDaysBefore * agingPeriodsFreq;
|
return {
|
||||||
const startAging = moment(asDay).startOf('day');
|
total: amount,
|
||||||
const endAging = startAging.clone().subtract('days', totalAgingDays).endOf('day');
|
formattedTotal: this.formatNumber(amount),
|
||||||
|
currencyCode: this.baseCurrency,
|
||||||
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;
|
|
||||||
}
|
|
||||||
return agingPeriods;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
/**
|
||||||
|
* 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, customer) => {
|
||||||
|
const totalPeriod = contactsAgingPeriods[index]
|
||||||
|
? contactsAgingPeriods[index].total
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return acc + totalPeriod;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the due invoices by the given customer id.
|
||||||
|
* @param {number} customerId -
|
||||||
|
* @return {ISaleInvoice[]}
|
||||||
|
*/
|
||||||
|
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 & IAgingPeriod)[] {
|
||||||
|
return this.agingPeriods.map((agingPeriod, index) => {
|
||||||
|
const total = sumBy(contactsAgingPeriods, `aging[${index}].total`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...agingPeriod,
|
||||||
|
...this.formatTotalAmount(total),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the current invoices by the given contact id.
|
||||||
|
* @param {number} contactId
|
||||||
|
* @return {(ISaleInvoice | IBill)[]}
|
||||||
|
*/
|
||||||
|
protected getCurrentInvoicesByContactId(
|
||||||
|
contactId: number
|
||||||
|
): (ISaleInvoice | IBill)[] {
|
||||||
|
return get(this.currentInvoicesByContactId, contactId, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the contact total due amount.
|
||||||
|
* @param {number} contactId
|
||||||
|
* @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 customers sections.
|
||||||
|
* @param {IARAgingSummaryCustomer[]} contactsSections -
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
protected getTotalCurrent(
|
||||||
|
customersSummary: IARAgingSummaryCustomer[]
|
||||||
|
): number {
|
||||||
|
return sumBy(customersSummary, summary => summary.current.total);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the total of the given aging periods.
|
||||||
|
* @param {IAgingPeriodTotal[]} agingPeriods
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
protected getAgingPeriodsTotal(
|
||||||
|
agingPeriods: IAgingPeriodTotal[],
|
||||||
|
): number {
|
||||||
|
return sumBy(agingPeriods, 'total');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ import BalanceSheetStructure from 'data/BalanceSheetStructure';
|
|||||||
import FinancialSheet from '../FinancialSheet';
|
import FinancialSheet from '../FinancialSheet';
|
||||||
|
|
||||||
export default class BalanceSheetStatement extends FinancialSheet {
|
export default class BalanceSheetStatement extends FinancialSheet {
|
||||||
query: IBalanceSheetQuery;
|
readonly query: IBalanceSheetQuery;
|
||||||
tenantId: number;
|
readonly tenantId: number;
|
||||||
accounts: IAccount & { type: IAccountType }[];
|
readonly accounts: IAccount & { type: IAccountType }[];
|
||||||
journalFinancial: IJournalPoster;
|
readonly journalFinancial: IJournalPoster;
|
||||||
comparatorDateType: string;
|
readonly comparatorDateType: string;
|
||||||
dateRangeSet: string[];
|
readonly dateRangeSet: string[];
|
||||||
baseCurrency: string;
|
readonly baseCurrency: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor method.
|
* Constructor method.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Service, Inject } from "typedi";
|
import { Service, Inject } from 'typedi';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { ServiceError } from "exceptions";
|
import { ServiceError } from 'exceptions';
|
||||||
import { difference } from 'lodash';
|
import { difference } from 'lodash';
|
||||||
import { IGeneralLedgerSheetQuery } from 'interfaces';
|
import { IGeneralLedgerSheetQuery } from 'interfaces';
|
||||||
import TenancyService from 'services/Tenancy/TenancyService';
|
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';
|
import GeneralLedgerSheet from 'services/FinancialStatements/GeneralLedger/GeneralLedger';
|
||||||
|
|
||||||
const ERRORS = {
|
const ERRORS = {
|
||||||
@@ -39,8 +39,8 @@ export default class GeneralLedgerService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates accounts existance on the storage.
|
* Validates accounts existance on the storage.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {number[]} accountsIds
|
* @param {number[]} accountsIds
|
||||||
*/
|
*/
|
||||||
async validateAccountsExistance(tenantId: number, accountsIds: number[]) {
|
async validateAccountsExistance(tenantId: number, accountsIds: number[]) {
|
||||||
const { Account } = this.tenancy.models(tenantId);
|
const { Account } = this.tenancy.models(tenantId);
|
||||||
@@ -49,35 +49,42 @@ export default class GeneralLedgerService {
|
|||||||
const storedAccountsIds = storedAccounts.map((a) => a.id);
|
const storedAccountsIds = storedAccounts.map((a) => a.id);
|
||||||
|
|
||||||
if (difference(accountsIds, storedAccountsIds).length > 0) {
|
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.
|
* Retrieve general ledger report statement.
|
||||||
* ----------
|
* ----------
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {IGeneralLedgerSheetQuery} query
|
* @param {IGeneralLedgerSheetQuery} query
|
||||||
* @return {IGeneralLedgerStatement}
|
* @return {IGeneralLedgerStatement}
|
||||||
*/
|
*/
|
||||||
async generalLedger(tenantId: number, query: IGeneralLedgerSheetQuery):
|
async generalLedger(
|
||||||
Promise<{
|
tenantId: number,
|
||||||
data: any,
|
query: IGeneralLedgerSheetQuery
|
||||||
query: IGeneralLedgerSheetQuery,
|
): Promise<{
|
||||||
}> {
|
data: any;
|
||||||
|
query: IGeneralLedgerSheetQuery;
|
||||||
|
}> {
|
||||||
const {
|
const {
|
||||||
accountRepository,
|
accountRepository,
|
||||||
transactionsRepository,
|
transactionsRepository,
|
||||||
} = this.tenancy.repositories(tenantId);
|
} = this.tenancy.repositories(tenantId);
|
||||||
|
const settings = this.tenancy.settings(tenantId);
|
||||||
|
|
||||||
const filter = {
|
const filter = {
|
||||||
...this.defaultQuery,
|
...this.defaultQuery,
|
||||||
...query,
|
...query,
|
||||||
};
|
};
|
||||||
this.logger.info('[general_ledger] trying to calculate the report.', { tenantId, filter })
|
this.logger.info('[general_ledger] trying to calculate the report.', {
|
||||||
|
tenantId,
|
||||||
const settings = this.tenancy.settings(tenantId);
|
filter,
|
||||||
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' });
|
});
|
||||||
|
const baseCurrency = settings.get({
|
||||||
|
group: 'organization',
|
||||||
|
key: 'base_currency',
|
||||||
|
});
|
||||||
|
|
||||||
// Retrieve all accounts from the storage.
|
// Retrieve all accounts from the storage.
|
||||||
const accounts = await accountRepository.all('type');
|
const accounts = await accountRepository.all('type');
|
||||||
@@ -98,12 +105,22 @@ export default class GeneralLedgerService {
|
|||||||
toDate: filter.toDate,
|
toDate: filter.toDate,
|
||||||
sumationCreditDebit: true,
|
sumationCreditDebit: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Transform array transactions to journal collection.
|
// Transform array transactions to journal collection.
|
||||||
const transactionsJournal = Journal.fromTransactions(transactions, tenantId, accountsGraph);
|
const transactionsJournal = Journal.fromTransactions(
|
||||||
const openingTransJournal = Journal.fromTransactions(openingBalanceTrans, tenantId, accountsGraph);
|
transactions,
|
||||||
const closingTransJournal = Journal.fromTransactions(closingBalanceTrans, tenantId, accountsGraph);
|
tenantId,
|
||||||
|
accountsGraph
|
||||||
|
);
|
||||||
|
const openingTransJournal = Journal.fromTransactions(
|
||||||
|
openingBalanceTrans,
|
||||||
|
tenantId,
|
||||||
|
accountsGraph
|
||||||
|
);
|
||||||
|
const closingTransJournal = Journal.fromTransactions(
|
||||||
|
closingBalanceTrans,
|
||||||
|
tenantId,
|
||||||
|
accountsGraph
|
||||||
|
);
|
||||||
// General ledger report instance.
|
// General ledger report instance.
|
||||||
const generalLedgerInstance = new GeneralLedgerSheet(
|
const generalLedgerInstance = new GeneralLedgerSheet(
|
||||||
tenantId,
|
tenantId,
|
||||||
@@ -120,6 +137,6 @@ export default class GeneralLedgerService {
|
|||||||
return {
|
return {
|
||||||
data: reportData,
|
data: reportData,
|
||||||
query: filter,
|
query: filter,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default class BillsService extends SalesInvoicesCost {
|
|||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
tenancy: TenancyService;
|
tenancy: TenancyService;
|
||||||
|
|
||||||
@EventDispatcher()
|
@EventDispatcher()
|
||||||
eventDispatcher: EventDispatcherInterface;
|
eventDispatcher: EventDispatcherInterface;
|
||||||
|
|
||||||
@@ -206,7 +206,7 @@ export default class BillsService extends SalesInvoicesCost {
|
|||||||
billDTO: IBillDTO,
|
billDTO: IBillDTO,
|
||||||
authorizedUser: ISystemUser
|
authorizedUser: ISystemUser
|
||||||
): Promise<IBill> {
|
): Promise<IBill> {
|
||||||
const { Bill } = this.tenancy.models(tenantId);
|
const { billRepository } = this.tenancy.repositories(tenantId);
|
||||||
|
|
||||||
this.logger.info('[bill] trying to create a new bill', {
|
this.logger.info('[bill] trying to create a new bill', {
|
||||||
tenantId,
|
tenantId,
|
||||||
@@ -236,7 +236,7 @@ export default class BillsService extends SalesInvoicesCost {
|
|||||||
billDTO.entries
|
billDTO.entries
|
||||||
);
|
);
|
||||||
// Inserts the bill graph object to the storage.
|
// Inserts the bill graph object to the storage.
|
||||||
const bill = await Bill.query().insertGraph({ ...billObj });
|
const bill = await billRepository.upsertGraph({ ...billObj });
|
||||||
|
|
||||||
// Triggers `onBillCreated` event.
|
// Triggers `onBillCreated` event.
|
||||||
await this.eventDispatcher.dispatch(events.bill.onCreated, {
|
await this.eventDispatcher.dispatch(events.bill.onCreated, {
|
||||||
@@ -275,7 +275,7 @@ export default class BillsService extends SalesInvoicesCost {
|
|||||||
billDTO: IBillEditDTO,
|
billDTO: IBillEditDTO,
|
||||||
authorizedUser: ISystemUser
|
authorizedUser: ISystemUser
|
||||||
): Promise<IBill> {
|
): Promise<IBill> {
|
||||||
const { Bill } = this.tenancy.models(tenantId);
|
const { billRepository } = this.tenancy.repositories(tenantId);
|
||||||
|
|
||||||
this.logger.info('[bill] trying to edit bill.', { tenantId, billId });
|
this.logger.info('[bill] trying to edit bill.', { tenantId, billId });
|
||||||
const oldBill = await this.getBillOrThrowError(tenantId, billId);
|
const oldBill = await this.getBillOrThrowError(tenantId, billId);
|
||||||
@@ -314,7 +314,7 @@ export default class BillsService extends SalesInvoicesCost {
|
|||||||
billDTO.entries
|
billDTO.entries
|
||||||
);
|
);
|
||||||
// Update the bill transaction.
|
// Update the bill transaction.
|
||||||
const bill = await Bill.query().upsertGraphAndFetch({
|
const bill = await billRepository.upsertGraph({
|
||||||
id: billId,
|
id: billId,
|
||||||
...billObj,
|
...billObj,
|
||||||
});
|
});
|
||||||
@@ -339,7 +339,8 @@ export default class BillsService extends SalesInvoicesCost {
|
|||||||
* @return {void}
|
* @return {void}
|
||||||
*/
|
*/
|
||||||
public async deleteBill(tenantId: number, billId: number) {
|
public async deleteBill(tenantId: number, billId: number) {
|
||||||
const { Bill, ItemEntry } = this.tenancy.models(tenantId);
|
const { ItemEntry } = this.tenancy.models(tenantId);
|
||||||
|
const { billRepository } = this.tenancy.repositories(tenantId);
|
||||||
|
|
||||||
// Retrieve the given bill or throw not found error.
|
// Retrieve the given bill or throw not found error.
|
||||||
const oldBill = await this.getBillOrThrowError(tenantId, billId);
|
const oldBill = await this.getBillOrThrowError(tenantId, billId);
|
||||||
@@ -351,7 +352,7 @@ export default class BillsService extends SalesInvoicesCost {
|
|||||||
.delete();
|
.delete();
|
||||||
|
|
||||||
// Delete the bill transaction.
|
// Delete the bill transaction.
|
||||||
const deleteBillOper = Bill.query().where('id', billId).delete();
|
const deleteBillOper = billRepository.deleteById(billId);
|
||||||
|
|
||||||
await Promise.all([deleteBillEntriesOper, deleteBillOper]);
|
await Promise.all([deleteBillEntriesOper, deleteBillOper]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user