diff --git a/server/src/api/controllers/FinancialStatements/TransactionsByVendors/index.ts b/server/src/api/controllers/FinancialStatements/TransactionsByVendors/index.ts index 4b3dc5f12..2f24c2259 100644 --- a/server/src/api/controllers/FinancialStatements/TransactionsByVendors/index.ts +++ b/server/src/api/controllers/FinancialStatements/TransactionsByVendors/index.ts @@ -6,6 +6,7 @@ import BaseFinancialReportController from '../BaseFinancialReportController'; import TransactionsByVendorsTableRows from 'services/FinancialStatements/TransactionsByVendor/TransactionsByVendorTableRows'; import TransactionsByVendorsService from 'services/FinancialStatements/TransactionsByVendor/TransactionsByVendorService'; import { ITransactionsByVendorsStatement } from 'interfaces'; + export default class TransactionsByVendorsReportController extends BaseFinancialReportController { @Inject() transactionsByVendorsService: TransactionsByVendorsService; @@ -44,7 +45,7 @@ export default class TransactionsByVendorsReportController extends BaseFinancial * Transformes the report statement to table rows. * @param {ITransactionsByVendorsStatement} statement - */ - transformToTableRows({ data }: ITransactionsByVendorsStatement) { + private transformToTableRows({ data }: ITransactionsByVendorsStatement) { return { table: { data: this.transactionsByVendorsTableRows.tableRows(data), @@ -56,7 +57,7 @@ export default class TransactionsByVendorsReportController extends BaseFinancial * Transformes the report statement to json response. * @param {ITransactionsByVendorsStatement} statement - */ - transformToJsonResponse({ + private transformToJsonResponse({ data, columns, query, diff --git a/server/src/interfaces/Ledger.ts b/server/src/interfaces/Ledger.ts new file mode 100644 index 000000000..89f829ae2 --- /dev/null +++ b/server/src/interfaces/Ledger.ts @@ -0,0 +1,17 @@ +export interface ILedger { + entries: ILedgerEntry[]; + + getEntries(): ILedgerEntry[]; + whereContactId(contactId: number): ILedger; + whereFromDate(fromDate: Date | string): ILedger; + whereToDate(toDate: Date | string): ILedger; +} + +export interface ILedgerEntry { + credit: number; + debit: number; + accountId?: number; + accountNormal: string; + contactId?: number; + date: Date | string; +} diff --git a/server/src/interfaces/TransactionsByContacts.ts b/server/src/interfaces/TransactionsByContacts.ts index dfb02b1e9..0c9b96ae5 100644 --- a/server/src/interfaces/TransactionsByContacts.ts +++ b/server/src/interfaces/TransactionsByContacts.ts @@ -10,6 +10,7 @@ export interface ITransactionsByContactsTransaction { date: string|Date, credit: ITransactionsByContactsAmount; debit: ITransactionsByContactsAmount; + accountName: string, runningBalance: ITransactionsByContactsAmount; currencyCode: string; referenceNumber: string; diff --git a/server/src/interfaces/index.ts b/server/src/interfaces/index.ts index 89d226413..090f22f32 100644 --- a/server/src/interfaces/index.ts +++ b/server/src/interfaces/index.ts @@ -49,4 +49,5 @@ export * from './ContactBalanceSummary'; export * from './TransactionsByCustomers'; export * from './TransactionsByContacts'; export * from './TransactionsByVendors'; -export * from './Table'; \ No newline at end of file +export * from './Table'; +export * from './Ledger'; \ No newline at end of file diff --git a/server/src/models/AccountTransaction.js b/server/src/models/AccountTransaction.js index 9465084a4..0d615a294 100644 --- a/server/src/models/AccountTransaction.js +++ b/server/src/models/AccountTransaction.js @@ -120,6 +120,25 @@ export default class AccountTransaction extends TenantModel { query.modify('filterDateRange', null, toDate) query.modify('sumationCreditDebit') }, + + contactsOpeningBalance(query, openingDate, receivableAccounts, customersIds) { + // Filter by date. + query.modify('filterDateRange', null, openingDate); + + // Filter by customers. + query.whereNot('contactId', null); + query.where('accountId', receivableAccounts); + + if (customersIds) { + query.whereIn('contactId', customersIds); + } + + // Group by the contact transactions. + query.groupBy('contactId'); + query.sum('credit as credit'); + query.sum('debit as debit'); + query.select('contactId'); + } }; } diff --git a/server/src/services/Accounting/JournalPoster.ts b/server/src/services/Accounting/JournalPoster.ts index dc26a1d71..befb71f64 100644 --- a/server/src/services/Accounting/JournalPoster.ts +++ b/server/src/services/Accounting/JournalPoster.ts @@ -694,4 +694,51 @@ export default class JournalPoster implements IJournalPoster { getAccountEntries(accountId: number) { return this.entries.filter((entry) => entry.account === accountId); } + + /** + * Retrieve total balnace of the given customer/vendor contact. + * @param {Number} accountId + * @param {Number} contactId + * @param {String} contactType + * @param {Date} closingDate + */ + getEntriesBalance( + entries + ) { + let balance = 0; + + entries.forEach((entry) => { + if (entry.credit) { + balance -= entry.credit; + } + if (entry.debit) { + balance += entry.debit; + } + }); + return balance; + } + + getContactEntries( + contactId: number, + openingDate: Date, + closingDate?: Date, + ) { + const momentClosingDate = moment(closingDate); + const momentOpeningDate = moment(openingDate); + + return this.entries.filter((entry) => { + if ( + (closingDate && + !momentClosingDate.isAfter(entry.date, 'day') && + !momentClosingDate.isSame(entry.date, 'day')) || + (openingDate && + !momentOpeningDate.isBefore(entry.date, 'day') && + !momentOpeningDate.isSame(entry.date)) || + (entry.contactId === contactId) + ) { + return true; + } + return false; + }); + } } diff --git a/server/src/services/Accounting/Ledger.ts b/server/src/services/Accounting/Ledger.ts new file mode 100644 index 000000000..2831651cd --- /dev/null +++ b/server/src/services/Accounting/Ledger.ts @@ -0,0 +1,92 @@ +import moment from 'moment'; +import { + ILedger, + ILedgerEntry +} from 'interfaces'; + +export default class Ledger implements ILedger { + readonly entries: ILedgerEntry[]; + + /** + * Constructor method. + * @param {ILedgerEntry[]} entries + */ + constructor(entries: ILedgerEntry[]) { + this.entries = Ledger.mappingEntries(entries); + } + + /** + * Filters the ledegr entries. + * @param callback + * @returns + */ + filter(callback) { + const entries = this.entries.filter(callback); + return new Ledger(entries); + } + + getEntries(): ILedgerEntry[] { + return this.entries; + } + + whereContactId(contactId: number): ILedger { + return this.filter((entry) => entry.contactId === contactId); + } + + whereAccountId(accountId: number): ILedger { + return this.filter((entry) => entry.accountId === accountId); + } + + whereFromDate(fromDate: Date | string): ILedger { + const fromDateParsed = moment(fromDate); + + return this.filter( + (entry) => + fromDateParsed.isBefore(entry.date) || fromDateParsed.isSame(entry.date) + ); + } + + whereToDate(toDate: Date | string): ILedger { + const toDateParsed = moment(toDate); + + return this.filter( + (entry) => + toDateParsed.isAfter(entry.date) || toDateParsed.isSame(entry.date) + ); + } + + /** + * Retrieve the closing balance of the entries. + * @returns {number} + */ + getClosingBalance() { + let closingBalance = 0; + + this.entries.forEach((entry) => { + if (entry.accountNormal === 'credit') { + closingBalance += entry.credit ? entry.credit : -1 * entry.debit; + + } else if (entry.accountNormal === 'debit') { + closingBalance += entry.debit ? entry.debit : -1 * entry.credit; + } + }); + return closingBalance; + } + + static mappingEntries(entries): ILedgerEntry[] { + return entries.map(this.mapEntry); + } + + static mapEntry(entry): ILedgerEntry { + return { + credit: entry.credit, + debit: entry.debit, + accountNormal: entry.accountNormal, + accountId: entry.accountId, + contactId: entry.contactId, + date: entry.date, + transactionNumber: entry.transactionNumber, + referenceNumber: entry.referenceNumber, + } + } +} diff --git a/server/src/services/FinancialStatements/ContactBalanceSummary/ContactBalanceSummary.ts b/server/src/services/FinancialStatements/ContactBalanceSummary/ContactBalanceSummary.ts index 45912d97f..0d8d84aa1 100644 --- a/server/src/services/FinancialStatements/ContactBalanceSummary/ContactBalanceSummary.ts +++ b/server/src/services/FinancialStatements/ContactBalanceSummary/ContactBalanceSummary.ts @@ -100,7 +100,7 @@ export class ContactBalanceSummaryReport extends FinancialSheet { ): IContactBalanceSummaryAmount { return { amount, - formattedAmount: this.formatNumber(amount), + formattedAmount: this.formatNumber(amount, { money: true }), currencyCode: this.baseCurrency, }; } @@ -113,7 +113,7 @@ export class ContactBalanceSummaryReport extends FinancialSheet { protected getTotalFormat(amount: number): IContactBalanceSummaryAmount { return { amount, - formattedAmount: this.formatNumber(amount), + formattedAmount: this.formatNumber(amount, { money: true }), currencyCode: this.baseCurrency, }; } diff --git a/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummary.ts b/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummary.ts index 185392833..af1142a1c 100644 --- a/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummary.ts +++ b/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummary.ts @@ -1,4 +1,4 @@ -import { sumBy } from 'lodash'; +import { get } from 'lodash'; import * as R from 'ramda'; import { IJournalPoster, @@ -45,11 +45,12 @@ export class CustomerBalanceSummaryReport extends ContactBalanceSummaryReport { * @returns {ICustomerBalanceSummaryCustomer} */ private customerMapper(customer: ICustomer): ICustomerBalanceSummaryCustomer { - const balance = this.receivableLedger.getContactBalance(null, customer.id); + const customerBalance = this.receivableLedger.get(customer.id); + const balanceAmount = get(customerBalance, 'balance', 0); return { customerName: customer.displayName, - total: this.getContactTotalFormat(balance), + total: this.getContactTotalFormat(balanceAmount), }; } diff --git a/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryService.ts b/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryService.ts index 259a66d55..35c46cad8 100644 --- a/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryService.ts +++ b/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryService.ts @@ -1,20 +1,22 @@ import { Inject } from 'typedi'; import moment from 'moment'; +import { map } from 'lodash'; import TenancyService from 'services/Tenancy/TenancyService'; -import Journal from 'services/Accounting/JournalPoster'; +import * as R from 'ramda'; +import { transformToMap } from 'utils'; import { ICustomerBalanceSummaryService, ICustomerBalanceSummaryQuery, ICustomerBalanceSummaryStatement, } from 'interfaces'; import { CustomerBalanceSummaryReport } from './CustomerBalanceSummary'; +import { ACCOUNT_TYPE } from 'data/AccountTypes'; export default class CustomerBalanceSummaryService implements ICustomerBalanceSummaryService { - @Inject() tenancy: TenancyService; - + @Inject('logger') logger: any; @@ -40,61 +42,83 @@ export default class CustomerBalanceSummaryService }; } + customersBalancesQuery(query) { + query.groupBy('contactId'); + query.sum('credit as credit'); + query.sum('debit as debit'); + query.select('contactId'); + } + + /** + * Retrieve the customers credit/debit totals + * @param {number} tenantId + * @returns + */ + async getCustomersCreditDebitTotals(tenantId: number) { + const { AccountTransaction, Account } = this.tenancy.models(tenantId); + + const receivableAccounts = await Account.query().where( + 'accountType', + ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE + ); + const receivableAccountsIds = map(receivableAccounts, 'id'); + + const customersTotals = await AccountTransaction.query().onBuild((query) => { + query.whereIn('accountId', receivableAccountsIds); + this.customersBalancesQuery(query); + }); + + return R.compose( + (customers) => transformToMap(customers, 'contactId'), + (customers) => customers.map((customer) => ({ + ...customer, + balance: customer.debit - customer.credit + })), + )(customersTotals); + } + /** * Retrieve the statment of customer balance summary report. - * @param {number} tenantId - * @param {ICustomerBalanceSummaryQuery} query + * @param {number} tenantId + * @param {ICustomerBalanceSummaryQuery} query * @return {Promise} */ async customerBalanceSummary( tenantId: number, query: ICustomerBalanceSummaryQuery ): Promise { - const { - accountRepository, - transactionsRepository, - } = this.tenancy.repositories(tenantId); - const { Customer } = 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, - 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 + this.logger.info( + '[customer_balance_summary] trying to calculate the report.', + { + filter, + tenantId, + } ); // Retrieve the customers list ordered by the display name. const customers = await Customer.query().orderBy('displayName'); + // Retrieve the customers debit/credit totals. + const customersBalances = await this.getCustomersCreditDebitTotals(tenantId); + // Report instance. const reportInstance = new CustomerBalanceSummaryReport( - transactionsJournal, + customersBalances, customers, filter, - baseCurrency, + baseCurrency ); // Retrieve the report statement. const reportData = reportInstance.reportData(); diff --git a/server/src/services/FinancialStatements/FinancialSheet.ts b/server/src/services/FinancialStatements/FinancialSheet.ts index e9d9af0de..d1a83ad31 100644 --- a/server/src/services/FinancialStatements/FinancialSheet.ts +++ b/server/src/services/FinancialStatements/FinancialSheet.ts @@ -56,10 +56,20 @@ export default class FinancialSheet { }); } - + /** + * Formates the amount to the percentage string. + * @param {number} amount + * @returns {string} + */ protected formatPercentage( amount ): string { - return `%${amount * 100}`; + const percentage = amount * 100; + + return formatNumber(percentage, { + symbol: '%', + excerptZero: true, + money: false, + }) } } diff --git a/server/src/services/FinancialStatements/TransactionsByContact/TransactionsByContact.ts b/server/src/services/FinancialStatements/TransactionsByContact/TransactionsByContact.ts index 0b686ffca..b74305264 100644 --- a/server/src/services/FinancialStatements/TransactionsByContact/TransactionsByContact.ts +++ b/server/src/services/FinancialStatements/TransactionsByContact/TransactionsByContact.ts @@ -1,14 +1,18 @@ -import { sumBy } from 'lodash'; +import { sumBy, defaultTo } from 'lodash'; import { ITransactionsByContactsTransaction, ITransactionsByContactsAmount, - ITransactionsByContacts, + ITransactionsByContactsFilter, IContact, + ILedger, } from 'interfaces'; import FinancialSheet from '../FinancialSheet'; export default class TransactionsByContact extends FinancialSheet { readonly contacts: IContact[]; + readonly ledger: ILedger; + readonly filter: ITransactionsByContactsFilter; + readonly accountsGraph: any; /** * Customer transaction mapper. @@ -18,11 +22,13 @@ export default class TransactionsByContact extends FinancialSheet { protected contactTransactionMapper( transaction ): Omit { + const account = this.accountsGraph.getNodeData(transaction.accountId); const currencyCode = 'USD'; return { credit: this.getContactAmount(transaction.credit, currencyCode), debit: this.getContactAmount(transaction.debit, currencyCode), + accountName: account.name, currencyCode: 'USD', transactionNumber: transaction.transactionNumber, referenceNumber: transaction.referenceNumber, @@ -69,8 +75,8 @@ export default class TransactionsByContact extends FinancialSheet { ): number { const closingBalance = openingBalance; - const totalCredit = sumBy(customerTransactions, 'credit'); - const totalDebit = sumBy(customerTransactions, 'debit'); + const totalCredit = sumBy(customerTransactions, 'credit.amount'); + const totalDebit = sumBy(customerTransactions, 'debit.amount'); return closingBalance + (totalDebit - totalCredit); } @@ -81,7 +87,14 @@ export default class TransactionsByContact extends FinancialSheet { * @returns {number} */ protected getContactOpeningBalance(customerId: number): number { - return 0; + const openingBalanceLedger = this.ledger + .whereContactId(customerId) + .whereToDate(this.filter.fromDate); + + // Retrieve the closing balance of the ledger. + const openingBalance = openingBalanceLedger.getClosingBalance(); + + return defaultTo(openingBalance, 0); } /** @@ -100,4 +113,21 @@ export default class TransactionsByContact extends FinancialSheet { currencyCode, }; } + + /** + * Retrieve the contact total amount format meta. + * @param {number} amount - Amount. + * @param {string} currencyCode - Currency code./ + * @returns {ITransactionsByContactsAmount} + */ + protected getTotalAmountMeta( + amount: number, + currencyCode: string + ) { + return { + amount, + formattedAmount: this.formatTotalNumber(amount, { currencyCode }), + currencyCode, + }; + } } diff --git a/server/src/services/FinancialStatements/TransactionsByContact/TransactionsByContactTableRows.ts b/server/src/services/FinancialStatements/TransactionsByContact/TransactionsByContactTableRows.ts index 481bcee18..c71028838 100644 --- a/server/src/services/FinancialStatements/TransactionsByContact/TransactionsByContactTableRows.ts +++ b/server/src/services/FinancialStatements/TransactionsByContact/TransactionsByContactTableRows.ts @@ -1,4 +1,5 @@ import moment from 'moment'; +import * as R from 'ramda'; import { tableMapper, tableRowMapper } from 'utils'; import { ITransactionsByContactsContact, @@ -28,11 +29,12 @@ export default class TransactionsByContactsTableRows { ): ITableRow[] { const columns = [ { key: 'date', accessor: this.dateAccessor }, - { key: 'account', accessor: 'account.name' }, - { key: 'referenceType', accessor: 'referenceType' }, - { key: 'transactionType', accessor: 'transactionType' }, + { key: 'account', accessor: 'accountName' }, + { key: 'referenceNumber', accessor: 'referenceNumber' }, + { key: 'transactionNumber', accessor: 'transactionNumber' }, { key: 'credit', accessor: 'credit.formattedAmount' }, { key: 'debit', accessor: 'debit.formattedAmount' }, + { key: 'runningBalance', accessor: 'runningBalance.formattedAmount' }, ]; return tableMapper(contact.transactions, columns, { rowTypes: [ROW_TYPE.TRANSACTION], @@ -49,6 +51,7 @@ export default class TransactionsByContactsTableRows { ): ITableRow { const columns = [ { key: 'openingBalanceLabel', value: 'Opening balance' }, + ...R.repeat({ key: 'empty', value: '' }, 5), { key: 'openingBalanceValue', accessor: 'openingBalance.formattedAmount', @@ -69,6 +72,7 @@ export default class TransactionsByContactsTableRows { ): ITableRow { const columns = [ { key: 'closingBalanceLabel', value: 'Closing balance' }, + ...R.repeat({ key: 'empty', value: '' }, 5), { key: 'closingBalanceValue', accessor: 'closingBalance.formattedAmount', diff --git a/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomers.ts b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomers.ts index d107aa544..71b04e21a 100644 --- a/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomers.ts +++ b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomers.ts @@ -11,30 +11,34 @@ import { ICustomer, } from 'interfaces'; import TransactionsByContact from '../TransactionsByContact/TransactionsByContact'; +import Ledger from 'services/Accounting/Ledger'; export default class TransactionsByCustomers extends TransactionsByContact { readonly customers: ICustomer[]; - readonly transactionsByContact: any; + readonly ledger: Ledger; readonly filter: ITransactionsByCustomersFilter; readonly baseCurrency: string; readonly numberFormat: INumberFormatQuery; + readonly accountsGraph: any; /** * Constructor method. * @param {ICustomer} customers - * @param {Map} transactionsByContact + * @param {Map} transactionsLedger * @param {string} baseCurrency */ constructor( customers: ICustomer[], - transactionsByContact: Map, + accountsGraph: any, + ledger: Ledger, filter: ITransactionsByCustomersFilter, baseCurrency: string ) { super(); this.customers = customers; - this.transactionsByContact = transactionsByContact; + this.accountsGraph = accountsGraph; + this.ledger = ledger; this.baseCurrency = baseCurrency; this.filter = filter; this.numberFormat = this.filter.numberFormat; @@ -50,12 +54,17 @@ export default class TransactionsByCustomers extends TransactionsByContact { customerId: number, openingBalance: number ): ITransactionsByCustomersTransaction[] { - const transactions = this.transactionsByContact.get(customerId + '') || []; + const ledger = this.ledger + .whereContactId(customerId) + .whereFromDate(this.filter.fromDate) + .whereToDate(this.filter.toDate); - return R.compose( + const ledgerEntries = ledger.getEntries(); + + return R.compose( R.curry(this.contactTransactionRunningBalance)(openingBalance), R.map(this.contactTransactionMapper.bind(this)) - ).bind(this)(transactions); + ).bind(this)(ledgerEntries); } /** @@ -66,17 +75,17 @@ export default class TransactionsByCustomers extends TransactionsByContact { private customerMapper( customer: ICustomer ): ITransactionsByCustomersCustomer { - const openingBalance = this.getContactOpeningBalance(1); + const openingBalance = this.getContactOpeningBalance(customer.id); const transactions = this.customerTransactions(customer.id, openingBalance); - const closingBalance = this.getContactClosingBalance(transactions, 0); + const closingBalance = this.getContactClosingBalance(transactions, openingBalance); return { customerName: customer.displayName, - openingBalance: this.getContactAmount( + openingBalance: this.getTotalAmountMeta( openingBalance, customer.currencyCode ), - closingBalance: this.getContactAmount( + closingBalance: this.getTotalAmountMeta( closingBalance, customer.currencyCode ), diff --git a/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts index 9c8d354e7..7293eea41 100644 --- a/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts +++ b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts @@ -1,6 +1,7 @@ import { Inject } from 'typedi'; +import * as R from 'ramda'; import moment from 'moment'; -import { groupBy } from 'lodash'; +import { map } from 'lodash'; import TenancyService from 'services/Tenancy/TenancyService'; import { ITransactionsByCustomersService, @@ -8,6 +9,9 @@ import { ITransactionsByCustomersStatement, } from 'interfaces'; import TransactionsByCustomers from './TransactionsByCustomers'; +import Ledger from 'services/Accounting/Ledger'; +import { ACCOUNT_TYPE } from 'data/AccountTypes'; +import AccountRepository from 'repositories/AccountRepository'; export default class TransactionsByCustomersService implements ITransactionsByCustomersService { @@ -23,8 +27,8 @@ export default class TransactionsByCustomersService */ get defaultQuery(): ITransactionsByCustomersFilter { return { - fromDate: moment().format('YYYY-MM-DD'), - toDate: moment().format('YYYY-MM-DD'), + fromDate: moment().startOf('year').format('YYYY-MM-DD'), + toDate: moment().endOf('year').format('YYYY-MM-DD'), numberFormat: { precision: 2, divideOn1000: false, @@ -40,6 +44,80 @@ export default class TransactionsByCustomersService }; } + /** + * Retrieve the accounts receivable. + * @param {number} tenantId + * @returns + */ + async getReceivableAccounts(tenantId: number) { + const { Account } = this.tenancy.models(tenantId); + + const accounts = await Account.query().where( + 'accountType', + ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE + ); + return accounts; + } + + /** + * Retrieve the customers opening balance transactions. + * @param {number} tenantId + * @param {number} openingDate + * @param {number} customersIds + * @returns {} + */ + async getCustomersOpeningBalance( + tenantId: number, + openingDate: Date, + customersIds?: number[] + ): Promise { + const { AccountTransaction } = this.tenancy.models(tenantId); + + const receivableAccounts = await this.getReceivableAccounts(tenantId); + const receivableAccountsIds = map(receivableAccounts, 'id'); + + const openingTransactions = await AccountTransaction.query().modify( + 'contactsOpeningBalance', + openingDate, + receivableAccountsIds, + customersIds + ); + return R.compose( + R.map(R.assoc('date', openingDate)), + R.map(R.assoc('accountNormal', 'debit')) + )(openingTransactions); + } + + /** + * + * @param {number} tenantId + * @param {Date|string} openingDate + * @param {number[]} customersIds + */ + async getCustomersPeriodTransactions( + tenantId: number, + fromDate: Date, + toDate: Date, + ): Promise { + const { AccountTransaction } = this.tenancy.models(tenantId); + + const receivableAccounts = await this.getReceivableAccounts(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', 'debit')))(transactions); + } + /** * Retrieve transactions by by the customers. * @param {number} tenantId @@ -50,8 +128,8 @@ export default class TransactionsByCustomersService tenantId: number, query: ITransactionsByCustomersFilter ): Promise { - const { transactionsRepository } = this.tenancy.repositories(tenantId); const { Customer } = this.tenancy.models(tenantId); + const { accountRepository } = this.tenancy.repositories(tenantId); // Settings tenant service. const settings = this.tenancy.settings(tenantId); @@ -64,21 +142,35 @@ export default class TransactionsByCustomersService ...this.defaultQuery, ...query, }; + + const accountsGraph = await accountRepository.getDependencyGraph(); const customers = await Customer.query().orderBy('displayName'); - // 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')) + const openingBalanceDate = moment(filter.fromDate).subtract(1, 'days').toDate(); + + // Retrieve all ledger transactions of the opening balance of. + const openingBalanceTransactions = await this.getCustomersOpeningBalance( + tenantId, + openingBalanceDate, ); + // Retrieve all ledger transactions between opeing and closing period. + const customersTransactions = await this.getCustomersPeriodTransactions( + tenantId, + query.fromDate, + query.toDate, + ); + // Concats the opening balance and period customer ledger transactions. + const journalTransactions = [ + ...openingBalanceTransactions, + ...customersTransactions, + ]; + const journal = new Ledger(journalTransactions); + // Transactions by customers data mapper. const reportInstance = new TransactionsByCustomers( customers, - transactionsMap, + accountsGraph, + journal, filter, baseCurrency ); diff --git a/server/src/utils/index.ts b/server/src/utils/index.ts index 634340f64..9d8fb0edd 100644 --- a/server/src/utils/index.ts +++ b/server/src/utils/index.ts @@ -242,10 +242,11 @@ const formatNumber = ( decimal = '.', zeroSign = '', money = true, - currencyCode + currencyCode, + symbol } ) => { - const symbol = getCurrencySign(currencyCode); + const formattedSymbol = getCurrencySign(currencyCode); const negForamt = getNegativeFormat(negativeFormat); const format = '%s%v'; @@ -256,7 +257,7 @@ const formatNumber = ( } return accounting.formatMoney( formattedBalance, - money ? symbol : '', + money ? formattedSymbol : symbol ? symbol : '', precision, thousand, decimal,