WIP: customer balance report.

This commit is contained in:
a.bouhuolia
2021-05-05 02:19:43 +02:00
parent 8275d3d395
commit 8ca3509f03
14 changed files with 843 additions and 1 deletions

View File

@@ -689,6 +689,7 @@ export default class JournalPoster implements IJournalPoster {
});
return balance;
}
getAccountEntries(accountId: number) {
return this.entries.filter((entry) => entry.account === accountId);

View File

@@ -0,0 +1,193 @@
import { sumBy } from 'lodash';
import * as R from 'ramda';
import FinancialSheet from '../FinancialSheet';
import {
IJournalPoster,
ICustomer,
ICustomerBalanceSummaryCustomer,
ICustomerBalanceSummaryQuery,
ICustomerBalanceSummaryData,
ICustomerBalanceSummaryTotal,
} from 'interfaces';
export class CustomerBalanceSummaryReport extends FinancialSheet {
receivableLedger: IJournalPoster;
baseCurrency: string;
customers: ICustomer[];
filter: ICustomerBalanceSummaryQuery;
/**
* Constructor method.
* @param {IJournalPoster} receivableLedger
* @param {ICustomer[]} customers
* @param {ICustomerBalanceSummaryQuery} filter
* @param {string} baseCurrency
*/
constructor(
receivableLedger: IJournalPoster,
customers: ICustomer[],
filter: ICustomerBalanceSummaryQuery,
baseCurrency: string
) {
super();
this.receivableLedger = receivableLedger;
this.baseCurrency = baseCurrency;
this.customers = customers;
this.filter = filter;
}
getAmountMeta(amount: number) {
return {
amount,
formattedAmount: amount,
currencyCode: this.baseCurrency,
};
}
getPercentageMeta(amount: number) {
return {
amount,
formattedAmount: this.formatPercentage(amount),
};
}
/**
* Customer section mapper.
* @param {ICustomer} customer
* @returns {ICustomerBalanceSummaryCustomer}
*/
private customerMapper(customer: ICustomer): ICustomerBalanceSummaryCustomer {
const balance = this.receivableLedger.getContactBalance(null, customer.id);
return {
customerName: customer.displayName,
total: this.getAmountMeta(balance),
};
}
/**
* Retrieve the customer summary section with percentage of column.
* @param {number} total
* @param {ICustomerBalanceSummaryCustomer} customer
* @returns {ICustomerBalanceSummaryCustomer}
*/
private customerCamparsionPercentageOfColumnMapper(
total: number,
customer: ICustomerBalanceSummaryCustomer
): ICustomerBalanceSummaryCustomer {
const amount = this.getCustomerPercentageOfColumn(
total,
customer.total.amount
);
return {
...customer,
percentageOfColumn: this.getPercentageMeta(amount),
};
}
/**
* Mappes the customers summary sections with percentage of column.
* @param {ICustomerBalanceSummaryCustomer[]} customers -
* @return {ICustomerBalanceSummaryCustomer[]}
*/
private customerCamparsionPercentageOfColumn(
customers: ICustomerBalanceSummaryCustomer[]
): ICustomerBalanceSummaryCustomer[] {
const customersTotal = this.getCustomersTotal(customers);
const camparsionPercentageOfColummn = R.curry(
this.customerCamparsionPercentageOfColumnMapper.bind(this)
)(customersTotal);
return customers.map(camparsionPercentageOfColummn);
}
/**
* Mappes the customer model object to customer balance summary section.
* @param {ICustomer[]} customers - Customers.
* @returns {ICustomerBalanceSummaryCustomer[]}
*/
private customersMapper(
customers: ICustomer[]
): ICustomerBalanceSummaryCustomer[] {
return customers.map(this.customerMapper.bind(this));
}
/**
* Retrieve the customers sections of the report.
* @param {ICustomer} customers
* @returns {ICustomerBalanceSummaryCustomer[]}
*/
private getCustomersSection(
customers: ICustomer[]
): ICustomerBalanceSummaryCustomer[] {
return R.compose(
R.when(
R.always(this.filter.comparison.percentageOfColumn),
this.customerCamparsionPercentageOfColumn.bind(this)
),
this.customersMapper.bind(this)
).bind(this)(customers);
}
/**
* Retrieve the customers total.
* @param {ICustomerBalanceSummaryCustomer} customers
* @returns {number}
*/
private getCustomersTotal(
customers: ICustomerBalanceSummaryCustomer[]
): number {
return sumBy(
customers,
(customer: ICustomerBalanceSummaryCustomer) => customer.total.amount
);
}
/**
* Calculates the customer percentage of column.
* @param {number} customerBalance - Customer balance.
* @param {number} totalBalance - Total customers balance.
* @returns {number}
*/
private getCustomerPercentageOfColumn(
customerBalance: number,
totalBalance: number
) {
return customerBalance > 0 ? totalBalance / customerBalance : 0;
}
/**
* Retrieve the customers total section.
* @param {ICustomer[]} customers
* @returns {ICustomerBalanceSummaryTotal}
*/
private customersTotalSection(
customers: ICustomerBalanceSummaryCustomer[]
): ICustomerBalanceSummaryTotal {
const customersTotal = this.getCustomersTotal(customers);
return {
total: this.getAmountMeta(customersTotal),
percentageOfColumn: this.getPercentageMeta(1),
};
}
/**
* Retrieve the report statement data.
* @returns {ICustomerBalanceSummaryData}
*/
public reportData(): ICustomerBalanceSummaryData {
const customersSections = this.getCustomersSection(this.customers);
const customersTotal = this.customersTotalSection(customersSections);
return {
customers: customersSections,
total: customersTotal,
};
}
reportColumns() {
return [];
}
}

View File

@@ -0,0 +1,110 @@
import { Inject } from 'typedi';
import moment from 'moment';
import TenancyService from 'services/Tenancy/TenancyService';
import Journal from 'services/Accounting/JournalPoster';
import {
ICustomerBalanceSummaryService,
ICustomerBalanceSummaryQuery,
ICustomerBalanceSummaryStatement,
} from 'interfaces';
import { CustomerBalanceSummaryReport } from './CustomerBalanceSummary';
export default class CustomerBalanceSummaryService
implements ICustomerBalanceSummaryService {
@Inject()
tenancy: TenancyService;
@Inject('logger')
logger: any;
/**
* Defaults balance sheet filter query.
* @return {ICustomerBalanceSummaryQuery}
*/
get defaultQuery(): ICustomerBalanceSummaryQuery {
return {
asDate: moment().format('YYYY-MM-DD'),
numberFormat: {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'total',
negativeFormat: 'mines',
},
comparison: {
percentageOfColumn: true,
},
noneZero: false,
noneTransactions: false,
};
}
/**
* Retrieve the statment of customer balance summary report.
* @param {number} tenantId
* @param {ICustomerBalanceSummaryQuery} query
* @return {Promise<ICustomerBalanceSummaryStatement>}
*/
async customerBalanceSummary(
tenantId: number,
query: ICustomerBalanceSummaryQuery
): Promise<ICustomerBalanceSummaryStatement> {
const {
accountRepository,
transactionsRepository,
} = this.tenancy.repositories(tenantId);
const { Customer } = this.tenancy.models(tenantId);
// Settings tenant service.
const settings = this.tenancy.settings(tenantId);
const baseCurrency = settings.get({
group: 'organization', key: 'base_currency',
});
const filter = {
...this.defaultQuery,
...query,
};
this.logger.info('[customer_balance_summary] trying to calculate the report.', {
filter,
tenantId,
});
// Retrieve all accounts on the storage.
const accounts = await accountRepository.all();
const accountsGraph = await accountRepository.getDependencyGraph();
// Retrieve all journal transactions based on the given query.
const transactions = await transactionsRepository.journal({
toDate: query.asDate,
});
// Transform transactions to journal collection.
const transactionsJournal = Journal.fromTransactions(
transactions,
tenantId,
accountsGraph
);
// Retrieve the customers list ordered by the display name.
const customers = await Customer.query().orderBy('displayName');
// Report instance.
const reportInstance = new CustomerBalanceSummaryReport(
transactionsJournal,
customers,
filter,
baseCurrency,
);
// Retrieve the report statement.
const reportData = reportInstance.reportData();
// Retrieve the report columns.
const reportColumns = reportInstance.reportColumns();
return {
data: reportData,
columns: reportColumns,
};
}
}

View File

@@ -0,0 +1,108 @@
import { get } from 'lodash';
import {
ICustomerBalanceSummaryData,
ICustomerBalanceSummaryCustomer,
ICustomerBalanceSummaryTotal,
} from 'interfaces';
import { Service } from 'typedi';
interface IColumnMapperMeta {
key: string;
accessor?: string;
value?: string;
}
interface ITableCell {
value: string;
key: string;
}
type ITableRow = {
rows: ITableCell[];
};
enum TABLE_ROWS_TYPES {
CUSTOMER = 'CUSTOMER',
TOTAL = 'TOTAL',
}
function tableMapper(
data: Object[],
columns: IColumnMapperMeta[],
rowsMeta
): ITableRow[] {
return data.map((object) => tableRowMapper(object, columns, rowsMeta));
}
function tableRowMapper(
object: Object,
columns: IColumnMapperMeta[],
rowMeta
): ITableRow {
const cells = columns.map((column) => ({
key: column.key,
value: column.value ? column.value : get(object, column.accessor),
}));
return {
cells,
...rowMeta,
};
}
@Service()
export default class CustomerBalanceSummaryTableRows {
/**
* Transformes the customers to table rows.
* @param {ICustomerBalanceSummaryCustomer[]} customers
* @returns {ITableRow[]}
*/
private customersTransformer(
customers: ICustomerBalanceSummaryCustomer[]
): ITableRow[] {
const columns = [
{ key: 'customerName', accessor: 'customerName' },
{ key: 'total', accessor: 'total.formattedAmount' },
{
key: 'percentageOfColumn',
accessor: 'percentageOfColumn.formattedAmount',
},
];
return tableMapper(customers, columns, {
rowTypes: [TABLE_ROWS_TYPES.CUSTOMER],
});
}
/**
* Transformes the total to table row.
* @param {ICustomerBalanceSummaryTotal} total
* @returns {ITableRow}
*/
private totalTransformer(total: ICustomerBalanceSummaryTotal) {
const columns = [
{ key: 'total', value: 'Total' },
{ key: 'total', accessor: 'total.formattedAmount' },
{
key: 'percentageOfColumn',
accessor: 'percentageOfColumn.formattedAmount',
},
];
return tableRowMapper(total, columns, {
rowTypes: [TABLE_ROWS_TYPES.TOTAL],
});
}
/**
* Transformes the customer balance summary to table rows.
* @param {ICustomerBalanceSummaryData} customerBalanceSummary
* @returns {ITableRow[]}
*/
public tableRowsTransformer(
customerBalanceSummary: ICustomerBalanceSummaryData
): ITableRow[] {
return [
...this.customersTransformer(customerBalanceSummary.customers),
this.totalTransformer(customerBalanceSummary.total),
];
}
}

View File

@@ -55,4 +55,11 @@ export default class FinancialSheet {
...settings
});
}
protected formatPercentage(
amount
): string {
return `%${amount * 100}`;
}
}

