WIP: customer/vendor balance summary.

This commit is contained in:
a.bouhuolia
2021-05-05 21:41:10 +02:00
parent 8ca3509f03
commit 97d12d4294
18 changed files with 674 additions and 176 deletions

View File

@@ -0,0 +1,125 @@
import { sumBy } from 'lodash';
import * as R from 'ramda';
import FinancialSheet from '../FinancialSheet';
import {
IContactBalanceSummaryContact,
IContactBalanceSummaryTotal,
IContactBalanceSummaryAmount,
IContactBalanceSummaryPercentage
} from 'interfaces';
export class ContactBalanceSummaryReport extends FinancialSheet {
readonly baseCurrency: string;
/**
* Calculates the contact percentage of column.
* @param {number} customerBalance - Contact balance.
* @param {number} totalBalance - Total contacts balance.
* @returns {number}
*/
protected getContactPercentageOfColumn(
customerBalance: number,
totalBalance: number
): number {
return customerBalance > 0 ? totalBalance / customerBalance : 0;
}
/**
* Retrieve the contacts total.
* @param {IContactBalanceSummaryContact} contacts
* @returns {number}
*/
protected getContactsTotal(
contacts: IContactBalanceSummaryContact[]
): number {
return sumBy(
contacts,
(contact: IContactBalanceSummaryContact) => contact.total.amount
);
}
/**
* Retrieve the contacts total section.
* @param {IContactBalanceSummaryContact[]} contacts
* @returns {IContactBalanceSummaryTotal}
*/
protected getContactsTotalSection(
contacts: IContactBalanceSummaryContact[]
): IContactBalanceSummaryTotal {
const customersTotal = this.getContactsTotal(contacts);
return {
total: this.getTotalFormat(customersTotal),
percentageOfColumn: this.getPercentageMeta(1),
};
}
/**
* Retrieve the contact summary section with percentage of column.
* @param {number} total
* @param {IContactBalanceSummaryContact} contact
* @returns {IContactBalanceSummaryContact}
*/
private contactCamparsionPercentageOfColumnMapper(
total: number,
contact: IContactBalanceSummaryContact
): IContactBalanceSummaryContact {
const amount = this.getContactPercentageOfColumn(
total,
contact.total.amount
);
return {
...contact,
percentageOfColumn: this.getPercentageMeta(amount),
};
}
/**
* Mappes the contacts summary sections with percentage of column.
* @param {IContactBalanceSummaryContact[]} contacts -
* @return {IContactBalanceSummaryContact[]}
*/
protected contactCamparsionPercentageOfColumn(
contacts: IContactBalanceSummaryContact[]
): IContactBalanceSummaryContact[] {
const customersTotal = this.getContactsTotal(contacts);
const camparsionPercentageOfColummn = R.curry(
this.contactCamparsionPercentageOfColumnMapper.bind(this)
)(customersTotal);
return contacts.map(camparsionPercentageOfColummn);
}
getContactTotalFormat(amount: number) {
return {
amount,
formattedAmount: this.formatNumber(amount),
currencyCode: this.baseCurrency,
};
}
/**
* Retrieve the total amount of contacts sections.
* @param {number} amount
* @returns {IContactBalanceSummaryAmount}
*/
getTotalFormat(amount: number): IContactBalanceSummaryAmount {
return {
amount,
formattedAmount: this.formatNumber(amount),
currencyCode: this.baseCurrency,
};
}
/**
* Retrieve the percentage amount object.
* @param {number} amount
* @returns {IContactBalanceSummaryPercentage}
*/
getPercentageMeta(amount: number): IContactBalanceSummaryPercentage {
return {
amount,
formattedAmount: this.formatPercentage(amount),
};
}
}

View File

