From e174b81e16d8966238fb44cd05440df64502c97d Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Sat, 8 May 2021 04:32:13 +0200 Subject: [PATCH] fix: vendor summary balance and transaction report. --- server/src/interfaces/VendorBalanceSummary.ts | 1 + .../ContactBalanceSummary.ts | 2 +- .../TransactionsByCustomers.ts | 3 - .../TransactionsByCustomersService.ts | 7 +- .../TransactionsByVendor.ts | 28 ++-- .../TransactionsByVendorService.ts | 147 ++++++++++++++---- .../VendorBalanceSummary.ts | 15 +- .../VendorBalanceSummaryService.ts | 128 ++++++++++----- 8 files changed, 237 insertions(+), 94 deletions(-) diff --git a/server/src/interfaces/VendorBalanceSummary.ts b/server/src/interfaces/VendorBalanceSummary.ts index b06cd26fd..2fd4ecae4 100644 --- a/server/src/interfaces/VendorBalanceSummary.ts +++ b/server/src/interfaces/VendorBalanceSummary.ts @@ -2,6 +2,7 @@ import { INumberFormatQuery } from './FinancialStatements'; export interface IVendorBalanceSummaryQuery { asDate: Date; + vendorsIds: number[], numberFormat: INumberFormatQuery; comparison: { percentageOfColumn: boolean; diff --git a/server/src/services/FinancialStatements/ContactBalanceSummary/ContactBalanceSummary.ts b/server/src/services/FinancialStatements/ContactBalanceSummary/ContactBalanceSummary.ts index 0d8d84aa1..62439cca3 100644 --- a/server/src/services/FinancialStatements/ContactBalanceSummary/ContactBalanceSummary.ts +++ b/server/src/services/FinancialStatements/ContactBalanceSummary/ContactBalanceSummary.ts @@ -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, }; } diff --git a/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomers.ts b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomers.ts index 71b04e21a..853e8e9a6 100644 --- a/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomers.ts +++ b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomers.ts @@ -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'; diff --git a/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts index 40a9c956c..bc9c91160 100644 --- a/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts +++ b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts @@ -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, }; } diff --git a/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendor.ts b/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendor.ts index 41c200964..7d48d6f71 100644 --- a/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendor.ts +++ b/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendor.ts @@ -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, + 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 ), diff --git a/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorService.ts b/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorService.ts index 7993c94c3..456750a45 100644 --- a/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorService.ts +++ b/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorService.ts @@ -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 { + 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 { + 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 { + 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 { - 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, }; } } diff --git a/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummary.ts b/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummary.ts index f98a47972..fee03080a 100644 --- a/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummary.ts +++ b/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummary.ts @@ -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), }; } diff --git a/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryService.ts b/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryService.ts index 45c97ea0c..1df040a4a 100644 --- a/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryService.ts +++ b/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryService.ts @@ -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 { + 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} */ async vendorBalanceSummary( tenantId: number, query: IVendorBalanceSummaryQuery ): Promise { - 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, }; } }