fix: customer balance and transactions report.

This commit is contained in:
a.bouhuolia
2021-05-08 01:26:42 +02:00
parent b5ed7af7eb
commit 531949e090
6 changed files with 95 additions and 57 deletions

View File

@@ -4,11 +4,13 @@ import {
IContactBalanceSummaryQuery, IContactBalanceSummaryQuery,
IContactBalanceSummaryAmount, IContactBalanceSummaryAmount,
IContactBalanceSummaryPercentage, IContactBalanceSummaryPercentage,
IContactBalanceSummaryTotal IContactBalanceSummaryTotal,
} from './ContactBalanceSummary'; } from './ContactBalanceSummary';
export interface ICustomerBalanceSummaryQuery export interface ICustomerBalanceSummaryQuery
extends IContactBalanceSummaryQuery {} extends IContactBalanceSummaryQuery {
customersIds?: number[];
}
export interface ICustomerBalanceSummaryAmount export interface ICustomerBalanceSummaryAmount
extends IContactBalanceSummaryAmount {} extends IContactBalanceSummaryAmount {}
@@ -22,7 +24,8 @@ export interface ICustomerBalanceSummaryCustomer {
percentageOfColumn?: ICustomerBalanceSummaryPercentage; percentageOfColumn?: ICustomerBalanceSummaryPercentage;
} }
export interface ICustomerBalanceSummaryTotal extends IContactBalanceSummaryTotal { export interface ICustomerBalanceSummaryTotal
extends IContactBalanceSummaryTotal {
total: ICustomerBalanceSummaryAmount; total: ICustomerBalanceSummaryAmount;
percentageOfColumn?: ICustomerBalanceSummaryPercentage; percentageOfColumn?: ICustomerBalanceSummaryPercentage;
} }

View File

