fix: AR/AP aging summary report.

This commit is contained in:
a.bouhuolia
2021-01-14 13:16:32 +02:00
parent 343185b8bd
commit a747750d88
13 changed files with 287 additions and 154 deletions

View File

@@ -1,6 +1,7 @@
import {
IAgingPeriod,
IAgingPeriodTotal
IAgingPeriodTotal,
IAgingAmount
} from './AgingReport';
import {
INumberFormatQuery
@@ -17,14 +18,15 @@ export interface IAPAgingSummaryQuery {
export interface IAPAgingSummaryVendor {
vendorName: string,
current: IAgingPeriodTotal,
aging: (IAgingPeriod & IAgingPeriodTotal)[],
total: IAgingPeriodTotal,
current: IAgingAmount,
aging: IAgingPeriodTotal[],
total: IAgingAmount,
};
export interface IAPAgingSummaryTotal {
current: IAgingPeriodTotal,
aging: (IAgingPeriodTotal & IAgingPeriod)[],
current: IAgingAmount,
aging: IAgingPeriodTotal[],
total: IAgingAmount,
};
export interface IAPAgingSummaryData {

View File

@@ -1,10 +1,5 @@
import {
IAgingPeriod,
IAgingPeriodTotal
} from './AgingReport';
import {
INumberFormatQuery
} from './FinancialStatements';
import { IAgingPeriod, IAgingPeriodTotal, IAgingAmount } from './AgingReport';
import { INumberFormatQuery } from './FinancialStatements';
export interface IARAgingSummaryQuery {
asDate: Date | string;
@@ -17,20 +12,20 @@ export interface IARAgingSummaryQuery {
export interface IARAgingSummaryCustomer {
customerName: string;
current: IAgingPeriodTotal,
aging: (IAgingPeriodTotal & IAgingPeriod)[];
total: IAgingPeriodTotal;
current: IAgingAmount;
aging: IAgingPeriodTotal[];
total: IAgingAmount;
}
export interface IARAgingSummaryTotal {
current: IAgingPeriodTotal,
aging: (IAgingPeriodTotal & IAgingPeriod)[],
total: IAgingPeriodTotal,
};
current: IAgingAmount;
aging: IAgingPeriodTotal[];
total: IAgingAmount;
}
export interface IARAgingSummaryData {
customers: IARAgingSummaryCustomer[],
total: IARAgingSummaryTotal,
};
customers: IARAgingSummaryCustomer[];
total: IARAgingSummaryTotal;
}
export type IARAgingSummaryColumns = IAgingPeriod[];
export type IARAgingSummaryColumns = IAgingPeriod[];

View File

@@ -1,12 +1,22 @@
export interface IAgingPeriodTotal {
total: number;
formattedTotal: string;
export interface IAgingPeriodTotal extends IAgingPeriod {
total: IAgingAmount;
};
export interface IAgingAmount {
amount: number;
formattedAmount: string;
currencyCode: string;
}
export interface IAgingPeriod {
fromPeriod: Date|string;
toPeriod: Date|string;
fromPeriod: Date | string;
toPeriod: Date | string;
beforeDays: number;
toDays: number;
}
}
export interface IAgingSummaryContact {
current: IAgingAmount;
aging: IAgingPeriodTotal[];
total: IAgingAmount;
}

View File

@@ -7,7 +7,7 @@ export default class CustomerRepository extends TenantRepository {
*/
constructor(knex, cache) {
super(knex, cache);
this.repositoryName = 'ContactRepository';
this.repositoryName = 'CustomerRepository';
}
/**

View File

@@ -7,7 +7,7 @@ export default class VendorRepository extends TenantRepository {
*/
constructor(knex, cache) {
super(knex, cache);
this.repositoryName = 'ContactRepository';
this.repositoryName = 'VendorRepository';
}
/**
@@ -17,6 +17,10 @@ export default class VendorRepository extends TenantRepository {
return Vendor.bindKnex(this.knex);
}
unpaid() {
}
changeBalance(vendorId: number, amount: number) {
return super.changeNumber({ id: vendorId }, 'balance', amount);
}

View File

@@ -7,9 +7,11 @@ import {
IVendor,
IAPAgingSummaryData,
IAPAgingSummaryVendor,
IAPAgingSummaryColumns
IAPAgingSummaryColumns,
IAPAgingSummaryTotal
} from 'interfaces';
import { Dictionary } from 'tsyringe/dist/typings/types';
export default class APAgingSummarySheet extends AgingSummaryReport {
readonly tenantId: number;
readonly query: IAPAgingSummaryQuery;
@@ -56,6 +58,23 @@ export default class APAgingSummarySheet extends AgingSummaryReport {
);
}
/**
* Retrieve the vendors aging and current total.
* @param {IAPAgingSummaryTotal} vendorsAgingPeriods
* @return {IAPAgingSummaryTotal}
*/
getVendorsTotal(vendorsAgingPeriods): IAPAgingSummaryTotal {
const totalAgingPeriods = this.getTotalAgingPeriods(vendorsAgingPeriods);
const totalCurrent = this.getTotalCurrent(vendorsAgingPeriods);
const totalVendorsTotal = this.getTotalContactsTotals(vendorsAgingPeriods);
return {
current: this.formatTotalAmount(totalCurrent),
aging: totalAgingPeriods,
total: this.formatTotalAmount(totalVendorsTotal),
};
}
/**
* Retrieve the vendor section data.
* @param {IVendor} vendor
@@ -85,7 +104,7 @@ export default class APAgingSummarySheet extends AgingSummaryReport {
.map((vendor) => this.vendorData(vendor))
.filter(
(vendor: IAPAgingSummaryVendor) =>
!(vendor.total.total === 0 && this.query.noneZero)
!(vendor.total.amount === 0 && this.query.noneZero)
);
}
@@ -95,16 +114,12 @@ export default class APAgingSummarySheet extends AgingSummaryReport {
*/
public reportData(): IAPAgingSummaryData {
const vendorsAgingPeriods = this.vendorsWalker(this.contacts);
const totalAgingPeriods = this.getTotalAgingPeriods(vendorsAgingPeriods);
const totalCurrent = this.getTotalCurrent(vendorsAgingPeriods);
const vendorsTotal = this.getVendorsTotal(vendorsAgingPeriods);
return {
vendors: vendorsAgingPeriods,
total: {
current: this.formatTotalAmount(totalCurrent),
aging: totalAgingPeriods,
},
}
total: vendorsTotal,
};
}
/**
@@ -113,4 +128,4 @@ export default class APAgingSummarySheet extends AgingSummaryReport {
public reportColumns(): IAPAgingSummaryColumns {
return this.agingPeriods;
}
}
}

View File

@@ -7,6 +7,7 @@ import {
ISaleInvoice,
IARAgingSummaryData,
IARAgingSummaryColumns,
IARAgingSummaryTotal,
} from 'interfaces';
import AgingSummaryReport from './AgingSummary';
import { Dictionary } from 'tsyringe/dist/typings/types';
@@ -44,8 +45,14 @@ export default class ARAgingSummarySheet extends AgingSummaryReport {
this.baseCurrency = baseCurrency;
this.numberFormat = this.query.numberFormat;
this.overdueInvoicesByContactId = groupBy(overdueSaleInvoices, 'customerId');
this.currentInvoicesByContactId = groupBy(currentSaleInvoices, 'customerId');
this.overdueInvoicesByContactId = groupBy(
overdueSaleInvoices,
'customerId'
);
this.currentInvoicesByContactId = groupBy(
currentSaleInvoices,
'customerId'
);
// Initializes the aging periods.
this.agingPeriods = this.agingRangePeriods(
@@ -84,27 +91,41 @@ export default class ARAgingSummarySheet extends AgingSummaryReport {
.map((customer) => this.customerData(customer))
.filter(
(customer: IARAgingSummaryCustomer) =>
!(customer.total.total === 0 && this.query.noneZero)
!(customer.total.amount === 0 && this.query.noneZero)
);
}
/**
* Retrieve the customers aging and current total.
* @param {IARAgingSummaryCustomer} customersAgingPeriods
*/
private getCustomersTotal(
customersAgingPeriods: IARAgingSummaryCustomer[]
): IARAgingSummaryTotal {
const totalAgingPeriods = this.getTotalAgingPeriods(customersAgingPeriods);
const totalCurrent = this.getTotalCurrent(customersAgingPeriods);
const totalCustomersTotal = this.getTotalContactsTotals(
customersAgingPeriods
);
return {
current: this.formatTotalAmount(totalCurrent),
aging: totalAgingPeriods,
total: this.formatTotalAmount(totalCustomersTotal),
};
}
/**
* Retrieve A/R aging summary report data.
* @return {IARAgingSummaryData}
*/
public reportData(): IARAgingSummaryData {
const customersAgingPeriods = this.customersWalker(this.contacts);
const totalAgingPeriods = this.getTotalAgingPeriods(customersAgingPeriods);
const totalCurrent = this.getTotalCurrent(customersAgingPeriods);
const totalCustomersTotal = this.getTotalContactsTotals(customersAgingPeriods);
const customersTotal = this.getCustomersTotal(customersAgingPeriods);
return {
customers: customersAgingPeriods,
total: {
current: this.formatTotalAmount(totalCurrent),
aging: totalAgingPeriods,
total: this.formatTotalAmount(totalCustomersTotal),
}
total: customersTotal,
};
}

View File

@@ -8,6 +8,8 @@ import {
IContact,
IARAgingSummaryQuery,
IFormatNumberSettings,
IAgingAmount,
IAgingSummaryContact
} from 'interfaces';
import AgingReport from './AgingReport';
import { Dictionary } from 'tsyringe/dist/typings/types';
@@ -25,60 +27,61 @@ export default abstract class AgingSummaryReport extends AgingReport {
>;
/**
* Setes initial aging periods to the given customer id.
* @param {number} customerId - Customer id.
* Setes initial aging periods to the contact.
*/
protected getInitialAgingPeriodsTotal() {
protected getInitialAgingPeriodsTotal(): IAgingPeriodTotal[] {
return this.agingPeriods.map((agingPeriod) => ({
...agingPeriod,
...this.formatAmount(0),
total: this.formatAmount(0),
}));
}
/**
* Calculates the given contact aging periods.
* @param {ICustomer} customer
* @return {(IAgingPeriod & IAgingPeriodTotal)[]}
* @param {number} contactId - Contact id.
* @return {IAgingPeriodTotal[]}
*/
protected getContactAgingPeriods(
contactId: number
): (IAgingPeriod & IAgingPeriodTotal)[] {
protected getContactAgingPeriods(contactId: number): IAgingPeriodTotal[] {
const unpaidInvoices = this.getUnpaidInvoicesByContactId(contactId);
const initialAgingPeriods = this.getInitialAgingPeriodsTotal();
return unpaidInvoices.reduce((agingPeriods, unpaidInvoice) => {
const newAgingPeriods = this.getContactAgingDueAmount(
agingPeriods,
unpaidInvoice.dueAmount,
unpaidInvoice.getOverdueDays(this.query.asDate)
);
return newAgingPeriods;
}, initialAgingPeriods);
return unpaidInvoices.reduce(
(agingPeriods: IAgingPeriodTotal[], unpaidInvoice) => {
const newAgingPeriods = this.getContactAgingDueAmount(
agingPeriods,
unpaidInvoice.dueAmount,
unpaidInvoice.getOverdueDays(this.query.asDate)
);
return newAgingPeriods;
},
initialAgingPeriods
);
}
/**
* Sets the customer aging due amount to the table. (Xx)
* @param {number} customerId - Customer id.
* Sets the contact aging due amount to the table.
* @param {IAgingPeriodTotal} agingPeriods - Aging periods.
* @param {number} dueAmount - Due amount.
* @param {number} overdueDays - Overdue days.
* @return {IAgingPeriodTotal[]}
*/
protected getContactAgingDueAmount(
agingPeriods: any,
agingPeriods: IAgingPeriodTotal[],
dueAmount: number,
overdueDays: number
): (IAgingPeriod & IAgingPeriodTotal)[] {
): IAgingPeriodTotal[] {
const newAgingPeriods = agingPeriods.map((agingPeriod) => {
const isInAgingPeriod =
agingPeriod.beforeDays <= overdueDays &&
(agingPeriod.toDays > overdueDays || !agingPeriod.toDays);
const total = isInAgingPeriod
? agingPeriod.total + dueAmount
: agingPeriod.total;
const total: number = isInAgingPeriod
? agingPeriod.total.amount + dueAmount
: agingPeriod.total.amount;
return {
...agingPeriod,
total,
total: this.formatAmount(total),
};
});
return newAgingPeriods;
@@ -87,27 +90,34 @@ export default abstract class AgingSummaryReport extends AgingReport {
/**
* Retrieve the aging period total object.
* @param {number} amount
* @return {IAgingPeriodTotal}
* @param {IFormatNumberSettings} settings - Override the format number settings.
* @return {IAgingAmount}
*/
protected formatAmount(
amount: number,
settings: IFormatNumberSettings = {}
): IAgingPeriodTotal {
): IAgingAmount {
return {
total: amount,
formattedTotal: this.formatNumber(amount, settings),
amount,
formattedAmount: this.formatNumber(amount, settings),
currencyCode: this.baseCurrency,
};
}
/**
* Retrieve the aging period total object.
* @param {number} amount
* @param {IFormatNumberSettings} settings - Override the format number settings.
* @return {IAgingPeriodTotal}
*/
protected formatTotalAmount(
amount: number,
settings: IFormatNumberSettings = {}
): IAgingPeriodTotal {
): IAgingAmount {
return this.formatAmount(amount, {
money: true,
excerptZero: false,
...settings
...settings,
});
}
@@ -118,9 +128,9 @@ export default abstract class AgingSummaryReport extends AgingReport {
*/
protected getTotalAgingPeriodByIndex(
contactsAgingPeriods: any,
index: number
index: number,
): number {
return this.contacts.reduce((acc, customer) => {
return this.contacts.reduce((acc, contact) => {
const totalPeriod = contactsAgingPeriods[index]
? contactsAgingPeriods[index].total
: 0;
@@ -130,9 +140,9 @@ export default abstract class AgingSummaryReport extends AgingReport {
}
/**
* Retrieve the due invoices by the given customer id.
* @param {number} customerId -
* @return {ISaleInvoice[]}
* Retrieve the due invoices by the given contact id.
* @param {number} contactId -
* @return {(ISaleInvoice | IBill)[]}
*/
protected getUnpaidInvoicesByContactId(
contactId: number
@@ -146,13 +156,23 @@ export default abstract class AgingSummaryReport extends AgingReport {
*/
protected getTotalAgingPeriods(
contactsAgingPeriods: IARAgingSummaryCustomer[]
): (IAgingPeriodTotal & IAgingPeriod)[] {
): IAgingPeriodTotal[] {
return this.agingPeriods.map((agingPeriod, index) => {
const total = sumBy(contactsAgingPeriods, `aging[${index}].total`);
const total = sumBy(
contactsAgingPeriods,
(summary: IARAgingSummaryCustomer) => {
const aging = summary.aging[index];
if (!aging) {
return 0;
}
return aging.total.amount;
}
);
return {
...agingPeriod,
...this.formatTotalAmount(total),
total: this.formatTotalAmount(total),
};
});
}
@@ -179,14 +199,14 @@ export default abstract class AgingSummaryReport extends AgingReport {
}
/**
* Retrieve to total sumation of the given customers sections.
* Retrieve to total sumation of the given contacts summeries sections.
* @param {IARAgingSummaryCustomer[]} contactsSections -
* @return {number}
*/
protected getTotalCurrent(
customersSummary: IARAgingSummaryCustomer[]
contactsSummaries: IAgingSummaryContact[]
): number {
return sumBy(customersSummary, (summary) => summary.current.total);
return sumBy(contactsSummaries, (summary) => summary.current.amount);
}
/**
@@ -195,13 +215,16 @@ export default abstract class AgingSummaryReport extends AgingReport {
* @return {number}
*/
protected getAgingPeriodsTotal(agingPeriods: IAgingPeriodTotal[]): number {
return sumBy(agingPeriods, 'total');
return sumBy(agingPeriods, (period) => period.total.amount);
}
/**
* Retrieve total of contacts totals.
* @param {IAgingSummaryContact[]} contactsSummaries
*/
protected getTotalContactsTotals(
customersSummary: IARAgingSummaryCustomer[]
contactsSummaries: IAgingSummaryContact[]
): number {
return sumBy(customersSummary, (summary) => summary.total.total);
return sumBy(contactsSummaries, (summary) => summary.total.amount);
}
}

View File

@@ -1,16 +1,56 @@
import {
formatNumber
} from 'utils';
import { IFormatNumberSettings, INumberFormatQuery } from 'interfaces';
import { formatNumber } from 'utils';
export default class FinancialSheet {
numberFormat: { noCents: boolean, divideOn1000: boolean };
numberFormat: INumberFormatQuery;
/**
* Transformes the number format query to settings
*/
protected transfromFormatQueryToSettings(): IFormatNumberSettings {
const { numberFormat } = this;
return {
precision: numberFormat.precision,
divideOn1000: numberFormat.divideOn1000,
excerptZero: !numberFormat.showZero,
negativeFormat: numberFormat.negativeFormat,
money: numberFormat.formatMoney === 'always',
};
}
/**
* Formating amount based on the given report query.
* @param {number} number
* @param {number} number -
* @param {IFormatNumberSettings} overrideSettings -
* @return {string}
*/
protected formatNumber(number): string {
return formatNumber(number, this.numberFormat);
protected formatNumber(
number,
overrideSettings: IFormatNumberSettings = {}
): string {
const settings = {
...this.transfromFormatQueryToSettings(),
...overrideSettings,
};
return formatNumber(number, settings);
}
}
/**
* Formatting full amount with different format settings.
* @param {number} amount -
* @param {IFormatNumberSettings} settings -
*/
protected formatTotalNumber(
amount: number,
settings: IFormatNumberSettings = {}
): string {
const { numberFormat } = this;
return this.formatNumber(amount, {
money: numberFormat.formatMoney === 'none' ? false : true,
excerptZero: false,
...settings
});
}
}

View File

@@ -7,9 +7,9 @@ import {
IAccount,
IJournalPoster,
IAccountType,
IJournalEntry
IJournalEntry,
} from 'interfaces';
import FinancialSheet from "../FinancialSheet";
import FinancialSheet from '../FinancialSheet';
export default class GeneralLedgerSheet extends FinancialSheet {
tenantId: number;
@@ -35,7 +35,7 @@ export default class GeneralLedgerSheet extends FinancialSheet {
transactions: IJournalPoster,
openingBalancesJournal: IJournalPoster,
closingBalancesJournal: IJournalPoster,
baseCurrency: string,
baseCurrency: string
) {
super();
@@ -51,7 +51,7 @@ export default class GeneralLedgerSheet extends FinancialSheet {
/**
* Mapping the account transactions to general ledger transactions of the given account.
* @param {IAccount} account
* @param {IAccount} account
* @return {IGeneralLedgerSheetAccountTransaction[]}
*/
private accountTransactionsMapper(
@@ -59,34 +59,45 @@ export default class GeneralLedgerSheet extends FinancialSheet {
): IGeneralLedgerSheetAccountTransaction[] {
const entries = this.transactions.getAccountEntries(account.id);
return entries.map((transaction: IJournalEntry): IGeneralLedgerSheetAccountTransaction => {
let amount = 0;
return entries.map(
(transaction: IJournalEntry): IGeneralLedgerSheetAccountTransaction => {
let amount = 0;
if (account.type.normal === 'credit') {
amount += transaction.credit - transaction.debit;
} else if (account.type.normal === 'debit') {
amount += transaction.debit - transaction.credit;
if (account.type.normal === 'credit') {
amount += transaction.credit - transaction.debit;
} else if (account.type.normal === 'debit') {
amount += transaction.debit - transaction.credit;
}
const formattedAmount = this.formatNumber(amount);
return {
...pick(transaction, [
'id',
'note',
'transactionType',
'referenceType',
'referenceId',
'referenceTypeFormatted',
'date',
]),
amount,
formattedAmount,
currencyCode: this.baseCurrency,
};
}
const formattedAmount = this.formatNumber(amount);
return {
...pick(transaction, ['id', 'note', 'transactionType', 'referenceType',
'referenceId', 'date']),
amount,
formattedAmount,
currencyCode: this.baseCurrency,
};
});
);
}
/**
* Retrieve account opening balance.
* @param {IAccount} account
* @param {IAccount} account
* @return {IGeneralLedgerSheetAccountBalance}
*/
private accountOpeningBalance(account: IAccount): IGeneralLedgerSheetAccountBalance {
private accountOpeningBalance(
account: IAccount
): IGeneralLedgerSheetAccountBalance {
const amount = this.openingBalancesJournal.getAccountBalance(account.id);
const formattedAmount = this.formatNumber(amount);
const formattedAmount = this.formatTotalNumber(amount);
const currencyCode = this.baseCurrency;
const date = this.query.fromDate;
@@ -95,12 +106,14 @@ export default class GeneralLedgerSheet extends FinancialSheet {
/**
* Retrieve account closing balance.
* @param {IAccount} account
* @param {IAccount} account
* @return {IGeneralLedgerSheetAccountBalance}
*/
private accountClosingBalance(account: IAccount): IGeneralLedgerSheetAccountBalance {
private accountClosingBalance(
account: IAccount
): IGeneralLedgerSheetAccountBalance {
const amount = this.closingBalancesJournal.getAccountBalance(account.id);
const formattedAmount = this.formatNumber(amount);
const formattedAmount = this.formatTotalNumber(amount);
const currencyCode = this.baseCurrency;
const date = this.query.toDate;
@@ -109,35 +122,42 @@ export default class GeneralLedgerSheet extends FinancialSheet {
/**
* Retreive general ledger accounts sections.
* @param {IAccount} account
* @param {IAccount} account
* @return {IGeneralLedgerSheetAccount}
*/
private accountMapper(
account: IAccount & { type: IAccountType },
account: IAccount & { type: IAccountType }
): IGeneralLedgerSheetAccount {
return {
...pick(account, ['id', 'name', 'code', 'index', 'parentAccountId']),
opening: this.accountOpeningBalance(account),
transactions: this.accountTransactionsMapper(account),
closing: this.accountClosingBalance(account),
}
};
}
/**
* Retrieve mapped accounts with general ledger transactions and opeing/closing balance.
* @param {IAccount[]} accounts -
* @param {IAccount[]} accounts -
* @return {IGeneralLedgerSheetAccount[]}
*/
private accountsWalker(
accounts: IAccount & { type: IAccountType }[]
): IGeneralLedgerSheetAccount[] {
return accounts
.map((account: IAccount & { type: IAccountType }) => this.accountMapper(account))
// Filter general ledger accounts that have no transactions when `noneTransactions` is on.
.filter((generalLedgerAccount: IGeneralLedgerSheetAccount) => (
!(generalLedgerAccount.transactions.length === 0 && this.query.noneTransactions)
));
return (
accounts
.map((account: IAccount & { type: IAccountType }) =>
this.accountMapper(account)
)
// Filter general ledger accounts that have no transactions when `noneTransactions` is on.
.filter(
(generalLedgerAccount: IGeneralLedgerSheetAccount) =>
!(
generalLedgerAccount.transactions.length === 0 &&
this.query.noneTransactions
)
)
);
}
/**
@@ -147,4 +167,4 @@ export default class GeneralLedgerSheet extends FinancialSheet {
public reportData(): IGeneralLedgerSheetAccount[] {
return this.accountsWalker(this.accounts);
}
}
}

View File

@@ -85,8 +85,7 @@ export default class GeneralLedgerService {
group: 'organization',
key: 'base_currency',
});
// Retrieve all accounts from the storage.
// Retrieve all accounts with associated type from the storage.
const accounts = await accountRepository.all('type');
const accountsGraph = await accountRepository.getDependencyGraph();
@@ -111,11 +110,13 @@ export default class GeneralLedgerService {
tenantId,
accountsGraph
);
// Accounts opening transactions.
const openingTransJournal = Journal.fromTransactions(
openingBalanceTrans,
tenantId,
accountsGraph
);
// Accounts closing transactions.
const closingTransJournal = Journal.fromTransactions(
closingBalanceTrans,
tenantId,

View File

@@ -57,7 +57,6 @@ export default class JournalSheetService {
group: 'organization',
key: 'base_currency',
});
// Retrieve all accounts on the storage.
const accountsGraph = await accountRepository.getDependencyGraph();