View File

@@ -0,0 +1,81 @@
import * as R from 'ramda';
import FinancialSheet from '../FinancialSheet';
import { IAccountTransaction, ICustomer } from 'interfaces';
import { transaction } from 'objection';
export default class TransactionsByCustomers extends FinancialSheet {
customers: ICustomer[];
transactionsByContact: any;
baseCurrency: string;
/**
* Constructor method.
* @param {ICustomer} customers
* @param transactionsByContact
* @param {string} baseCurrency
*/
constructor(
customers: ICustomer[],
transactionsByContact: Map<number, IAccountTransaction[]>,
baseCurrency: string
) {
super();
this.customers = customers;
this.transactionsByContact = transactionsByContact;
this.baseCurrency = baseCurrency;
}
/**
*
*/
private customerTransactionMapper(
transaction
): ITransactionsByCustomersTransaction {
return {
credit: transaction.credit,
debit: transaction.debit,
transactionNumber: transaction.transactionNumber,
referenceNumber: transaction.referenceNumber,
date: transaction.date,
createdAt: transaction.createdAt,
};
}
private customerTransactionRunningBalance(
openingBalance: number,
transaction: ITransactionsByCustomersTransaction
): ITransactionsByCustomersTransaction {
}
private customerTransactions(customerId: number) {
const transactions = this.transactionsByContact.get(customerId + '') || [];
return R.compose(
R.map(this.customerTransactionMapper),
R.map(R.curry(this.customerTransactionRunningBalance(0)))
).bind(this)(transactions);
}
private customerMapper(customer: ICustomer) {
return {
customerName: customer.displayName,
openingBalance: {},
closingBalance: {},
transactions: this.customerTransactions(customer.id),
};
}
private customersMapper(customers: ICustomer[]) {
return customers.map(this.customerMapper.bind(this));
}
public reportData() {
return this.customersMapper(this.customers);
}
public reportColumns() {
return [];
}
}

