This commit is contained in:
elforjani3
2021-01-09 13:56:48 +02:00
27 changed files with 870 additions and 646 deletions

View File

@@ -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;
}

View File

@@ -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);
}
}
}

View File

@@ -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(
@@ -41,7 +41,7 @@ export default class ARAgingSummaryReportController extends BaseController {
],
[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.
*/
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);

View File

@@ -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;
};
}
}

View File

@@ -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) {
}
}

View File

@@ -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),
})),
],
},
});
}
}

View File

@@ -321,7 +321,6 @@ export default class SaleInvoicesController extends BaseController {
tenantId,
customerId
);
return res.status(200).send({
sales_invoices: this.transfromToResponse(salesInvoices),
});

View 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[];

View File

@@ -1,45 +1,34 @@
import {
IAgingPeriod,
IAgingPeriodTotal
} from './AgingReport';
export interface IARAgingSummaryQuery {
asDate: Date | string,
agingDaysBefore: number,
agingPeriods: number,
asDate: Date | string;
agingDaysBefore: number;
agingPeriods: number;
numberFormat: {
noCents: number,
divideOn1000: number,
},
customersIds: number[],
noneZero: boolean,
noCents: boolean;
divideOn1000: boolean;
};
customersIds: number[];
noneZero: boolean;
}
export interface IAgingPeriod {
fromPeriod: Date,
toPeriod: Date,
beforeDays: number,
toDays: number,
};
export interface IAgingPeriodClosingBalance extends IAgingPeriod {
closingBalance: number,
};
export interface IAgingPeriodTotal extends IAgingPeriod {
total: number,
};
export interface ARAgingSummaryCustomerPeriod {
export interface IARAgingSummaryCustomer {
customerName: string;
current: IAgingPeriodTotal,
aging: (IAgingPeriodTotal & IAgingPeriod)[];
total: IAgingPeriodTotal;
}
export interface ARAgingSummaryCustomerTotal {
amount: number,
formattedAmount: string,
currencyCode: string,
export interface IARAgingSummaryTotal {
current: IAgingPeriodTotal,
aging: (IAgingPeriodTotal & IAgingPeriod)[],
};
export interface IARAgingSummaryData {
customers: IARAgingSummaryCustomer[],
total: IARAgingSummaryTotal,
}
export interface ARAgingSummaryCustomer {
customerName: string,
aging: IAgingPeriodTotal[],
total: ARAgingSummaryCustomerTotal,
};
export type IARAgingSummaryColumns = IAgingPeriod[];

View 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;
}

View File

@@ -42,6 +42,9 @@ export interface IBill {
amount: number,
paymentAmount: number,
dueAmount: number,
overdueDays: number,
invLotNumber: string,
openedAt: Date | string,

View File

@@ -7,6 +7,7 @@ export interface ISaleInvoice {
invoiceDate: Date,
dueDate: Date,
dueAmount: number,
overdueDays: number,
customerId: number,
entries: IItemEntry[],
deliveredAt: string | Date,

View File

@@ -35,5 +35,7 @@ export * from './TrialBalanceSheet';
export * from './GeneralLedgerSheet'
export * from './ProfitLossSheet';
export * from './JournalReport';
export * from './AgingReport';
export * from './ARAgingSummaryReport';
export * from './APAgingSummaryReport';
export * from './Mailable';

View File

@@ -48,6 +48,12 @@ export default class Bill extends TenantModel {
overdue(query) {
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.
*/
@@ -61,7 +67,13 @@ export default class Bill extends TenantModel {
paid(query) {
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'];
}
/**
/**
* Virtual attributes.
*/
static get virtualAttributes() {
@@ -117,7 +129,7 @@ export default class Bill extends TenantModel {
*/
get isFullyPaid() {
return this.dueAmount === 0;
}
}
/**
* Detarmines whether the bill paid fully or partially.
@@ -133,7 +145,9 @@ export default class Bill extends TenantModel {
*/
get remainingDays() {
// Can't continue in case due date not defined.
if (!this.dueDate) { return null; }
if (!this.dueDate) {
return null;
}
const date = moment();
const dueDate = moment(this.dueDate);
@@ -146,13 +160,7 @@ export default class Bill extends TenantModel {
* @return {number|null}
*/
get overdueDays() {
// Can't continue in case due date not defined.
if (!this.dueDate) { return null; }
const date = moment();
const dueDate = moment(this.dueDate);
return Math.max(date.diff(dueDate, 'days'), 0);
return this.getOverdueDays();
}
/**
@@ -163,6 +171,17 @@ export default class Bill extends TenantModel {
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.
*/
@@ -180,7 +199,7 @@ export default class Bill extends TenantModel {
},
filter(query) {
query.where('contact_service', 'vendor');
}
},
},
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.
* @param {Array} billsIds
* @param {number} vendorId -
* @param {Array} billsIds
* @param {number} vendorId -
* @return {Array}
*/
static async getNotFoundBills(billsIds, vendorId) {
const storedBills = await this.query()
.onBuild((builder) => {
builder.whereIn('id', billsIds);
const storedBills = await this.query().onBuild((builder) => {
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 notFoundBillsIds = difference(
billsIds,
storedBillsIds,
);
const notFoundBillsIds = difference(billsIds, storedBillsIds);
return notFoundBillsIds;
}
@@ -263,19 +278,25 @@ export default class Bill extends TenantModel {
label: 'Status',
options: [],
query: (query, role) => {
switch(role.value) {
switch (role.value) {
case 'draft':
query.modify('draft'); break;
query.modify('draft');
break;
case 'opened':
query.modify('opened'); break;
query.modify('opened');
break;
case 'unpaid':
query.modify('unpaid'); break;
query.modify('unpaid');
break;
case 'overdue':
query.modify('overdue'); break;
query.modify('overdue');
break;
case 'partially-paid':
query.modify('partiallyPaid'); break;
query.modify('partiallyPaid');
break;
case 'paid':
query.modify('paid'); break;
query.modify('paid');
break;
}
},
},
@@ -295,14 +316,12 @@ export default class Bill extends TenantModel {
label: 'Note',
column: 'note',
},
user: {
},
user: {},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
},
}
};
}
}

View File

@@ -103,19 +103,27 @@ export default class SaleInvoice extends TenantModel {
* @return {number|null}
*/
get overdueDays() {
// Can't continue in case due date not defined.
if (!this.dueDate) { return null; }
const date = moment();
const dueDate = moment(this.dueDate);
return Math.max(date.diff(dueDate, 'days'), 0);
return this.getOverdueDays();
}
static get resourceable() {
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.
*/
@@ -163,8 +171,14 @@ export default class SaleInvoice extends TenantModel {
/**
* Filters the overdue invoices.
*/
overdue(query) {
query.where('due_date', '<', moment().format('YYYY-MM-DD'));
overdue(query, asDate = 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.
@@ -178,6 +192,12 @@ export default class SaleInvoice extends TenantModel {
*/
paid(query) {
query.where(raw('PAYMENT_AMOUNT = BALANCE'));
},
/**
* Filters the sale invoices from the given date.
*/
fromDate(query, fromDate) {
query.where('invoice_date', '<=', fromDate)
}
};
}

View File

@@ -1,3 +1,4 @@
import moment from 'moment';
import { Bill } from 'models';
import TenantRepository from 'repositories/TenantRepository';
@@ -8,4 +9,30 @@ export default class BillRepository extends TenantRepository {
get model() {
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);
})
}
}

View File

@@ -1,3 +1,4 @@
import moment from 'moment';
import { SaleInvoice } from 'models';
import TenantRepository from 'repositories/TenantRepository';
@@ -8,4 +9,30 @@ export default class SaleInvoiceRepository extends TenantRepository {
get model() {
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);
});
}
}

View File

@@ -291,8 +291,6 @@ export default class JournalCommands {
referenceType: ['SaleInvoice'],
index: [3, 4],
});
console.log(transactions);
this.journal.fromTransactions(transactions);
this.journal.removeEntries();
}

View File

@@ -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 {
@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 };
}
}

View File

@@ -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;
}
}