@@ -5,6 +5,7 @@ export interface ILedger {
whereContactId(contactId: number): ILedger; whereContactId(contactId: number): ILedger;
whereFromDate(fromDate: Date | string): ILedger; whereFromDate(fromDate: Date | string): ILedger;
whereToDate(toDate: Date | string): ILedger; whereToDate(toDate: Date | string): ILedger;
getClosingBalance(): number;
} }
export interface ILedgerEntry { export interface ILedgerEntry {

View File

@@ -1,7 +1,7 @@
import { get } from 'lodash'; import { get } from 'lodash';
import * as R from 'ramda'; import * as R from 'ramda';
import { import {
IJournalPoster, ILedger,
ICustomer, ICustomer,
ICustomerBalanceSummaryCustomer, ICustomerBalanceSummaryCustomer,
ICustomerBalanceSummaryQuery, ICustomerBalanceSummaryQuery,
@@ -11,7 +11,7 @@ import {
import { ContactBalanceSummaryReport } from '../ContactBalanceSummary/ContactBalanceSummary'; import { ContactBalanceSummaryReport } from '../ContactBalanceSummary/ContactBalanceSummary';
export class CustomerBalanceSummaryReport extends ContactBalanceSummaryReport { export class CustomerBalanceSummaryReport extends ContactBalanceSummaryReport {
readonly receivableLedger: IJournalPoster; readonly ledger: ILedger;
readonly baseCurrency: string; readonly baseCurrency: string;
readonly customers: ICustomer[]; readonly customers: ICustomer[];
readonly filter: ICustomerBalanceSummaryQuery; readonly filter: ICustomerBalanceSummaryQuery;
@@ -25,14 +25,14 @@ export class CustomerBalanceSummaryReport extends ContactBalanceSummaryReport {
* @param {string} baseCurrency * @param {string} baseCurrency
*/ */
constructor( constructor(
receivableLedger: IJournalPoster, ledger: ILedger,
customers: ICustomer[], customers: ICustomer[],
filter: ICustomerBalanceSummaryQuery, filter: ICustomerBalanceSummaryQuery,
baseCurrency: string baseCurrency: string
) { ) {
super(); super();
this.receivableLedger = receivableLedger; this.ledger = ledger;
this.baseCurrency = baseCurrency; this.baseCurrency = baseCurrency;
this.customers = customers; this.customers = customers;
this.filter = filter; this.filter = filter;
@@ -45,12 +45,13 @@ export class CustomerBalanceSummaryReport extends ContactBalanceSummaryReport {
* @returns {ICustomerBalanceSummaryCustomer} * @returns {ICustomerBalanceSummaryCustomer}
*/ */
private customerMapper(customer: ICustomer): ICustomerBalanceSummaryCustomer { private customerMapper(customer: ICustomer): ICustomerBalanceSummaryCustomer {
const customerBalance = this.receivableLedger.get(customer.id); const closingBalance = this.ledger
const balanceAmount = get(customerBalance, 'balance', 0); .whereContactId(customer.id)
.getClosingBalance();
return { return {
customerName: customer.displayName, customerName: customer.displayName,
total: this.getContactTotalFormat(balanceAmount), total: this.getContactTotalFormat(closingBalance),
}; };
} }
@@ -96,7 +97,11 @@ export class CustomerBalanceSummaryReport extends ContactBalanceSummaryReport {
}; };
} }
reportColumns() { /**
* Retrieve the report statement columns
* @returns
*/
public reportColumns() {
return []; return [];
} }
} }

View File

@@ -1,16 +1,17 @@
import { Inject } from 'typedi'; import { Inject } from 'typedi';
import moment from 'moment'; import moment from 'moment';
import { map } from 'lodash'; import { isEmpty, map } from 'lodash';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import * as R from 'ramda'; import * as R from 'ramda';
import { transformToMap } from 'utils';
import { import {
ICustomerBalanceSummaryService, ICustomerBalanceSummaryService,
ICustomerBalanceSummaryQuery, ICustomerBalanceSummaryQuery,
ICustomerBalanceSummaryStatement, ICustomerBalanceSummaryStatement,
ICustomer
} from 'interfaces'; } from 'interfaces';
import { CustomerBalanceSummaryReport } from './CustomerBalanceSummary'; import { CustomerBalanceSummaryReport } from './CustomerBalanceSummary';
import { ACCOUNT_TYPE } from 'data/AccountTypes'; import { ACCOUNT_TYPE } from 'data/AccountTypes';
import Ledger from 'services/Accounting/Ledger';
export default class CustomerBalanceSummaryService export default class CustomerBalanceSummaryService
implements ICustomerBalanceSummaryService { implements ICustomerBalanceSummaryService {
@@ -42,39 +43,64 @@ export default class CustomerBalanceSummaryService
}; };
} }
customersBalancesQuery(query) { /**
query.groupBy('contactId'); * Retrieve the A/R accounts.
query.sum('credit as credit'); * @param tenantId
query.sum('debit as debit'); * @returns
query.select('contactId'); */
private getReceivableAccounts(tenantId: number) {
const { Account } = this.tenancy.models(tenantId);
return Account.query().where(
'accountType',
ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE
);
} }
/** /**
* Retrieve the customers credit/debit totals * Retrieve the customers credit/debit totals
* @param {number} tenantId * @param {number} tenantId
* @returns * @returns
*/ */
async getCustomersCreditDebitTotals(tenantId: number) { private async getReportCustomersTransactions(tenantId: number, asDate: any) {
const { AccountTransaction, Account } = this.tenancy.models(tenantId); const { AccountTransaction } = this.tenancy.models(tenantId);
const receivableAccounts = await Account.query().where( // Retrieve the receivable accounts A/R.
'accountType', const receivableAccounts = await this.getReceivableAccounts(tenantId);
ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE
);
const receivableAccountsIds = map(receivableAccounts, 'id'); const receivableAccountsIds = map(receivableAccounts, 'id');
const customersTotals = await AccountTransaction.query().onBuild((query) => { // Retrieve the customers transactions of A/R accounts.
query.whereIn('accountId', receivableAccountsIds); const customersTranasctions = await AccountTransaction.query().onBuild(
this.customersBalancesQuery(query); (query) => {
}); query.whereIn('accountId', receivableAccountsIds);
query.modify('filterDateRange', null, asDate);
query.groupBy('contactId');
query.sum('credit as credit');
query.sum('debit as debit');
query.select('contactId');
}
);
const commonProps = { accountNormal: 'debit', date: asDate };
return R.compose( return R.map(R.merge(commonProps))(customersTranasctions);
(customers) => transformToMap(customers, 'contactId'), }
(customers) => customers.map((customer) => ({
...customer, /**
balance: customer.debit - customer.credit * Retrieve the report customers.
})), * @param {number} tenantId
)(customersTotals); * @param {number[]} customersIds
* @returns {ICustomer[]}
*/
private getReportCustomers(tenantId: number, customersIds: number[]): ICustomer[] {
const { Customer } = this.tenancy.models(tenantId);
return Customer.query()
.orderBy('displayName')
.onBuild((query) => {
if (!isEmpty(customersIds)) {
query.whereIn('id', customersIds);
}
});
} }
/** /**
@@ -87,19 +113,15 @@ export default class CustomerBalanceSummaryService
tenantId: number, tenantId: number,
query: ICustomerBalanceSummaryQuery query: ICustomerBalanceSummaryQuery
): Promise<ICustomerBalanceSummaryStatement> { ): Promise<ICustomerBalanceSummaryStatement> {
const { Customer } = this.tenancy.models(tenantId);
// Settings tenant service. // Settings tenant service.
const settings = this.tenancy.settings(tenantId); const settings = this.tenancy.settings(tenantId);
const baseCurrency = settings.get({ const baseCurrency = settings.get({
group: 'organization', group: 'organization',
key: 'base_currency', key: 'base_currency',
}); });
// Merges the default query and request query.
const filter = { ...this.defaultQuery, ...query };
const filter = {
...this.defaultQuery,
...query,
};
this.logger.info( this.logger.info(
'[customer_balance_summary] trying to calculate the report.', '[customer_balance_summary] trying to calculate the report.',
{ {
@@ -108,27 +130,30 @@ export default class CustomerBalanceSummaryService
} }
); );
// Retrieve the customers list ordered by the display name. // Retrieve the customers list ordered by the display name.
const customers = await Customer.query().orderBy('displayName'); const customers = await this.getReportCustomers(
tenantId,
query.customersIds
);
// Retrieve the customers debit/credit totals. // Retrieve the customers debit/credit totals.
const customersBalances = await this.getCustomersCreditDebitTotals(tenantId); const customersTransactions = await this.getReportCustomersTransactions(
tenantId,
filter.asDate
);
// Ledger query.
const ledger = new Ledger(customersTransactions);
// Report instance. // Report instance.
const reportInstance = new CustomerBalanceSummaryReport( const report = new CustomerBalanceSummaryReport(
customersBalances, ledger,
customers, customers,
filter, filter,
baseCurrency baseCurrency
); );
// Retrieve the report statement.
const reportData = reportInstance.reportData();
// Retrieve the report columns.
const reportColumns = reportInstance.reportColumns();
return { return {
data: reportData, data: report.reportData(),
columns: reportColumns, columns: report.reportColumns(),
query: filter
}; };
} }
} }

View File

@@ -142,7 +142,6 @@ export default class TransactionsByCustomersService
...this.defaultQuery, ...this.defaultQuery,
...query, ...query,
}; };
const accountsGraph = await accountRepository.getDependencyGraph(); const accountsGraph = await accountRepository.getDependencyGraph();
const customers = await Customer.query().orderBy('displayName'); const customers = await Customer.query().orderBy('displayName');
@@ -181,6 +180,7 @@ export default class TransactionsByCustomersService
return { return {
data: reportData, data: reportData,
columns: reportColumns, columns: reportColumns,
query: filter,
}; };
} }
} }

View File

@@ -17,7 +17,11 @@ export default class TransactionsByCustomersTableRows extends TransactionsByCont
* @returns {ITableRow[]} * @returns {ITableRow[]}
*/ */
private customerDetails(customer: ITransactionsByCustomersCustomer) { private customerDetails(customer: ITransactionsByCustomersCustomer) {
const columns = [{ key: 'customerName', accessor: 'customerName' }]; const columns = [
{ key: 'customerName', accessor: 'customerName' },
...R.repeat({ key: 'empty', value: '' }, 5),
{ key: 'closingBalanceValue', accessor: 'closingBalance.formattedAmount' },
];
return { return {
...tableRowMapper(customer, columns, { rowTypes: [ROW_TYPE.CUSTOMER] }), ...tableRowMapper(customer, columns, { rowTypes: [ROW_TYPE.CUSTOMER] }),