mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 14:20:31 +00:00
feat: AR/AP aging summary report.
This commit is contained in:
@@ -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(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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[];
|
||||||
@@ -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[];
|
||||||
@@ -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() {
|
||||||
@@ -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: {
|
||||||
@@ -204,21 +223,17 @@ export default class Bill extends TenantModel {
|
|||||||
* @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) {
|
if (vendorId) {
|
||||||
builder.where('vendor_id', 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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
@@ -37,7 +38,7 @@ export default class ARAgingSummaryService {
|
|||||||
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.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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