View File

@@ -2,8 +2,8 @@ import moment from 'moment';
import { Inject, Service } from 'typedi';
import { IARAgingSummaryQuery } from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService';
import Journal from 'services/Accounting/JournalPoster';
import ARAgingSummarySheet from './ARAgingSummarySheet';
import SaleInvoiceRepository from 'repositories/SaleInvoiceRepository';
@Service()
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 query
*
* @param {number} tenantId
* @param query
*/
async ARAgingSummary(tenantId: number, query: IARAgingSummaryQuery) {
const {
customerRepository,
accountRepository,
transactionsRepository,
accountTypeRepository
saleInvoiceRepository,
} = this.tenancy.repositories(tenantId);
const { Account } = this.tenancy.models(tenantId);
const filter = {
...this.defaultQuery,
...query,
};
this.logger.info('[AR_Aging_Summary] try to calculate the report.', { tenantId, filter });
this.logger.info('[AR_Aging_Summary] try to calculate the report.', {
tenantId,
filter,
});
// Settings tenant service.
const settings = this.tenancy.settings(tenantId);
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' });
// Retrieve all accounts graph on the storage.
const accountsGraph = await accountRepository.getDependencyGraph();
const baseCurrency = settings.get({
group: 'organization',
key: 'base_currency',
});
// Retrieve all customers from the storage.
const customers = await customerRepository.all();
// Retrieve AR account type.
const ARType = await accountTypeRepository.getByKey('accounts_receivable');
// Retreive AR account.
const ARAccount = await Account.query().findOne('account_type_id', ARType.id);
// Retrieve journal transactions based on the given query.
const transactions = await transactionsRepository.journal({
toDate: filter.asDate,
contactType: 'customer',
contactsIds: customers.map(customer => customer.id),
});
// Converts transactions array to journal collection.
const journal = Journal.fromTransactions(transactions, tenantId, accountsGraph);
// AR aging summary report instnace.
// Retrieve all overdue sale invoices.
const overdueSaleInvoices = await saleInvoiceRepository.overdueInvoices(
filter.asDate
);
// Retrieve all due sale invoices.
const currentInvoices = await saleInvoiceRepository.dueInvoices(
filter.asDate
);
// AR aging summary report instance.
const ARAgingSummaryReport = new ARAgingSummarySheet(
tenantId,
filter,
customers,
journal,
ARAccount,
overdueSaleInvoices,
currentInvoices,
baseCurrency
);
// AR aging summary report data and columns.
const data = ARAgingSummaryReport.reportData();
const columns = ARAgingSummaryReport.reportColumns();
return { data, columns };
return { data, columns, query: filter };
}
}
}

