fix: vendor summary balance and transaction report.

This commit is contained in:
a.bouhuolia
2021-05-08 04:32:13 +02:00
parent 531949e090
commit e174b81e16
8 changed files with 237 additions and 94 deletions

View File

@@ -2,6 +2,7 @@ import { INumberFormatQuery } from './FinancialStatements';
export interface IVendorBalanceSummaryQuery { export interface IVendorBalanceSummaryQuery {
asDate: Date; asDate: Date;
vendorsIds: number[],
numberFormat: INumberFormatQuery; numberFormat: INumberFormatQuery;
comparison: { comparison: {
percentageOfColumn: boolean; percentageOfColumn: boolean;

View File

@@ -113,7 +113,7 @@ export class ContactBalanceSummaryReport extends FinancialSheet {
protected getTotalFormat(amount: number): IContactBalanceSummaryAmount { protected getTotalFormat(amount: number): IContactBalanceSummaryAmount {
return { return {
amount, amount,
formattedAmount: this.formatNumber(amount, { money: true }), formattedAmount: this.formatTotalNumber(amount, { money: true }),
currencyCode: this.baseCurrency, currencyCode: this.baseCurrency,
}; };
} }

View File

@@ -1,13 +1,10 @@
import * as R from 'ramda'; import * as R from 'ramda';
import { sumBy } from 'lodash';
import { import {
ITransactionsByCustomersTransaction, ITransactionsByCustomersTransaction,
ITransactionsByCustomersFilter, ITransactionsByCustomersFilter,
ITransactionsByCustomersCustomer, ITransactionsByCustomersCustomer,
ITransactionsByCustomersAmount,
ITransactionsByCustomersData, ITransactionsByCustomersData,
INumberFormatQuery, INumberFormatQuery,
IAccountTransaction,
ICustomer, ICustomer,
} from 'interfaces'; } from 'interfaces';
import TransactionsByContact from '../TransactionsByContact/TransactionsByContact'; import TransactionsByContact from '../TransactionsByContact/TransactionsByContact';

View File

@@ -173,13 +173,10 @@ export default class TransactionsByCustomersService
filter, filter,
baseCurrency baseCurrency
); );
const reportData = reportInstance.reportData();
const reportColumns = reportInstance.reportColumns();
return { return {
data: reportData, data: reportInstance.reportData(),
columns: reportColumns, columns: reportInstance.reportColumns(),
query: filter, query: filter,
}; };
} }

View File

@@ -4,9 +4,8 @@ import {
ITransactionsByVendorsFilter, ITransactionsByVendorsFilter,
ITransactionsByVendorsTransaction, ITransactionsByVendorsTransaction,
ITransactionsByVendorsVendor, ITransactionsByVendorsVendor,
ITransactionsByVendorsAmount,
ITransactionsByVendorsData, ITransactionsByVendorsData,
IAccountTransaction, ILedger,
INumberFormatQuery, INumberFormatQuery,
IVendor IVendor
} from 'interfaces'; } from 'interfaces';
@@ -18,6 +17,8 @@ export default class TransactionsByVendors extends TransactionsByContact{
readonly filter: ITransactionsByVendorsFilter; readonly filter: ITransactionsByVendorsFilter;
readonly baseCurrency: string; readonly baseCurrency: string;
readonly numberFormat: INumberFormatQuery; readonly numberFormat: INumberFormatQuery;
readonly accountsGraph: any;
readonly ledger: ILedger;
/** /**
* Constructor method. * Constructor method.
@@ -27,14 +28,16 @@ export default class TransactionsByVendors extends TransactionsByContact{
*/ */
constructor( constructor(
vendors: IVendor[], vendors: IVendor[],
transactionsByContact: Map<number, IAccountTransaction[]>, accountsGraph: any,
ledger: ILedger,
filter: ITransactionsByVendorsFilter, filter: ITransactionsByVendorsFilter,
baseCurrency: string baseCurrency: string
) { ) {
super(); super();
this.contacts = vendors; this.contacts = vendors;
this.transactionsByContact = transactionsByContact; this.accountsGraph = accountsGraph;
this.ledger = ledger;
this.baseCurrency = baseCurrency; this.baseCurrency = baseCurrency;
this.filter = filter; this.filter = filter;
this.numberFormat = this.filter.numberFormat; this.numberFormat = this.filter.numberFormat;
@@ -50,12 +53,17 @@ export default class TransactionsByVendors extends TransactionsByContact{
vendorId: number, vendorId: number,
openingBalance: number openingBalance: number
): ITransactionsByVendorsTransaction[] { ): ITransactionsByVendorsTransaction[] {
const transactions = this.transactionsByContact.get(vendorId + '') || []; const openingBalanceLedger = this.ledger
.whereContactId(vendorId)
.whereFromDate(this.filter.fromDate)
.whereToDate(this.filter.toDate);
const openingEntries = openingBalanceLedger.getEntries();
return R.compose( return R.compose(
R.curry(this.contactTransactionRunningBalance)(openingBalance), R.curry(this.contactTransactionRunningBalance)(openingBalance),
R.map(this.contactTransactionMapper.bind(this)) R.map(this.contactTransactionMapper.bind(this))
).bind(this)(transactions); ).bind(this)(openingEntries);
} }
/** /**
@@ -66,17 +74,17 @@ export default class TransactionsByVendors extends TransactionsByContact{
private vendorMapper( private vendorMapper(
vendor: IVendor vendor: IVendor
): ITransactionsByVendorsVendor { ): ITransactionsByVendorsVendor {
const openingBalance = this.getContactOpeningBalance(1); const openingBalance = this.getContactOpeningBalance(vendor.id);
const transactions = this.vendorTransactions(vendor.id, openingBalance); const transactions = this.vendorTransactions(vendor.id, openingBalance);
const closingBalance = this.getContactClosingBalance(transactions, 0); const closingBalance = this.getContactClosingBalance(transactions, openingBalance);
return { return {
vendorName: vendor.displayName, vendorName: vendor.displayName,
openingBalance: this.getContactAmount( openingBalance: this.getTotalAmountMeta(
openingBalance, openingBalance,
vendor.currencyCode vendor.currencyCode
), ),
closingBalance: this.getContactAmount( closingBalance: this.getTotalAmountMeta(
closingBalance, closingBalance,
vendor.currencyCode vendor.currencyCode
), ),

View File

@@ -1,13 +1,17 @@
import { Inject } from 'typedi'; import { Inject } from 'typedi';
import moment from 'moment'; import moment from 'moment';
import { groupBy } from 'lodash'; import * as R from 'ramda';
import { map } from 'lodash';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import { import {
IVendor,
ITransactionsByVendorsService, ITransactionsByVendorsService,
ITransactionsByVendorsFilter, ITransactionsByVendorsFilter,
ITransactionsByVendorsStatement, ITransactionsByVendorsStatement,
} from 'interfaces'; } from 'interfaces';
import TransactionsByVendor from './TransactionsByVendor'; import TransactionsByVendor from './TransactionsByVendor';
import { ACCOUNT_TYPE } from 'data/AccountTypes';
import Ledger from 'services/Accounting/Ledger';
export default class TransactionsByVendorsService export default class TransactionsByVendorsService
implements ITransactionsByVendorsService { implements ITransactionsByVendorsService {
@@ -40,6 +44,100 @@ export default class TransactionsByVendorsService
}; };
} }
/**
* Retrieve the report vendors.
* @param tenantId
* @returns
*/
private getReportVendors(tenantId: number): Promise<IVendor[]> {
const { Vendor } = this.tenancy.models(tenantId);
return Vendor.query().orderBy('displayName');
}
/**
* Retrieve the accounts receivable.
* @param {number} tenantId
* @returns
*/
private async getPayableAccounts(tenantId: number) {
const { Account } = this.tenancy.models(tenantId);
const accounts = await Account.query().where(
'accountType',
ACCOUNT_TYPE.ACCOUNTS_PAYABLE
);
return accounts;
}
/**
* Retrieve the customers opening balance transactions.
* @param {number} tenantId
* @param {number} openingDate
* @param {number} customersIds
* @returns {}
*/
private async getVendorsOpeningBalance(
tenantId: number,
openingDate: Date,
customersIds?: number[]
): Promise<ILedgerEntry[]> {
const { AccountTransaction } = this.tenancy.models(tenantId);
const payableAccounts = await this.getPayableAccounts(tenantId);
const payableAccountsIds = map(payableAccounts, 'id');
const openingTransactions = await AccountTransaction.query().modify(
'contactsOpeningBalance',
openingDate,
payableAccountsIds,
customersIds
);
return R.compose(
R.map(R.assoc('date', openingDate)),
R.map(R.assoc('accountNormal', 'credit'))
)(openingTransactions);
}
/**
*
* @param {number} tenantId
* @param {Date|string} openingDate
* @param {number[]} customersIds
*/
async getVendorsPeriodTransactions(
tenantId: number,
fromDate: Date,
toDate: Date
): Promise<ILedgerEntry[]> {
const { AccountTransaction } = this.tenancy.models(tenantId);
const receivableAccounts = await this.getPayableAccounts(tenantId);
const receivableAccountsIds = map(receivableAccounts, 'id');
const transactions = await AccountTransaction.query().onBuild((query) => {
// Filter by date.
query.modify('filterDateRange', fromDate, toDate);
// Filter by customers.
query.whereNot('contactId', null);
// Filter by accounts.
query.whereIn('accountId', receivableAccountsIds);
});
return R.compose(R.map(R.assoc('accountNormal', 'credit')))(transactions);
}
async getReportTransactions(tenantId: number, fromDate: Date, toDate: Date) {
const openingBalanceDate = moment(fromDate).subtract(1, 'days').toDate();
return [
...(await this.getVendorsOpeningBalance(tenantId, openingBalanceDate)),
...(await this.getVendorsPeriodTransactions(tenantId, fromDate, toDate)),
];
}
/** /**
* Retrieve transactions by by the customers. * Retrieve transactions by by the customers.
* @param {number} tenantId * @param {number} tenantId
@@ -50,9 +148,8 @@ export default class TransactionsByVendorsService
tenantId: number, tenantId: number,
query: ITransactionsByVendorsFilter query: ITransactionsByVendorsFilter
): Promise<ITransactionsByVendorsStatement> { ): Promise<ITransactionsByVendorsStatement> {
const { transactionsRepository } = this.tenancy.repositories(tenantId); const { accountRepository } = this.tenancy.repositories(tenantId);
const { Vendor } = 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({
@@ -60,37 +157,35 @@ export default class TransactionsByVendorsService
key: 'base_currency', key: 'base_currency',
}); });
const filter = { const filter = { ...this.defaultQuery, ...query };
...this.defaultQuery,
...query,
};
const vendors = await Vendor.query().orderBy('displayName');
// Retrieve all journal transactions based on the given query. // Retrieve the report vendors.
const transactions = await transactionsRepository.journal({ const vendors = await this.getReportVendors(tenantId);
fromDate: query.fromDate,
toDate: query.toDate, // Retrieve the accounts graph.
}); const accountsGraph = await accountRepository.getDependencyGraph();
// Transactions map by contact id.
const transactionsMap = new Map( // Journal transactions.
Object.entries(groupBy(transactions, 'contactId')) const journalTransactions = await this.getReportTransactions(
tenantId,
filter.fromDate,
filter.toDate
); );
// Ledger collection.
const journal = new Ledger(journalTransactions);
// Transactions by customers data mapper. // Transactions by customers data mapper.
const reportInstance = new TransactionsByVendor( const reportInstance = new TransactionsByVendor(
vendors, vendors,
transactionsMap, accountsGraph,
journal,
filter, filter,
baseCurrency baseCurrency
); );
// Retrieve the report data.
const reportData = reportInstance.reportData();
// Retireve the report columns.
const reportColumns = reportInstance.reportColumns();
return { return {
data: reportData, data: reportInstance.reportData(),
columns: reportColumns, columns: reportInstance.reportColumns(),
query: filter,
}; };
} }
} }

View File

@@ -1,17 +1,16 @@
import * as R from 'ramda'; import * as R from 'ramda';
import { import {
IJournalPoster, ILedger,
IVendor, IVendor,
IVendorBalanceSummaryVendor, IVendorBalanceSummaryVendor,
IVendorBalanceSummaryQuery, IVendorBalanceSummaryQuery,
IVendorBalanceSummaryData, IVendorBalanceSummaryData,
IVendorBalanceSummaryTotal,
INumberFormatQuery, INumberFormatQuery,
} from 'interfaces'; } from 'interfaces';
import { ContactBalanceSummaryReport } from '../ContactBalanceSummary/ContactBalanceSummary'; import { ContactBalanceSummaryReport } from '../ContactBalanceSummary/ContactBalanceSummary';
export class VendorBalanceSummaryReport extends ContactBalanceSummaryReport { export class VendorBalanceSummaryReport extends ContactBalanceSummaryReport {
readonly payableLedger: IJournalPoster; readonly ledger: ILedger;
readonly baseCurrency: string; readonly baseCurrency: string;
readonly vendors: IVendor[]; readonly vendors: IVendor[];
readonly filter: IVendorBalanceSummaryQuery; readonly filter: IVendorBalanceSummaryQuery;
@@ -25,14 +24,14 @@ export class VendorBalanceSummaryReport extends ContactBalanceSummaryReport {
* @param {string} baseCurrency * @param {string} baseCurrency
*/ */
constructor( constructor(
payableLedger: IJournalPoster, ledger: ILedger,
vendors: IVendor[], vendors: IVendor[],
filter: IVendorBalanceSummaryQuery, filter: IVendorBalanceSummaryQuery,
baseCurrency: string baseCurrency: string
) { ) {
super(); super();
this.payableLedger = payableLedger; this.ledger = ledger;
this.baseCurrency = baseCurrency; this.baseCurrency = baseCurrency;
this.vendors = vendors; this.vendors = vendors;
this.filter = filter; this.filter = filter;
@@ -45,11 +44,13 @@ export class VendorBalanceSummaryReport extends ContactBalanceSummaryReport {
* @returns {IVendorBalanceSummaryVendor} * @returns {IVendorBalanceSummaryVendor}
*/ */
private vendorMapper(vendor: IVendor): IVendorBalanceSummaryVendor { private vendorMapper(vendor: IVendor): IVendorBalanceSummaryVendor {
const balance = this.payableLedger.getContactBalance(null, vendor.id); const closingBalance = this.ledger
.whereContactId(vendor.id)
.getClosingBalance();
return { return {
vendorName: vendor.displayName, vendorName: vendor.displayName,
total: this.getContactTotalFormat(balance), total: this.getContactTotalFormat(closingBalance),
}; };
} }

View File

@@ -1,20 +1,24 @@
import { Inject } from 'typedi'; import { Inject } from 'typedi';
import moment from 'moment'; import moment from 'moment';
import { map } from 'lodash';
import * as R from 'ramda';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import Journal from 'services/Accounting/JournalPoster';
import { import {
IVendor,
IVendorBalanceSummaryService, IVendorBalanceSummaryService,
IVendorBalanceSummaryQuery, IVendorBalanceSummaryQuery,
IVendorBalanceSummaryStatement, IVendorBalanceSummaryStatement,
} from 'interfaces'; } from 'interfaces';
import { VendorBalanceSummaryReport } from './VendorBalanceSummary'; import { VendorBalanceSummaryReport } from './VendorBalanceSummary';
import { isEmpty } from 'lodash';
import { ACCOUNT_TYPE } from 'data/AccountTypes';
import Ledger from 'services/Accounting/Ledger';
export default class VendorBalanceSummaryService export default class VendorBalanceSummaryService
implements IVendorBalanceSummaryService { implements IVendorBalanceSummaryService {
@Inject() @Inject()
tenancy: TenancyService; tenancy: TenancyService;
@Inject('logger') @Inject('logger')
logger: any; logger: any;
@@ -40,70 +44,110 @@ export default class VendorBalanceSummaryService
}; };
} }
/**
* Retrieve the report vendors.
* @param {number} tenantId
* @param {number[]} vendorsIds - Vendors ids.
* @returns {IVendor[]}
*/
getReportVendors(
tenantId: number,
vendorsIds?: number[]
): Promise<IVendor[]> {
const { Vendor } = this.tenancy.models(tenantId);
return Vendor.query()
.orderBy('displayName')
.onBuild((query) => {
if (!isEmpty(vendorsIds)) {
query.whereIn('id', vendorsIds);
}
});
}
getPayableAccounts(tenantId: number) {
const { Account } = this.tenancy.models(tenantId);
return Account.query().where('accountType', ACCOUNT_TYPE.ACCOUNTS_PAYABLE);
}
/**
* Retrieve
* @param tenantId
* @param asDate
* @returns
*/
async getReportVendorsTransactions(tenantId: number, asDate: Date | string) {
const { AccountTransaction } = this.tenancy.models(tenantId);
// Retrieve payable accounts .
const payableAccounts = await this.getPayableAccounts(tenantId);
const payableAccountsIds = map(payableAccounts, 'id');
// Retrieve the customers transactions of A/R accounts.
const customersTranasctions = await AccountTransaction.query().onBuild(
(query) => {
query.whereIn('accountId', payableAccountsIds);
query.modify('filterDateRange', null, asDate);
query.groupBy('contactId');
query.sum('credit as credit');
query.sum('debit as debit');
query.select('contactId');
}
);
const commonProps = { accountNormal: 'credit', date: asDate };
return R.map(R.merge(commonProps))(customersTranasctions);
}
/** /**
* Retrieve the statment of customer balance summary report. * Retrieve the statment of customer balance summary report.
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
* @param {IVendorBalanceSummaryQuery} query - * @param {IVendorBalanceSummaryQuery} query -
* @return {Promise<IVendorBalanceSummaryStatement>} * @return {Promise<IVendorBalanceSummaryStatement>}
*/ */
async vendorBalanceSummary( async vendorBalanceSummary(
tenantId: number, tenantId: number,
query: IVendorBalanceSummaryQuery query: IVendorBalanceSummaryQuery
): Promise<IVendorBalanceSummaryStatement> { ): Promise<IVendorBalanceSummaryStatement> {
const {
accountRepository,
transactionsRepository,
} = this.tenancy.repositories(tenantId);
const { Vendor } = 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', key: 'base_currency', group: 'organization',
key: 'base_currency',
}); });
const filter = { const filter = { ...this.defaultQuery, ...query };
...this.defaultQuery, this.logger.info(
...query, '[customer_balance_summary] trying to calculate the report.',
}; {
this.logger.info('[customer_balance_summary] trying to calculate the report.', { filter,
filter, tenantId,
}
);
// Retrieve the vendors transactions.
const vendorsTransactions = await this.getReportVendorsTransactions(
tenantId, tenantId,
}); query.asDate
// 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. // Retrieve the customers list ordered by the display name.
const vendors = await Vendor.query().orderBy('displayName'); const vendors = await this.getReportVendors(tenantId, query.vendorsIds);
// Ledger query.
const ledger = new Ledger(vendorsTransactions);
// Report instance. // Report instance.
const reportInstance = new VendorBalanceSummaryReport( const reportInstance = new VendorBalanceSummaryReport(
transactionsJournal, ledger,
vendors, vendors,
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: reportInstance.reportData(),
columns: reportColumns, columns: reportInstance.reportColumns(),
query: filter,
}; };
} }
} }