feat: AR/AP aging summary report.

This commit is contained in:
a.bouhuolia
2021-01-09 13:37:53 +02:00
parent 09b2aa57a0
commit 40afb108e3
18 changed files with 283 additions and 100 deletions

View File

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

View File

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

View File

@@ -17,13 +17,19 @@ export interface IAPAgingSummaryQuery {
export interface IAPAgingSummaryVendor { export interface IAPAgingSummaryVendor {
vendorName: string, vendorName: string,
current: IAgingPeriodTotal,
aging: (IAgingPeriod & IAgingPeriodTotal)[], aging: (IAgingPeriod & IAgingPeriodTotal)[],
total: IAgingPeriodTotal, total: IAgingPeriodTotal,
} };
export interface IAPAgingSummaryTotal {
current: IAgingPeriodTotal,
aging: (IAgingPeriodTotal & IAgingPeriod)[],
};
export interface IAPAgingSummaryData { export interface IAPAgingSummaryData {
vendors: IAPAgingSummaryVendor[], vendors: IAPAgingSummaryVendor[],
total: (IAgingPeriod & IAgingPeriodTotal)[], total: IAPAgingSummaryTotal,
} };
export type IAPAgingSummaryColumns = IAgingPeriod[]; export type IAPAgingSummaryColumns = IAgingPeriod[];

View File

@@ -8,8 +8,8 @@ export interface IARAgingSummaryQuery {
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;
@@ -17,13 +17,18 @@ export interface IARAgingSummaryQuery {
export interface IARAgingSummaryCustomer { export interface IARAgingSummaryCustomer {
customerName: string; customerName: string;
current: IAgingPeriodTotal,
aging: (IAgingPeriodTotal & IAgingPeriod)[]; aging: (IAgingPeriodTotal & IAgingPeriod)[];
total: IAgingPeriodTotal; total: IAgingPeriodTotal;
} }
export interface IARAgingSummaryTotal {
current: IAgingPeriodTotal,
aging: (IAgingPeriodTotal & IAgingPeriod)[],
};
export interface IARAgingSummaryData { export interface IARAgingSummaryData {
customers: IARAgingSummaryCustomer[], customers: IARAgingSummaryCustomer[],
total: (IAgingPeriodTotal & IAgingPeriod)[] total: IARAgingSummaryTotal,
} }
export type IARAgingSummaryColumns = IAgingPeriod[]; export type IARAgingSummaryColumns = IAgingPeriod[];

View File

@@ -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,

View File

@@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,6 +36,7 @@ export default class PayableAgingSummaryService {
async APAgingSummary(tenantId: number, query) { async APAgingSummary(tenantId: number, query) {
const { const {
vendorRepository, vendorRepository,
billRepository
} = this.tenancy.repositories(tenantId); } = this.tenancy.repositories(tenantId);
const { Bill } = this.tenancy.models(tenantId); const { Bill } = this.tenancy.models(tenantId);
@@ -55,15 +56,19 @@ export default class PayableAgingSummaryService {
// Retrieve all vendors from the storage. // Retrieve all vendors from the storage.
const vendors = await vendorRepository.all(); const vendors = await vendorRepository.all();
// Retrieve all unpaid vendors bills. // Retrieve all overdue vendors bills.
const unpaidBills = await Bill.query().modify('unpaid'); const overdueBills = await billRepository.overdueBills(
filter.asDate,
);
const dueBills = await billRepository.dueBills(filter.asDate);
// A/P aging summary report instance. // A/P aging summary report instance.
const APAgingSummaryReport = new APAgingSummarySheet( const APAgingSummaryReport = new APAgingSummarySheet(
tenantId, tenantId,
filter, filter,
vendors, vendors,
unpaidBills, overdueBills,
dueBills,
baseCurrency, baseCurrency,
); );
// A/P aging summary report data and columns. // A/P aging summary report data and columns.

View File

@@ -1,4 +1,4 @@
import { groupBy, sumBy } from 'lodash'; import { groupBy, sum } from 'lodash';
import AgingSummaryReport from './AgingSummary'; import AgingSummaryReport from './AgingSummary';
import { import {
IAPAgingSummaryQuery, IAPAgingSummaryQuery,
@@ -17,7 +17,9 @@ export default class APAgingSummarySheet extends AgingSummaryReport {
readonly unpaidBills: IBill[]; readonly unpaidBills: IBill[];
readonly baseCurrency: string; readonly baseCurrency: string;
readonly unpaidInvoicesByContactId: Dictionary<IBill[]>; readonly overdueInvoicesByContactId: Dictionary<IBill[]>;
readonly currentInvoicesByContactId: Dictionary<IBill[]>;
readonly agingPeriods: IAgingPeriod[]; readonly agingPeriods: IAgingPeriod[];
/** /**
@@ -31,6 +33,7 @@ export default class APAgingSummarySheet extends AgingSummaryReport {
tenantId: number, tenantId: number,
query: IAPAgingSummaryQuery, query: IAPAgingSummaryQuery,
vendors: IVendor[], vendors: IVendor[],
overdueBills: IBill[],
unpaidBills: IBill[], unpaidBills: IBill[],
baseCurrency: string baseCurrency: string
) { ) {
@@ -40,10 +43,10 @@ export default class APAgingSummarySheet extends AgingSummaryReport {
this.query = query; this.query = query;
this.numberFormat = this.query.numberFormat; this.numberFormat = this.query.numberFormat;
this.contacts = vendors; this.contacts = vendors;
this.unpaidBills = unpaidBills;
this.baseCurrency = baseCurrency; this.baseCurrency = baseCurrency;
this.unpaidInvoicesByContactId = groupBy(unpaidBills, 'vendorId'); this.overdueInvoicesByContactId = groupBy(overdueBills, 'vendorId');
this.currentInvoicesByContactId = groupBy(unpaidBills, 'vendorId');
// Initializes the aging periods. // Initializes the aging periods.
this.agingPeriods = this.agingRangePeriods( this.agingPeriods = this.agingRangePeriods(
@@ -60,10 +63,14 @@ export default class APAgingSummarySheet extends AgingSummaryReport {
*/ */
private vendorData(vendor: IVendor): IAPAgingSummaryVendor { private vendorData(vendor: IVendor): IAPAgingSummaryVendor {
const agingPeriods = this.getContactAgingPeriods(vendor.id); const agingPeriods = this.getContactAgingPeriods(vendor.id);
const amount = sumBy(agingPeriods, 'total'); const currentTotal = this.getContactCurrentTotal(vendor.id);
const agingPeriodsTotal = this.getAgingPeriodsTotal(agingPeriods);
const amount = sum([agingPeriodsTotal, currentTotal]);
return { return {
vendorName: vendor.displayName, vendorName: vendor.displayName,
current: this.formatTotalAmount(currentTotal),
aging: agingPeriods, aging: agingPeriods,
total: this.formatTotalAmount(amount), total: this.formatTotalAmount(amount),
}; };
@@ -89,17 +96,21 @@ export default class APAgingSummarySheet extends AgingSummaryReport {
public reportData(): IAPAgingSummaryData { public reportData(): IAPAgingSummaryData {
const vendorsAgingPeriods = this.vendorsWalker(this.contacts); const vendorsAgingPeriods = this.vendorsWalker(this.contacts);
const totalAgingPeriods = this.getTotalAgingPeriods(vendorsAgingPeriods); const totalAgingPeriods = this.getTotalAgingPeriods(vendorsAgingPeriods);
const totalCurrent = this.getTotalCurrent(vendorsAgingPeriods);
return { return {
vendors: vendorsAgingPeriods, vendors: vendorsAgingPeriods,
total: totalAgingPeriods, total: {
current: this.formatTotalAmount(totalCurrent),
aging: totalAgingPeriods,
},
} }
} }
/** /**
* Retrieve the A/P aging summary report columns. * Retrieve the A/P aging summary report columns.
*/ */
reportColumns(): IAPAgingSummaryColumns { public reportColumns(): IAPAgingSummaryColumns {
return this.agingPeriods; return this.agingPeriods;
} }
} }

View File

@@ -3,6 +3,7 @@ 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 ARAgingSummarySheet from './ARAgingSummarySheet'; import ARAgingSummarySheet from './ARAgingSummarySheet';
import SaleInvoiceRepository from 'repositories/SaleInvoiceRepository';
@Service() @Service()
export default class ARAgingSummaryService { export default class ARAgingSummaryService {
@@ -30,14 +31,14 @@ export default class ARAgingSummaryService {
} }
/** /**
* *
* @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,
saleInvoiceRepository saleInvoiceRepository,
} = this.tenancy.repositories(tenantId); } = this.tenancy.repositories(tenantId);
const filter = { const filter = {
@@ -57,15 +58,21 @@ export default class ARAgingSummaryService {
// Retrieve all customers from the storage. // Retrieve all customers from the storage.
const customers = await customerRepository.all(); const customers = await customerRepository.all();
// Retrieve all overdue sale invoices.
const overdueSaleInvoices = await saleInvoiceRepository.overdueInvoices(
filter.asDate
);
// Retrieve all due sale invoices. // Retrieve all due sale invoices.
const dueSaleInvoices = await saleInvoiceRepository.dueInvoices(); const currentInvoices = await saleInvoiceRepository.dueInvoices(
filter.asDate
);
// AR aging summary report instance. // AR aging summary report instance.
const ARAgingSummaryReport = new ARAgingSummarySheet( const ARAgingSummaryReport = new ARAgingSummarySheet(
tenantId, tenantId,
filter, filter,
customers, customers,
dueSaleInvoices, overdueSaleInvoices,
currentInvoices,
baseCurrency baseCurrency
); );
// AR aging summary report data and columns. // AR aging summary report data and columns.

View File

@@ -1,9 +1,8 @@
import { groupBy, sumBy, defaultTo } from 'lodash'; import { groupBy, sum } from 'lodash';
import { import {
ICustomer, ICustomer,
IARAgingSummaryQuery, IARAgingSummaryQuery,
IARAgingSummaryCustomer, IARAgingSummaryCustomer,
IAgingPeriodTotal,
IAgingPeriod, IAgingPeriod,
ISaleInvoice, ISaleInvoice,
IARAgingSummaryData, IARAgingSummaryData,
@@ -18,8 +17,9 @@ export default class ARAgingSummarySheet extends AgingSummaryReport {
readonly contacts: ICustomer[]; readonly contacts: ICustomer[];
readonly agingPeriods: IAgingPeriod[]; readonly agingPeriods: IAgingPeriod[];
readonly baseCurrency: string; readonly baseCurrency: string;
readonly dueInvoices: ISaleInvoice[];
readonly unpaidInvoicesByContactId: Dictionary<ISaleInvoice[]>; readonly overdueInvoicesByContactId: Dictionary<ISaleInvoice[]>;
readonly currentInvoicesByContactId: Dictionary<ISaleInvoice[]>;
/** /**
* Constructor method. * Constructor method.
@@ -32,7 +32,8 @@ export default class ARAgingSummarySheet extends AgingSummaryReport {
tenantId: number, tenantId: number,
query: IARAgingSummaryQuery, query: IARAgingSummaryQuery,
customers: ICustomer[], customers: ICustomer[],
unpaidSaleInvoices: ISaleInvoice[], overdueSaleInvoices: ISaleInvoice[],
currentSaleInvoices: ISaleInvoice[],
baseCurrency: string baseCurrency: string
) { ) {
super(); super();
@@ -42,9 +43,9 @@ export default class ARAgingSummarySheet extends AgingSummaryReport {
this.query = query; this.query = query;
this.baseCurrency = baseCurrency; this.baseCurrency = baseCurrency;
this.numberFormat = this.query.numberFormat; this.numberFormat = this.query.numberFormat;
this.unpaidInvoicesByContactId = groupBy(unpaidSaleInvoices, 'customerId');
this.dueInvoices = unpaidSaleInvoices; this.overdueInvoicesByContactId = groupBy(overdueSaleInvoices, 'customerId');
this.periodsByContactId = {}; this.currentInvoicesByContactId = groupBy(currentSaleInvoices, 'customerId');
// Initializes the aging periods. // Initializes the aging periods.
this.agingPeriods = this.agingRangePeriods( this.agingPeriods = this.agingRangePeriods(
@@ -61,10 +62,13 @@ export default class ARAgingSummarySheet extends AgingSummaryReport {
*/ */
private customerData(customer: ICustomer): IARAgingSummaryCustomer { private customerData(customer: ICustomer): IARAgingSummaryCustomer {
const agingPeriods = this.getContactAgingPeriods(customer.id); const agingPeriods = this.getContactAgingPeriods(customer.id);
const amount = sumBy(agingPeriods, 'total'); const currentTotal = this.getContactCurrentTotal(customer.id);
const agingPeriodsTotal = this.getAgingPeriodsTotal(agingPeriods);
const amount = sum([agingPeriodsTotal, currentTotal]);
return { return {
customerName: customer.displayName, customerName: customer.displayName,
current: this.formatTotalAmount(currentTotal),
aging: agingPeriods, aging: agingPeriods,
total: this.formatTotalAmount(amount), total: this.formatTotalAmount(amount),
}; };
@@ -91,10 +95,14 @@ export default class ARAgingSummarySheet extends AgingSummaryReport {
public reportData(): IARAgingSummaryData { public reportData(): IARAgingSummaryData {
const customersAgingPeriods = this.customersWalker(this.contacts); const customersAgingPeriods = this.customersWalker(this.contacts);
const totalAgingPeriods = this.getTotalAgingPeriods(customersAgingPeriods); const totalAgingPeriods = this.getTotalAgingPeriods(customersAgingPeriods);
const totalCurrent = this.getTotalCurrent(customersAgingPeriods);
return { return {
customers: customersAgingPeriods, customers: customersAgingPeriods,
total: totalAgingPeriods, total: {
current: this.formatTotalAmount(totalCurrent),
aging: totalAgingPeriods,
}
}; };
} }

View File

@@ -1,4 +1,4 @@
import { defaultTo, sumBy } from 'lodash'; import { defaultTo, sumBy, get } from 'lodash';
import { import {
IAgingPeriod, IAgingPeriod,
ISaleInvoice, ISaleInvoice,
@@ -6,6 +6,7 @@ import {
IAgingPeriodTotal, IAgingPeriodTotal,
IARAgingSummaryCustomer, IARAgingSummaryCustomer,
IContact, IContact,
IARAgingSummaryQuery,
} from 'interfaces'; } from 'interfaces';
import AgingReport from './AgingReport'; import AgingReport from './AgingReport';
import { Dictionary } from 'tsyringe/dist/typings/types'; import { Dictionary } from 'tsyringe/dist/typings/types';
@@ -14,13 +15,13 @@ export default abstract class AgingSummaryReport extends AgingReport {
protected readonly contacts: IContact[]; protected readonly contacts: IContact[];
protected readonly agingPeriods: IAgingPeriod[] = []; protected readonly agingPeriods: IAgingPeriod[] = [];
protected readonly baseCurrency: string; protected readonly baseCurrency: string;
protected readonly unpaidInvoices: (ISaleInvoice | IBill)[]; protected readonly query: IARAgingSummaryQuery;
protected readonly unpaidInvoicesByContactId: Dictionary< protected readonly overdueInvoicesByContactId: Dictionary<
(ISaleInvoice | IBill)[]
>;
protected readonly currentInvoicesByContactId: Dictionary<
(ISaleInvoice | IBill)[] (ISaleInvoice | IBill)[]
>; >;
protected periodsByContactId: {
[key: number]: (IAgingPeriod & IAgingPeriodTotal)[];
} = {};
/** /**
* Setes initial aging periods to the given customer id. * Setes initial aging periods to the given customer id.
@@ -48,7 +49,7 @@ export default abstract class AgingSummaryReport extends AgingReport {
const newAgingPeriods = this.getContactAgingDueAmount( const newAgingPeriods = this.getContactAgingDueAmount(
agingPeriods, agingPeriods,
unpaidInvoice.dueAmount, unpaidInvoice.dueAmount,
unpaidInvoice.overdueDays unpaidInvoice.getOverdueDays(this.query.asDate)
); );
return newAgingPeriods; return newAgingPeriods;
}, initialAgingPeriods); }, initialAgingPeriods);
@@ -81,7 +82,7 @@ export default abstract class AgingSummaryReport extends AgingReport {
} }
/** /**
* Retrieve the aging period total object. (xx) * Retrieve the aging period total object.
* @param {number} amount * @param {number} amount
* @return {IAgingPeriodTotal} * @return {IAgingPeriodTotal}
*/ */
@@ -112,14 +113,14 @@ export default abstract class AgingSummaryReport extends AgingReport {
} }
/** /**
* Retrieve the due invoices by the given customer id. (XX) * Retrieve the due invoices by the given customer id.
* @param {number} customerId - * @param {number} customerId -
* @return {ISaleInvoice[]} * @return {ISaleInvoice[]}
*/ */
protected getUnpaidInvoicesByContactId( protected getUnpaidInvoicesByContactId(
contactId: number contactId: number
): (ISaleInvoice | IBill)[] { ): (ISaleInvoice | IBill)[] {
return defaultTo(this.unpaidInvoicesByContactId[contactId], []); return defaultTo(this.overdueInvoicesByContactId[contactId], []);
} }
/** /**
@@ -138,4 +139,47 @@ export default abstract class AgingSummaryReport extends AgingReport {
}; };
}); });
} }
/**
* 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

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