View File

@@ -1,57 +1,53 @@
import { groupBy, sum } from 'lodash';
import {
ICustomer,
IARAgingSummaryQuery,
ARAgingSummaryCustomer,
IAgingPeriodClosingBalance,
IAgingPeriodTotal,
IJournalPoster,
IAccount,
IAgingPeriod
} from "interfaces";
IARAgingSummaryCustomer,
IAgingPeriod,
ISaleInvoice,
IARAgingSummaryData,
IARAgingSummaryColumns,
} from 'interfaces';
import AgingSummaryReport from './AgingSummary';
import { Dictionary } from 'tsyringe/dist/typings/types';
export default class ARAgingSummarySheet extends AgingSummaryReport {
tenantId: number;
query: IARAgingSummaryQuery;
customers: ICustomer[];
journal: IJournalPoster;
ARAccount: IAccount;
agingPeriods: IAgingPeriod[];
baseCurrency: string;
readonly tenantId: number;
readonly query: IARAgingSummaryQuery;
readonly contacts: ICustomer[];
readonly agingPeriods: IAgingPeriod[];
readonly baseCurrency: string;
readonly overdueInvoicesByContactId: Dictionary<ISaleInvoice[]>;
readonly currentInvoicesByContactId: Dictionary<ISaleInvoice[]>;
/**
* Constructor method.
* @param {number} tenantId
* @param {IARAgingSummaryQuery} query
* @param {ICustomer[]} customers
* @param {IJournalPoster} journal
* @param {number} tenantId
* @param {IARAgingSummaryQuery} query
* @param {ICustomer[]} customers
* @param {IJournalPoster} journal
*/
constructor(
tenantId: number,
query: IARAgingSummaryQuery,
customers: ICustomer[],
journal: IJournalPoster,
ARAccount: IAccount,
baseCurrency: string,
overdueSaleInvoices: ISaleInvoice[],
currentSaleInvoices: ISaleInvoice[],
baseCurrency: string
) {
super();
this.tenantId = tenantId;
this.customers = customers;
this.contacts = customers;
this.query = query;
this.numberFormat = this.query.numberFormat;
this.journal = journal;
this.ARAccount = ARAccount;
this.baseCurrency = baseCurrency;
this.numberFormat = this.query.numberFormat;
this.initAgingPeriod();
}
this.overdueInvoicesByContactId = groupBy(overdueSaleInvoices, 'customerId');
this.currentInvoicesByContactId = groupBy(currentSaleInvoices, 'customerId');
/**
* Initializes the aging periods.
*/
private initAgingPeriod() {
// Initializes the aging periods.
this.agingPeriods = this.agingRangePeriods(
this.query.asDate,
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.
* @param {ICustomer} customer -
* @return {ARAgingSummaryCustomer[]}
* @return {IARAgingSummaryCustomer[]}
*/
private customerMapper(customer: ICustomer): ARAgingSummaryCustomer {
// Calculate the trial balance total of the given customer.
const trialBalance = this.journal.getContactTrialBalance(
this.ARAccount.id,
customer.id,
'customer'
);
const amount = trialBalance.balance;
const formattedAmount = this.formatNumber(amount);
const currencyCode = this.baseCurrency;
private customerData(customer: ICustomer): IARAgingSummaryCustomer {
const agingPeriods = this.getContactAgingPeriods(customer.id);
const currentTotal = this.getContactCurrentTotal(customer.id);
const agingPeriodsTotal = this.getAgingPeriodsTotal(agingPeriods);
const amount = sum([agingPeriodsTotal, currentTotal]);
return {
customerName: customer.displayName,
aging: this.getCustomerAging(customer, trialBalance.balance),
total: {
amount,
formattedAmount,
currencyCode,
},
current: this.formatTotalAmount(currentTotal),
aging: agingPeriods,
total: this.formatTotalAmount(amount),
};
}
/**
* Retrieve customers walker.
* @param {ICustomer[]} customers
* @return {ARAgingSummaryCustomer[]}
* Retrieve customers report.
* @param {ICustomer[]} customers
* @return {IARAgingSummaryCustomer[]}
*/
private customersWalker(customers: ICustomer[]): ARAgingSummaryCustomer[] {
private customersWalker(customers: ICustomer[]): IARAgingSummaryCustomer[] {
return customers
.map((customer: ICustomer) => this.customerMapper(customer))
// Filter customers that have zero total amount when `noneZero` is on.
.filter((customer: ARAgingSummaryCustomer) =>
!(customer.total.amount === 0 && this.query.noneZero),
.map((customer) => this.customerData(customer))
.filter(
(customer: IARAgingSummaryCustomer) =>
!(customer.total.total === 0 && this.query.noneZero)
);
}
/**
* Retrieve AR. aging summary report data.
* Retrieve A/R aging summary report data.
* @return {IARAgingSummaryData}
*/
public reportData() {
return this.customersWalker(this.customers);
public reportData(): IARAgingSummaryData {
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.
* @return {IARAgingSummaryColumns}
*/
reportColumns() {
return []
public reportColumns(): IARAgingSummaryColumns {
return this.agingPeriods;
}
}
}

View File

@@ -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;
}
}

View File

@@ -1,75 +1,185 @@
import moment from 'moment';
import { omit, reverse } from 'lodash';
import { IAgingPeriod, IAgingPeriodClosingBalance, IAgingPeriodTotal } from 'interfaces';
import FinancialSheet from '../FinancialSheet';
import { defaultTo, sumBy, get } from 'lodash';
import {
IAgingPeriod,
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)[]
>;
/**
*
* @param {Array} agingPeriods
* @param {Numeric} customerBalance
* Setes initial aging periods to the given customer id.
* @param {number} customerId - Customer id.
*/
contactAgingBalance(
agingPeriods: IAgingPeriodClosingBalance[],
receivableTotalCredit: number,
): IAgingPeriodTotal[] {
let prevAging = 0;
let receivableCredit = receivableTotalCredit;
let diff = receivableCredit;
protected getInitialAgingPeriodsTotal() {
return this.agingPeriods.map((agingPeriod) => ({
...agingPeriod,
...this.formatTotalAmount(0),
}));
}
const periods = reverse(agingPeriods).map((agingPeriod) => {
const agingAmount = (agingPeriod.closingBalance - prevAging);
const subtract = Math.min(diff, agingAmount);
diff -= Math.min(agingAmount, diff);
/**
* Calculates the given contact aging periods.
* @param {ICustomer} customer
* @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']),
total,
/**
* Sets the customer aging due amount to the table. (Xx)
* @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;
}
/**
*
* @param {*} asDay
* @param {*} agingDaysBefore
* @param {*} agingPeriodsFreq
* Retrieve the aging period total object.
* @param {number} amount
* @return {IAgingPeriodTotal}
*/
agingRangePeriods(asDay, agingDaysBefore, agingPeriodsFreq): IAgingPeriod[] {
const totalAgingDays = agingDaysBefore * agingPeriodsFreq;
const startAging = moment(asDay).startOf('day');
const endAging = startAging.clone().subtract('days', totalAgingDays).endOf('day');
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;
protected formatTotalAmount(amount: number): IAgingPeriodTotal {
return {
total: amount,
formattedTotal: this.formatNumber(amount),
currencyCode: this.baseCurrency,
};
}
}
/**
* Calculates the total of the aging period by the given index.
* @param {number} index
* @return {number}
*/
protected getTotalAgingPeriodByIndex(
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');
}
}

View File

@@ -14,13 +14,13 @@ import BalanceSheetStructure from 'data/BalanceSheetStructure';
import FinancialSheet from '../FinancialSheet';
export default class BalanceSheetStatement extends FinancialSheet {
query: IBalanceSheetQuery;
tenantId: number;
accounts: IAccount & { type: IAccountType }[];
journalFinancial: IJournalPoster;
comparatorDateType: string;
dateRangeSet: string[];
baseCurrency: string;
readonly query: IBalanceSheetQuery;
readonly tenantId: number;
readonly accounts: IAccount & { type: IAccountType }[];
readonly journalFinancial: IJournalPoster;
readonly comparatorDateType: string;
readonly dateRangeSet: string[];
readonly baseCurrency: string;
/**
* Constructor method.

View File

@@ -1,10 +1,10 @@
import { Service, Inject } from "typedi";
import { Service, Inject } from 'typedi';
import moment from 'moment';
import { ServiceError } from "exceptions";
import { ServiceError } from 'exceptions';
import { difference } from 'lodash';
import { IGeneralLedgerSheetQuery } from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService';
import Journal from "services/Accounting/JournalPoster";
import Journal from 'services/Accounting/JournalPoster';
import GeneralLedgerSheet from 'services/FinancialStatements/GeneralLedger/GeneralLedger';
const ERRORS = {
@@ -39,8 +39,8 @@ export default class GeneralLedgerService {
/**
* Validates accounts existance on the storage.
* @param {number} tenantId
* @param {number[]} accountsIds
* @param {number} tenantId
* @param {number[]} accountsIds
*/
async validateAccountsExistance(tenantId: number, accountsIds: number[]) {
const { Account } = this.tenancy.models(tenantId);
@@ -49,35 +49,42 @@ export default class GeneralLedgerService {
const storedAccountsIds = storedAccounts.map((a) => a.id);
if (difference(accountsIds, storedAccountsIds).length > 0) {
throw new ServiceError(ERRORS.ACCOUNTS_NOT_FOUND)
throw new ServiceError(ERRORS.ACCOUNTS_NOT_FOUND);
}
}
/**
* Retrieve general ledger report statement.
* ----------
* @param {number} tenantId
* @param {IGeneralLedgerSheetQuery} query
* @param {number} tenantId
* @param {IGeneralLedgerSheetQuery} query
* @return {IGeneralLedgerStatement}
*/
async generalLedger(tenantId: number, query: IGeneralLedgerSheetQuery):
Promise<{
data: any,
query: IGeneralLedgerSheetQuery,
}> {
async generalLedger(
tenantId: number,
query: IGeneralLedgerSheetQuery
): Promise<{
data: any;
query: IGeneralLedgerSheetQuery;
}> {
const {
accountRepository,
transactionsRepository,
} = this.tenancy.repositories(tenantId);
const settings = this.tenancy.settings(tenantId);
const filter = {
...this.defaultQuery,
...query,
};
this.logger.info('[general_ledger] trying to calculate the report.', { tenantId, filter })
const settings = this.tenancy.settings(tenantId);
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' });
this.logger.info('[general_ledger] trying to calculate the report.', {
tenantId,
filter,
});
const baseCurrency = settings.get({
group: 'organization',
key: 'base_currency',
});
// Retrieve all accounts from the storage.
const accounts = await accountRepository.all('type');
@@ -98,12 +105,22 @@ export default class GeneralLedgerService {
toDate: filter.toDate,
sumationCreditDebit: true,
});
// Transform array transactions to journal collection.
const transactionsJournal = Journal.fromTransactions(transactions, tenantId, accountsGraph);
const openingTransJournal = Journal.fromTransactions(openingBalanceTrans, tenantId, accountsGraph);
const closingTransJournal = Journal.fromTransactions(closingBalanceTrans, tenantId, accountsGraph);
const transactionsJournal = Journal.fromTransactions(
transactions,
tenantId,
accountsGraph
);
const openingTransJournal = Journal.fromTransactions(
openingBalanceTrans,
tenantId,
accountsGraph
);
const closingTransJournal = Journal.fromTransactions(
closingBalanceTrans,
tenantId,
accountsGraph
);
// General ledger report instance.
const generalLedgerInstance = new GeneralLedgerSheet(
tenantId,
@@ -120,6 +137,6 @@ export default class GeneralLedgerService {
return {
data: reportData,
query: filter,
}
};
}
}
}

View File

@@ -56,7 +56,7 @@ export default class BillsService extends SalesInvoicesCost {
@Inject()
tenancy: TenancyService;
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
@@ -206,7 +206,7 @@ export default class BillsService extends SalesInvoicesCost {
billDTO: IBillDTO,
authorizedUser: ISystemUser
): Promise<IBill> {
const { Bill } = this.tenancy.models(tenantId);
const { billRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[bill] trying to create a new bill', {
tenantId,
@@ -236,7 +236,7 @@ export default class BillsService extends SalesInvoicesCost {
billDTO.entries
);
// Inserts the bill graph object to the storage.
const bill = await Bill.query().insertGraph({ ...billObj });
const bill = await billRepository.upsertGraph({ ...billObj });
// Triggers `onBillCreated` event.
await this.eventDispatcher.dispatch(events.bill.onCreated, {
@@ -275,7 +275,7 @@ export default class BillsService extends SalesInvoicesCost {
billDTO: IBillEditDTO,
authorizedUser: ISystemUser
): Promise<IBill> {
const { Bill } = this.tenancy.models(tenantId);
const { billRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[bill] trying to edit bill.', { tenantId, billId });
const oldBill = await this.getBillOrThrowError(tenantId, billId);
@@ -314,7 +314,7 @@ export default class BillsService extends SalesInvoicesCost {
billDTO.entries
);
// Update the bill transaction.
const bill = await Bill.query().upsertGraphAndFetch({
const bill = await billRepository.upsertGraph({
id: billId,
...billObj,
});
@@ -339,7 +339,8 @@ export default class BillsService extends SalesInvoicesCost {
* @return {void}
*/
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.
const oldBill = await this.getBillOrThrowError(tenantId, billId);
@@ -351,7 +352,7 @@ export default class BillsService extends SalesInvoicesCost {
.delete();
// Delete the bill transaction.
const deleteBillOper = Bill.query().where('id', billId).delete();
const deleteBillOper = billRepository.deleteById(billId);
await Promise.all([deleteBillEntriesOper, deleteBillOper]);