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 {
asDate: Date;
vendorsIds: number[],
numberFormat: INumberFormatQuery;
comparison: {
percentageOfColumn: boolean;

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,17 @@
import { Inject } from 'typedi';
import moment from 'moment';
import { groupBy } from 'lodash';
import * as R from 'ramda';
import { map } from 'lodash';
import TenancyService from 'services/Tenancy/TenancyService';
import {
IVendor,
ITransactionsByVendorsService,
ITransactionsByVendorsFilter,
ITransactionsByVendorsStatement,
} from 'interfaces';
import TransactionsByVendor from './TransactionsByVendor';
import { ACCOUNT_TYPE } from 'data/AccountTypes';
import Ledger from 'services/Accounting/Ledger';
export default class TransactionsByVendorsService
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.
* @param {number} tenantId
@@ -50,9 +148,8 @@ export default class TransactionsByVendorsService
tenantId: number,
query: ITransactionsByVendorsFilter
): Promise<ITransactionsByVendorsStatement> {
const { transactionsRepository } = this.tenancy.repositories(tenantId);
const { Vendor } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
// Settings tenant service.
const settings = this.tenancy.settings(tenantId);
const baseCurrency = settings.get({
@@ -60,37 +157,35 @@ export default class TransactionsByVendorsService
key: 'base_currency',
});
const filter = {
...this.defaultQuery,
...query,
};
const vendors = await Vendor.query().orderBy('displayName');
const filter = { ...this.defaultQuery, ...query };
// Retrieve all journal transactions based on the given query.
const transactions = await transactionsRepository.journal({
fromDate: query.fromDate,
toDate: query.toDate,
});
// Transactions map by contact id.
const transactionsMap = new Map(
Object.entries(groupBy(transactions, 'contactId'))
// Retrieve the report vendors.
const vendors = await this.getReportVendors(tenantId);
// Retrieve the accounts graph.
const accountsGraph = await accountRepository.getDependencyGraph();
// Journal transactions.
const journalTransactions = await this.getReportTransactions(
tenantId,
filter.fromDate,
filter.toDate
);
// Ledger collection.
const journal = new Ledger(journalTransactions);
// Transactions by customers data mapper.
const reportInstance = new TransactionsByVendor(
vendors,
transactionsMap,
accountsGraph,
journal,
filter,
baseCurrency
);
// Retrieve the report data.
const reportData = reportInstance.reportData();
// Retireve the report columns.
const reportColumns = reportInstance.reportColumns();
return {
data: reportData,
columns: reportColumns,
data: reportInstance.reportData(),
columns: reportInstance.reportColumns(),
query: filter,
};
}
}

View File

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

View File

@@ -1,20 +1,24 @@
import { Inject } from 'typedi';
import moment from 'moment';
import { map } from 'lodash';
import * as R from 'ramda';
import TenancyService from 'services/Tenancy/TenancyService';
import Journal from 'services/Accounting/JournalPoster';
import {
IVendor,
IVendorBalanceSummaryService,
IVendorBalanceSummaryQuery,
IVendorBalanceSummaryStatement,
} from 'interfaces';
import { VendorBalanceSummaryReport } from './VendorBalanceSummary';
import { isEmpty } from 'lodash';
import { ACCOUNT_TYPE } from 'data/AccountTypes';
import Ledger from 'services/Accounting/Ledger';
export default class VendorBalanceSummaryService
implements IVendorBalanceSummaryService {
@Inject()
tenancy: TenancyService;
@Inject('logger')
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.
* @param {number} tenantId - Tenant id.
* @param {IVendorBalanceSummaryQuery} query -
* @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',
group: 'organization',
key: 'base_currency',
});
const filter = {
...this.defaultQuery,
...query,
};
this.logger.info('[customer_balance_summary] trying to calculate the report.', {
filter,
const filter = { ...this.defaultQuery, ...query };
this.logger.info(
'[customer_balance_summary] trying to calculate the report.',
{
filter,
tenantId,
}
);
// Retrieve the vendors transactions.
const vendorsTransactions = await this.getReportVendorsTransactions(
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
query.asDate
);
// 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.
const reportInstance = new VendorBalanceSummaryReport(
transactionsJournal,
ledger,
vendors,
filter,
baseCurrency,
baseCurrency
);
// Retrieve the report statement.
const reportData = reportInstance.reportData();
// Retrieve the report columns.
const reportColumns = reportInstance.reportColumns();
return {
data: reportData,
columns: reportColumns,
data: reportInstance.reportData(),
columns: reportInstance.reportColumns(),
query: filter,
};
}
}