@@ -1,20 +1,21 @@
import { sumBy } from 'lodash';
import * as R from 'ramda';
import FinancialSheet from '../FinancialSheet';
import {
IJournalPoster,
ICustomer,
ICustomerBalanceSummaryCustomer,
ICustomerBalanceSummaryQuery,
ICustomerBalanceSummaryData,
ICustomerBalanceSummaryTotal,
INumberFormatQuery,
} from 'interfaces';
import { ContactBalanceSummaryReport } from '../ContactBalanceSummary/ContactBalanceSummary';
export class CustomerBalanceSummaryReport extends FinancialSheet {
receivableLedger: IJournalPoster;
baseCurrency: string;
customers: ICustomer[];
filter: ICustomerBalanceSummaryQuery;
export class CustomerBalanceSummaryReport extends ContactBalanceSummaryReport {
readonly receivableLedger: IJournalPoster;
readonly baseCurrency: string;
readonly customers: ICustomer[];
readonly filter: ICustomerBalanceSummaryQuery;
readonly numberFormat: INumberFormatQuery;
/**
* Constructor method.
@@ -35,21 +36,7 @@ export class CustomerBalanceSummaryReport extends FinancialSheet {
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),
};
this.numberFormat = this.filter.numberFormat;
}
/**
@@ -62,46 +49,10 @@ export class CustomerBalanceSummaryReport extends FinancialSheet {
return {
customerName: customer.displayName,
total: this.getAmountMeta(balance),
total: this.getContactTotalFormat(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.
@@ -124,62 +75,19 @@ export class CustomerBalanceSummaryReport extends FinancialSheet {
return R.compose(
R.when(
R.always(this.filter.comparison.percentageOfColumn),
this.customerCamparsionPercentageOfColumn.bind(this)
this.contactCamparsionPercentageOfColumn.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);
const customersTotal = this.getContactsTotalSection(customersSections);
return {
customers: customersSections,

View File

@@ -1,55 +1,17 @@
import { get } from 'lodash';
import { Service } from 'typedi';
import {
ICustomerBalanceSummaryData,
ICustomerBalanceSummaryCustomer,
ICustomerBalanceSummaryTotal,
ITableRow,
} from 'interfaces';
import { Service } from 'typedi';
interface IColumnMapperMeta {
key: string;
accessor?: string;
value?: string;
}
interface ITableCell {
value: string;
key: string;
}
type ITableRow = {
rows: ITableCell[];
};
import { tableMapper, tableRowMapper } from 'utils';
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 {
/**
@@ -100,9 +62,11 @@ export default class CustomerBalanceSummaryTableRows {
public tableRowsTransformer(
customerBalanceSummary: ICustomerBalanceSummaryData
): ITableRow[] {
return [
...this.customersTransformer(customerBalanceSummary.customers),
this.totalTransformer(customerBalanceSummary.total),
];
const customers = this.customersTransformer(
customerBalanceSummary.customers
);
const total = this.totalTransformer(customerBalanceSummary.total);
return customers.length > 0 ? [...customers, total] : [];
}
}

View File

@@ -9,6 +9,7 @@ export default class FinancialSheet {
* Transformes the number format query to settings
*/
protected transfromFormatQueryToSettings(): IFormatNumberSettings {
console.log(this.numberFormat, 'XX');
const { numberFormat } = this;
return {

View File

@@ -0,0 +1,7 @@
export default class TransactionsByContact extends ITransactionsByContacts {
}

View File

@@ -0,0 +1,98 @@
import * as R from 'ramda';
import {
IJournalPoster,
IVendor,
IVendorBalanceSummaryVendor,
IVendorBalanceSummaryQuery,
IVendorBalanceSummaryData,
IVendorBalanceSummaryTotal,
INumberFormatQuery,
} from 'interfaces';
import { ContactBalanceSummaryReport } from '../ContactBalanceSummary/ContactBalanceSummary';
export class VendorBalanceSummaryReport extends ContactBalanceSummaryReport {
readonly payableLedger: IJournalPoster;
readonly baseCurrency: string;
readonly vendors: IVendor[];
readonly filter: IVendorBalanceSummaryQuery;
readonly numberFormat: INumberFormatQuery;
/**
* Constructor method.
* @param {IJournalPoster} receivableLedger
* @param {IVendor[]} vendors
* @param {IVendorBalanceSummaryQuery} filter
* @param {string} baseCurrency
*/
constructor(
payableLedger: IJournalPoster,
vendors: IVendor[],
filter: IVendorBalanceSummaryQuery,
baseCurrency: string
) {
super();
this.payableLedger = payableLedger;
this.baseCurrency = baseCurrency;
this.vendors = vendors;
this.filter = filter;
this.numberFormat = this.filter.numberFormat;
}
/**
* Customer section mapper.
* @param {IVendor} vendor
* @returns {IVendorBalanceSummaryVendor}
*/
private vendorMapper(vendor: IVendor): IVendorBalanceSummaryVendor {
const balance = this.payableLedger.getContactBalance(null, vendor.id);
return {
vendorName: vendor.displayName,
total: this.getContactTotalFormat(balance),
};
}
/**
* Mappes the vendor model object to vendor balance summary section.
* @param {IVendor[]} vendors - Customers.
* @returns {IVendorBalanceSummaryVendor[]}
*/
private vendorsMapper(
vendors: IVendor[]
): IVendorBalanceSummaryVendor[] {
return vendors.map(this.vendorMapper.bind(this));
}
/**
* Retrieve the vendors sections of the report.
* @param {IVendor} vendors
* @returns {IVendorBalanceSummaryVendor[]}
*/
private getVendorsSection(
vendors: IVendor[]
): IVendorBalanceSummaryVendor[] {
return R.compose(
R.when(
R.always(this.filter.comparison.percentageOfColumn),
this.contactCamparsionPercentageOfColumn.bind(this)
),
this.vendorsMapper.bind(this)
).bind(this)(vendors);
}
/**
* Retrieve the report statement data.
* @returns {IVendorBalanceSummaryData}
*/
public reportData(): IVendorBalanceSummaryData {
const vendors = this.getVendorsSection(this.vendors);
const total = this.getContactsTotalSection(vendors);
return { vendors, total };
}
reportColumns() {
return [];
}
}

View File

@@ -0,0 +1,109 @@
import { Inject } from 'typedi';
import moment from 'moment';
import TenancyService from 'services/Tenancy/TenancyService';
import Journal from 'services/Accounting/JournalPoster';
import {
IVendorBalanceSummaryService,
IVendorBalanceSummaryQuery,
IVendorBalanceSummaryStatement,
} from 'interfaces';
import { VendorBalanceSummaryReport } from './VendorBalanceSummary';
export default class VendorBalanceSummaryService
implements IVendorBalanceSummaryService {
@Inject()
tenancy: TenancyService;
@Inject('logger')
logger: any;
/**
* Defaults balance sheet filter query.
* @return {IVendorBalanceSummaryQuery}
*/
get defaultQuery(): IVendorBalanceSummaryQuery {
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 - Tenant id.
* @param {IVendorBalanceSummaryQuery} query -
* @return {Promise<IVendorBalanceSummaryStatement>}
*/
async vendorBalanceSummary(
tenantId: number,
query: IVendorBalanceSummaryQuery
): Promise<IVendorBalanceSummaryStatement> {
const {
accountRepository,
transactionsRepository,
} = this.tenancy.repositories(tenantId);
const { Vendor } = 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 vendors = await Vendor.query().orderBy('displayName');
// Report instance.
const reportInstance = new VendorBalanceSummaryReport(
transactionsJournal,
vendors,
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,70 @@
import { Service } from 'typedi';
import { tableMapper, tableRowMapper } from 'utils';
import {
IVendorBalanceSummaryData,
IVendorBalanceSummaryVendor,
IVendorBalanceSummaryTotal,
ITableRow,
} from 'interfaces';
enum TABLE_ROWS_TYPES {
VENDOR = 'VENDOR',
TOTAL = 'TOTAL',
}
@Service()
export default class VendorBalanceSummaryTableRows {
/**
* Transformes the vendors to table rows.
* @param {IVendorBalanceSummaryVendor[]} vendors
* @returns {ITableRow[]}
*/
private vendorsTransformer(
vendors: IVendorBalanceSummaryVendor[]
): ITableRow[] {
const columns = [
{ key: 'vendorName', accessor: 'vendorName' },
{ key: 'total', accessor: 'total.formattedAmount' },
{
key: 'percentageOfColumn',
accessor: 'percentageOfColumn.formattedAmount',
},
];
return tableMapper(vendors, columns, {
rowTypes: [TABLE_ROWS_TYPES.VENDOR],
});
}
/**
* Transformes the total to table row.
* @param {IVendorBalanceSummaryTotal} total
* @returns {ITableRow}
*/
private totalTransformer(total: IVendorBalanceSummaryTotal) {
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 vendor balance summary to table rows.
* @param {IVendorBalanceSummaryData} vendorBalanceSummary
* @returns {ITableRow[]}
*/
public tableRowsTransformer(
vendorBalanceSummary: IVendorBalanceSummaryData
): ITableRow[] {
const vendors = this.vendorsTransformer(vendorBalanceSummary.vendors);
const total = this.totalTransformer(vendorBalanceSummary.total);
return vendors.length > 0 ? [...vendors, total] : [];
}
}