View File

@@ -0,0 +1,84 @@
import { Inject } from 'typedi';
import moment from 'moment';
import { groupBy } from 'lodash';
import TenancyService from 'services/Tenancy/TenancyService';
import {
ITransactionsByCustomersService,
ITransactionsByCustomersFilter,
ITransactionsByCustomersStatement,
} from 'interfaces';
import TransactionsByCustomers from './TransactionsByCustomers';
export default class TransactionsByCustomersService implements ITransactionsByCustomersService {
@Inject()
tenancy: TenancyService;
@Inject('logger')
logger: any;
/**
* Defaults balance sheet filter query.
* @return {ICustomerBalanceSummaryQuery}
*/
get defaultQuery(): ITransactionsByCustomersFilter {
return {
fromDate: moment().format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'),
numberFormat: {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'total',
negativeFormat: 'mines',
},
comparison: {
percentageOfColumn: true,
},
noneZero: false,
noneTransactions: false,
};
}
/**
* Retrieve transactions by by the customers.
* @param {number} tenantId
* @param {ITransactionsByCustomersFilter} query
* @return {Promise<ITransactionsByCustomersStatement>}
*/
public async transactionsByCustomers(
tenantId: number,
query: ITransactionsByCustomersFilter
): Promise<ITransactionsByCustomersStatement> {
const { transactionsRepository } = this.tenancy.repositories(tenantId);
const { Customer } = this.tenancy.models(tenantId);
// Settings tenant service.
const settings = this.tenancy.settings(tenantId);
const baseCurrency = settings.get({
group: 'organization',
key: 'base_currency',
});
const customers = await Customer.query().orderBy('displayName');
// Retrieve all journal transactions based on the given query.
const transactions = await transactionsRepository.journal({
fromDate: query.fromDate,
toDate: query.toDate,
});
// Transactions by customers data mapper.
const reportInstance = new TransactionsByCustomers(
customers,
new Map(Object.entries(groupBy(transactions, 'contactId'))),
baseCurrency
);
const reportData = reportInstance.reportData();
const reportColumns = reportInstance.reportColumns();
return {
data: reportData,
columns: reportColumns,
};
}
}