diff --git a/server/src/api/controllers/FinancialStatements.ts b/server/src/api/controllers/FinancialStatements.ts index 052b7c84c..497b499cc 100644 --- a/server/src/api/controllers/FinancialStatements.ts +++ b/server/src/api/controllers/FinancialStatements.ts @@ -14,6 +14,7 @@ import InventoryValuationController from './FinancialStatements/InventoryValuati import CustomerBalanceSummaryController from './FinancialStatements/CustomerBalanceSummary'; import VendorBalanceSummaryController from './FinancialStatements/VendorBalanceSummary'; import TransactionsByCustomers from './FinancialStatements/TransactionsByCustomers'; +import TransactionsByVendors from './FinancialStatements/TransactionsByVendors'; @Service() export default class FinancialStatementsService { @@ -72,6 +73,10 @@ export default class FinancialStatementsService { '/transactions-by-customers', Container.get(TransactionsByCustomers).router(), ); + router.use( + '/transactions-by-vendors', + Container.get(TransactionsByVendors).router(), + ); return router; } } diff --git a/server/src/api/controllers/FinancialStatements/TransactionsByVendors/index.ts b/server/src/api/controllers/FinancialStatements/TransactionsByVendors/index.ts new file mode 100644 index 000000000..7ede03ee5 --- /dev/null +++ b/server/src/api/controllers/FinancialStatements/TransactionsByVendors/index.ts @@ -0,0 +1,82 @@ +import { Router, Request, Response, NextFunction } from 'express'; +import { query, ValidationChain } from 'express-validator'; +import { Inject } from 'typedi'; +import asyncMiddleware from 'api/middleware/asyncMiddleware'; +import BaseFinancialReportController from '../BaseFinancialReportController'; +import TransactionsByVendorsTableRows from 'services/FinancialStatements/TransactionsByVendor/TransactionsByVendorTableRows'; +import TransactionsByVendorsService from 'services/FinancialStatements/TransactionsByVendor/TransactionsByVendorService'; + +export default class TransactionsByVendorsReportController extends BaseFinancialReportController { + @Inject() + transactionsByVendorsService: TransactionsByVendorsService; + + @Inject() + transactionsByVendorsTableRows: TransactionsByVendorsTableRows; + + /** + * Router constructor. + */ + router() { + const router = Router(); + + router.get( + '/', + this.validationSchema, + asyncMiddleware(this.transactionsByVendors.bind(this)) + ); + return router; + } + + /** + * Validation schema. + */ + get validationSchema(): ValidationChain[] { + return [ + ...this.sheetNumberFormatValidationSchema, + query('from_date').optional().isISO8601(), + query('to_date').optional().isISO8601(), + query('none_zero').optional().isBoolean().toBoolean(), + query('none_transactions').optional().isBoolean().toBoolean(), + ]; + } + + /** + * Retrieve payable aging summary report. + * @param {Request} req - + * @param {Response} res - + * @param {NextFunction} next - + */ + async transactionsByVendors( + req: Request, + res: Response, + next: NextFunction + ) { + const { tenantId } = req; + const filter = this.matchedQueryData(req); + + try { + const { + data, + columns, + query, + } = await this.transactionsByVendorsService.transactionsByVendors( + tenantId, + filter + ); + + return res.status(200).send({ + table: { + rows: this.transactionsByVendorsTableRows.tableRows(data), + }, + }); + + return res.status(200).send({ + data: this.transfromToResponse(data), + columns: this.transfromToResponse(columns), + query: this.transfromToResponse(query), + }); + } catch (error) { + next(error); + } + } +} diff --git a/server/src/interfaces/TransactionsByContacts.ts b/server/src/interfaces/TransactionsByContacts.ts new file mode 100644 index 000000000..dfb02b1e9 --- /dev/null +++ b/server/src/interfaces/TransactionsByContacts.ts @@ -0,0 +1,32 @@ +import { INumberFormatQuery } from './FinancialStatements'; + +export interface ITransactionsByContactsAmount { + amount: number; + formattedAmount: string; + currencyCode: string; +} + +export interface ITransactionsByContactsTransaction { + date: string|Date, + credit: ITransactionsByContactsAmount; + debit: ITransactionsByContactsAmount; + runningBalance: ITransactionsByContactsAmount; + currencyCode: string; + referenceNumber: string; + transactionNumber: string; + createdAt: string|Date, +}; + +export interface ITransactionsByContactsContact { + openingBalance: ITransactionsByContactsAmount, + closingBalance: ITransactionsByContactsAmount, + transactions: ITransactionsByContactsTransaction[], +} + +export interface ITransactionsByContactsFilter { + fromDate: Date; + toDate: Date; + numberFormat: INumberFormatQuery; + noneTransactions: boolean; + noneZero: boolean; +} diff --git a/server/src/interfaces/TransactionsByCustomers.ts b/server/src/interfaces/TransactionsByCustomers.ts index f888b249a..9ccd8de9c 100644 --- a/server/src/interfaces/TransactionsByCustomers.ts +++ b/server/src/interfaces/TransactionsByCustomers.ts @@ -1,36 +1,24 @@ -import { INumberFormatQuery } from './FinancialStatements'; +import { + ITransactionsByContactsAmount, + ITransactionsByContactsTransaction, + ITransactionsByContactsFilter, +} from './TransactionsByContacts'; -export interface ITransactionsByCustomersAmount { - amount: number; - formattedAmount: string; - currencyCode: string; -} +export interface ITransactionsByCustomersAmount + extends ITransactionsByContactsAmount {} -export interface ITransactionsByCustomersTransaction { - date: string|Date, - credit: ITransactionsByCustomersAmount; - debit: ITransactionsByCustomersAmount; - runningBalance: ITransactionsByCustomersAmount; - currencyCode: string; - referenceNumber: string; - transactionNumber: string; - createdAt: string|Date, -}; +export interface ITransactionsByCustomersTransaction + extends ITransactionsByContactsTransaction {} export interface ITransactionsByCustomersCustomer { customerName: string; - openingBalance: any; - closingBalance: any; + openingBalance: ITransactionsByCustomersAmount; + closingBalance: ITransactionsByCustomersAmount; transactions: ITransactionsByCustomersTransaction[]; } -export interface ITransactionsByCustomersFilter { - fromDate: Date; - toDate: Date; - numberFormat: INumberFormatQuery; - noneTransactions: boolean; - noneZero: boolean; -} +export interface ITransactionsByCustomersFilter + extends ITransactionsByContactsFilter {} export type ITransactionsByCustomersData = ITransactionsByCustomersCustomer[]; diff --git a/server/src/interfaces/TransactionsByVendors.ts b/server/src/interfaces/TransactionsByVendors.ts new file mode 100644 index 000000000..c2a4bc77a --- /dev/null +++ b/server/src/interfaces/TransactionsByVendors.ts @@ -0,0 +1,34 @@ +import { + ITransactionsByContactsAmount, + ITransactionsByContactsTransaction, + ITransactionsByContactsFilter, +} from './TransactionsByContacts'; + +export interface ITransactionsByVendorsAmount + extends ITransactionsByContactsAmount {} + +export interface ITransactionsByVendorsTransaction + extends ITransactionsByContactsTransaction {} + +export interface ITransactionsByVendorsVendor { + vendorName: string; + openingBalance: ITransactionsByVendorsAmount; + closingBalance: ITransactionsByVendorsAmount; + transactions: ITransactionsByVendorsTransaction[]; +} + +export interface ITransactionsByVendorsFilter + extends ITransactionsByContactsFilter {} + +export type ITransactionsByVendorsData = ITransactionsByVendorsVendor[]; + +export interface ITransactionsByVendorsStatement { + data: ITransactionsByVendorsData; +} + +export interface ITransactionsByVendorsService { + transactionsByVendors( + tenantId: number, + filter: ITransactionsByVendorsFilter + ): Promise; +} diff --git a/server/src/interfaces/index.ts b/server/src/interfaces/index.ts index f004523bb..89d226413 100644 --- a/server/src/interfaces/index.ts +++ b/server/src/interfaces/index.ts @@ -47,4 +47,6 @@ export * from './CustomerBalanceSummary'; export * from './VendorBalanceSummary'; export * from './ContactBalanceSummary'; export * from './TransactionsByCustomers'; +export * from './TransactionsByContacts'; +export * from './TransactionsByVendors'; export * from './Table'; \ No newline at end of file diff --git a/server/src/services/FinancialStatements/TransactionsByContact/TransactionsByContact.ts b/server/src/services/FinancialStatements/TransactionsByContact/TransactionsByContact.ts index a7378c9a5..0b686ffca 100644 --- a/server/src/services/FinancialStatements/TransactionsByContact/TransactionsByContact.ts +++ b/server/src/services/FinancialStatements/TransactionsByContact/TransactionsByContact.ts @@ -1,7 +1,103 @@ +import { sumBy } from 'lodash'; +import { + ITransactionsByContactsTransaction, + ITransactionsByContactsAmount, + ITransactionsByContacts, + IContact, +} from 'interfaces'; +import FinancialSheet from '../FinancialSheet'; +export default class TransactionsByContact extends FinancialSheet { + readonly contacts: IContact[]; + /** + * Customer transaction mapper. + * @param {any} transaction - + * @return {Omit} + */ + protected contactTransactionMapper( + transaction + ): Omit { + const currencyCode = 'USD'; -export default class TransactionsByContact extends ITransactionsByContacts { + return { + credit: this.getContactAmount(transaction.credit, currencyCode), + debit: this.getContactAmount(transaction.debit, currencyCode), + currencyCode: 'USD', + transactionNumber: transaction.transactionNumber, + referenceNumber: transaction.referenceNumber, + date: transaction.date, + createdAt: transaction.createdAt, + }; + } - -} \ No newline at end of file + /** + * Customer transactions mapper with running balance. + * @param {number} openingBalance + * @param {ITransactionsByContactsTransaction[]} transactions + * @returns {ITransactionsByContactsTransaction[]} + */ + protected contactTransactionRunningBalance( + openingBalance: number, + transactions: Omit[] + ): any { + let _openingBalance = openingBalance; + + return transactions.map( + (transaction: ITransactionsByContactsTransaction) => { + _openingBalance += transaction.debit.amount; + _openingBalance -= transaction.credit.amount; + + const runningBalance = this.getContactAmount( + _openingBalance, + transaction.currencyCode + ); + return { ...transaction, runningBalance }; + } + ); + } + + /** + * Retrieve the customer closing balance from the given transactions and opening balance. + * @param {number} customerTransactions + * @param {number} openingBalance + * @returns {number} + */ + protected getContactClosingBalance( + customerTransactions: ITransactionsByContactsTransaction[], + openingBalance: number + ): number { + const closingBalance = openingBalance; + + const totalCredit = sumBy(customerTransactions, 'credit'); + const totalDebit = sumBy(customerTransactions, 'debit'); + + return closingBalance + (totalDebit - totalCredit); + } + + /** + * Retrieve the given customer opening balance from the given customer id. + * @param {number} customerId + * @returns {number} + */ + protected getContactOpeningBalance(customerId: number): number { + return 0; + } + + /** + * Retrieve the customer amount format meta. + * @param {number} amount + * @param {string} currencyCode + * @returns {ITransactionsByContactsAmount} + */ + protected getContactAmount( + amount: number, + currencyCode: string + ): ITransactionsByContactsAmount { + return { + amount, + formattedAmount: this.formatNumber(amount, { currencyCode }), + currencyCode, + }; + } +} diff --git a/server/src/services/FinancialStatements/TransactionsByContact/TransactionsByContactTableRows.ts b/server/src/services/FinancialStatements/TransactionsByContact/TransactionsByContactTableRows.ts new file mode 100644 index 000000000..053ceba97 --- /dev/null +++ b/server/src/services/FinancialStatements/TransactionsByContact/TransactionsByContactTableRows.ts @@ -0,0 +1,75 @@ +import { tableMapper, tableRowMapper } from 'utils'; +import { + ITransactionsByContactsContact, + ITableRow, +} from 'interfaces'; + +enum ROW_TYPE { + OPENING_BALANCE = 'OPENING_BALANCE', + CLOSING_BALANCE = 'CLOSING_BALANCE', + TRANSACTION = 'TRANSACTION', + CUSTOMER = 'CUSTOMER', +} + +export default class TransactionsByContactsTableRows { + /** + * Retrieve the table rows of contact transactions. + * @param {ITransactionsByCustomersCustomer} contact + * @returns {ITableRow[]} + */ + protected contactTransactions( + contact: ITransactionsByContactsContact + ): ITableRow[] { + const columns = [ + { key: 'date', accessor: 'date' }, + { key: 'account', accessor: 'account.name' }, + { key: 'referenceType', accessor: 'referenceType' }, + { key: 'transactionType', accessor: 'transactionType' }, + { key: 'credit', accessor: 'credit.formattedAmount' }, + { key: 'debit', accessor: 'debit.formattedAmount' }, + ]; + return tableMapper(contact.transactions, columns, { + rowTypes: [ROW_TYPE.TRANSACTION], + }); + } + + /** + * Retrieve the table row of contact opening balance. + * @param {ITransactionsByCustomersCustomer} contact + * @returns {ITableRow} + */ + protected contactOpeningBalance( + contact: ITransactionsByContactsContact + ): ITableRow { + const columns = [ + { key: 'openingBalanceLabel', value: 'Opening balance' }, + { + key: 'openingBalanceValue', + accessor: 'openingBalance.formattedAmount', + }, + ]; + return tableRowMapper(contact, columns, { + rowTypes: [ROW_TYPE.OPENING_BALANCE], + }); + } + + /** + * Retrieve the table row of contact closing balance. + * @param {ITransactionsByCustomersCustomer} contact - + * @returns {ITableRow} + */ + protected contactClosingBalance( + contact: ITransactionsByContactsContact + ): ITableRow { + const columns = [ + { key: 'openingBalanceLabel', value: 'Closing balance' }, + { + key: 'openingBalanceValue', + accessor: 'closingBalance.formattedAmount', + }, + ]; + return tableRowMapper(contact, columns, { + rowTypes: [ROW_TYPE.CLOSING_BALANCE], + }); + } +} diff --git a/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomers.ts b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomers.ts index fad0d198f..d107aa544 100644 --- a/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomers.ts +++ b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomers.ts @@ -1,6 +1,5 @@ import * as R from 'ramda'; import { sumBy } from 'lodash'; -import FinancialSheet from '../FinancialSheet'; import { ITransactionsByCustomersTransaction, ITransactionsByCustomersFilter, @@ -11,8 +10,9 @@ import { IAccountTransaction, ICustomer, } from 'interfaces'; +import TransactionsByContact from '../TransactionsByContact/TransactionsByContact'; -export default class TransactionsByCustomers extends FinancialSheet { +export default class TransactionsByCustomers extends TransactionsByContact { readonly customers: ICustomer[]; readonly transactionsByContact: any; readonly filter: ITransactionsByCustomersFilter; @@ -40,53 +40,6 @@ export default class TransactionsByCustomers extends FinancialSheet { this.numberFormat = this.filter.numberFormat; } - /** - * Customer transaction mapper. - * @param {any} transaction - - * @return {Omit} - */ - private customerTransactionMapper( - transaction - ): Omit { - const currencyCode = 'USD'; - - return { - credit: this.getCustomerAmount(transaction.credit, currencyCode), - debit: this.getCustomerAmount(transaction.debit, currencyCode), - currencyCode: 'USD', - transactionNumber: transaction.transactionNumber, - referenceNumber: transaction.referenceNumber, - date: transaction.date, - createdAt: transaction.createdAt, - }; - } - - /** - * Customer transactions mapper with running balance. - * @param {number} openingBalance - * @param {ITransactionsByCustomersTransaction[]} transactions - * @returns {ITransactionsByCustomersTransaction[]} - */ - private customerTransactionRunningBalance( - openingBalance: number, - transactions: Omit[] - ): any { - let _openingBalance = openingBalance; - - return transactions.map( - (transaction: ITransactionsByCustomersTransaction) => { - _openingBalance += transaction.debit.amount; - _openingBalance -= transaction.credit.amount; - - const runningBalance = this.getCustomerAmount( - _openingBalance, - transaction.currencyCode - ); - return { ...transaction, runningBalance }; - } - ); - } - /** * Retrieve the customer transactions from the given customer id and opening balance. * @param {number} customerId - Customer id. @@ -100,38 +53,11 @@ export default class TransactionsByCustomers extends FinancialSheet { const transactions = this.transactionsByContact.get(customerId + '') || []; return R.compose( - R.curry(this.customerTransactionRunningBalance)(openingBalance), - R.map(this.customerTransactionMapper.bind(this)) + R.curry(this.contactTransactionRunningBalance)(openingBalance), + R.map(this.contactTransactionMapper.bind(this)) ).bind(this)(transactions); } - /** - * Retrieve the customer closing balance from the given transactions and opening balance. - * @param {number} customerTransactions - * @param {number} openingBalance - * @returns {number} - */ - private getCustomerClosingBalance( - customerTransactions: ITransactionsByCustomersTransaction[], - openingBalance: number - ): number { - const closingBalance = openingBalance; - - const totalCredit = sumBy(customerTransactions, 'credit'); - const totalDebit = sumBy(customerTransactions, 'debit'); - - return closingBalance + (totalDebit - totalCredit); - } - - /** - * Retrieve the given customer opening balance from the given customer id. - * @param {number} customerId - * @returns {number} - */ - private getCustomerOpeningBalance(customerId: number): number { - return 0; - } - /** * Customer section mapper. * @param {ICustomer} customer @@ -140,17 +66,17 @@ export default class TransactionsByCustomers extends FinancialSheet { private customerMapper( customer: ICustomer ): ITransactionsByCustomersCustomer { - const openingBalance = this.getCustomerOpeningBalance(1); + const openingBalance = this.getContactOpeningBalance(1); const transactions = this.customerTransactions(customer.id, openingBalance); - const closingBalance = this.getCustomerClosingBalance(transactions, 0); + const closingBalance = this.getContactClosingBalance(transactions, 0); return { customerName: customer.displayName, - openingBalance: this.getCustomerAmount( + openingBalance: this.getContactAmount( openingBalance, customer.currencyCode ), - closingBalance: this.getCustomerAmount( + closingBalance: this.getContactAmount( closingBalance, customer.currencyCode ), @@ -158,23 +84,6 @@ export default class TransactionsByCustomers extends FinancialSheet { }; } - /** - * Retrieve the customer amount format meta. - * @param {number} amount - * @param {string} currencyCode - * @returns {ITransactionsByCustomersAmount} - */ - private getCustomerAmount( - amount: number, - currencyCode: string - ): ITransactionsByCustomersAmount { - return { - amount, - formattedAmount: this.formatNumber(amount, { currencyCode }), - currencyCode, - }; - } - /** * Retrieve the customers sections of the report. * @param {ICustomer[]} customers diff --git a/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersTableRows.ts b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersTableRows.ts index 09b66298e..be779f998 100644 --- a/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersTableRows.ts +++ b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersTableRows.ts @@ -1,6 +1,7 @@ import * as R from 'ramda'; import { tableRowMapper, tableMapper } from 'utils'; import { ITransactionsByCustomersCustomer, ITableRow } from 'interfaces'; +import TransactionsByContactsTableRows from '../TransactionsByContact/TransactionsByContactTableRows'; enum ROW_TYPE { OPENING_BALANCE = 'OPENING_BALANCE', @@ -9,102 +10,37 @@ enum ROW_TYPE { CUSTOMER = 'CUSTOMER', } -export default class TransactionsByCustomersTableRows { - /** - * Retrieve the table rows of customer transactions. - * @param {ITransactionsByCustomersCustomer} customer - * @returns {ITableRow[]} - */ - private customerTransactions( - customer: ITransactionsByCustomersCustomer - ): ITableRow[] { - const columns = [ - { key: 'date', accessor: 'date' }, - { key: 'account', accessor: 'account.name' }, - { key: 'referenceType', accessor: 'referenceType' }, - { key: 'transactionType', accessor: 'transactionType' }, - { key: 'credit', accessor: 'credit.formattedAmount' }, - { key: 'debit', accessor: 'debit.formattedAmount' }, - ]; - return tableMapper(customer.transactions, columns, { - rowTypes: [ROW_TYPE.TRANSACTION] - }); - } - - /** - * Retrieve the table row of customer opening balance. - * @param {ITransactionsByCustomersCustomer} customer - * @returns {ITableRow} - */ - private customerOpeningBalance( - customer: ITransactionsByCustomersCustomer - ): ITableRow { - const columns = [ - { key: 'openingBalanceLabel', value: 'Opening balance' }, - { - key: 'openingBalanceValue', - accessor: 'openingBalance.formattedAmount', - }, - ]; - return tableRowMapper(customer, columns, { - rowTypes: [ROW_TYPE.OPENING_BALANCE] - }); - } - - /** - * Retrieve the table row of customer closing balance. - * @param {ITransactionsByCustomersCustomer} customer - - * @returns {ITableRow} - */ - private customerClosingBalance( - customer: ITransactionsByCustomersCustomer - ): ITableRow { - const columns = [ - { key: 'openingBalanceLabel', value: 'Closing balance' }, - { - key: 'openingBalanceValue', - accessor: 'closingBalance.formattedAmount', - }, - ]; - return tableRowMapper(customer, columns, { - rowTypes: [ROW_TYPE.CLOSING_BALANCE] - }); - } - +export default class TransactionsByCustomersTableRows extends TransactionsByContactsTableRows { /** * Retrieve the table row of customer details. - * @param {ITransactionsByCustomersCustomer} customer - + * @param {ITransactionsByCustomersCustomer} customer - * @returns {ITableRow[]} */ - private customerDetails( - customer: ITransactionsByCustomersCustomer - ) { + private customerDetails(customer: ITransactionsByCustomersCustomer) { const columns = [{ key: 'customerName', accessor: 'customerName' }]; return { ...tableRowMapper(customer, columns, { rowTypes: [ROW_TYPE.CUSTOMER] }), children: R.pipe( - R.append(this.customerOpeningBalance(customer)), - R.concat(this.customerTransactions(customer)), - R.append(this.customerClosingBalance(customer)), + R.append(this.contactOpeningBalance(customer)), + R.concat(this.contactTransactions(customer)), + R.append(this.contactClosingBalance(customer)) )([]), - } + }; } /** * Retrieve the table rows of the customer section. - * @param {ITransactionsByCustomersCustomer} customer + * @param {ITransactionsByCustomersCustomer} customer * @returns {ITableRow[]} */ private customerRowsMapper(customer: ITransactionsByCustomersCustomer) { - return R.pipe( - R.append(this.customerDetails(customer)), - ).bind(this)([]); + return R.pipe(R.append(this.customerDetails(customer))).bind(this)([]); } /** * Retrieve the table rows of transactions by customers report. - * @param {ITransactionsByCustomersCustomer[]} customers + * @param {ITransactionsByCustomersCustomer[]} customers * @returns {ITableRow[]} */ public tableRows(customers: ITransactionsByCustomersCustomer[]): ITableRow[] { diff --git a/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendor.ts b/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendor.ts new file mode 100644 index 000000000..41c200964 --- /dev/null +++ b/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendor.ts @@ -0,0 +1,114 @@ +import * as R from 'ramda'; +import { sumBy } from 'lodash'; +import { + ITransactionsByVendorsFilter, + ITransactionsByVendorsTransaction, + ITransactionsByVendorsVendor, + ITransactionsByVendorsAmount, + ITransactionsByVendorsData, + IAccountTransaction, + INumberFormatQuery, + IVendor +} from 'interfaces'; +import TransactionsByContact from "../TransactionsByContact/TransactionsByContact"; + +export default class TransactionsByVendors extends TransactionsByContact{ + readonly contacts: IVendor[]; + readonly transactionsByContact: any; + readonly filter: ITransactionsByVendorsFilter; + readonly baseCurrency: string; + readonly numberFormat: INumberFormatQuery; + + /** + * Constructor method. + * @param {IVendor} vendors + * @param {Map} transactionsByContact + * @param {string} baseCurrency + */ + constructor( + vendors: IVendor[], + transactionsByContact: Map, + filter: ITransactionsByVendorsFilter, + baseCurrency: string + ) { + super(); + + this.contacts = vendors; + this.transactionsByContact = transactionsByContact; + this.baseCurrency = baseCurrency; + this.filter = filter; + this.numberFormat = this.filter.numberFormat; + } + + /** + * Retrieve the vendor transactions from the given vendor id and opening balance. + * @param {number} vendorId - Vendor id. + * @param {number} openingBalance - Opening balance amount. + * @returns {ITransactionsByVendorsTransaction[]} + */ + private vendorTransactions( + vendorId: number, + openingBalance: number + ): ITransactionsByVendorsTransaction[] { + const transactions = this.transactionsByContact.get(vendorId + '') || []; + + return R.compose( + R.curry(this.contactTransactionRunningBalance)(openingBalance), + R.map(this.contactTransactionMapper.bind(this)) + ).bind(this)(transactions); + } + + /** + * Vendor section mapper. + * @param {IVendor} vendor + * @returns {ITransactionsByVendorsVendor} + */ + private vendorMapper( + vendor: IVendor + ): ITransactionsByVendorsVendor { + const openingBalance = this.getContactOpeningBalance(1); + const transactions = this.vendorTransactions(vendor.id, openingBalance); + const closingBalance = this.getContactClosingBalance(transactions, 0); + + return { + vendorName: vendor.displayName, + openingBalance: this.getContactAmount( + openingBalance, + vendor.currencyCode + ), + closingBalance: this.getContactAmount( + closingBalance, + vendor.currencyCode + ), + transactions, + }; + } + + /** + * Retrieve the vendors sections of the report. + * @param {IVendor[]} vendors + * @returns {ITransactionsByVendorsVendor[]} + */ + private vendorsMapper( + vendors: IVendor[] + ): ITransactionsByVendorsVendor[] { + return R.compose(R.map(this.vendorMapper.bind(this))).bind(this)( + vendors + ); + } + + /** + * Retrieve the report data. + * @returns {ITransactionsByVendorsData} + */ + public reportData(): ITransactionsByVendorsData { + return this.vendorsMapper(this.contacts); + } + + /** + * Retrieve the report columns. + */ + public reportColumns() { + return []; + } +} \ No newline at end of file diff --git a/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorService.ts b/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorService.ts new file mode 100644 index 000000000..7993c94c3 --- /dev/null +++ b/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorService.ts @@ -0,0 +1,96 @@ +import { Inject } from 'typedi'; +import moment from 'moment'; +import { groupBy } from 'lodash'; +import TenancyService from 'services/Tenancy/TenancyService'; +import { + ITransactionsByVendorsService, + ITransactionsByVendorsFilter, + ITransactionsByVendorsStatement, +} from 'interfaces'; +import TransactionsByVendor from './TransactionsByVendor'; + +export default class TransactionsByVendorsService + implements ITransactionsByVendorsService { + @Inject() + tenancy: TenancyService; + + @Inject('logger') + logger: any; + + /** + * Defaults balance sheet filter query. + * @return {IVendorBalanceSummaryQuery} + */ + get defaultQuery(): ITransactionsByVendorsFilter { + return { + fromDate: moment().format('YYYY-MM-DD'), + toDate: moment().format('YYYY-MM-DD'), + numberFormat: { + precision: 2, + divideOn1000: false, + showZero: false, + formatMoney: 'total', + negativeFormat: 'mines', + }, + comparison: { + percentageOfColumn: true, + }, + noneZero: false, + noneTransactions: false, + }; + } + + /** + * Retrieve transactions by by the customers. + * @param {number} tenantId + * @param {ITransactionsByVendorsFilter} query + * @return {Promise} + */ + public async transactionsByVendors( + tenantId: number, + query: ITransactionsByVendorsFilter + ): Promise { + const { 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', + }); + + const filter = { + ...this.defaultQuery, + ...query, + }; + const vendors = await Vendor.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')) + ); + // Transactions by customers data mapper. + const reportInstance = new TransactionsByVendor( + vendors, + transactionsMap, + filter, + baseCurrency + ); + // Retrieve the report data. + const reportData = reportInstance.reportData(); + + // Retireve the report columns. + const reportColumns = reportInstance.reportColumns(); + + return { + data: reportData, + columns: reportColumns, + }; + } +} diff --git a/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorTableRows.ts b/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorTableRows.ts new file mode 100644 index 000000000..d0c1f9877 --- /dev/null +++ b/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorTableRows.ts @@ -0,0 +1,49 @@ +import * as R from 'ramda'; +import { tableRowMapper } from 'utils'; +import { ITransactionsByVendorsVendor, ITableRow } from 'interfaces'; +import TransactionsByContactsTableRows from '../TransactionsByContact/TransactionsByContactTableRows'; + +enum ROW_TYPE { + OPENING_BALANCE = 'OPENING_BALANCE', + CLOSING_BALANCE = 'CLOSING_BALANCE', + TRANSACTION = 'TRANSACTION', + VENDOR = 'VENDOR', +} + +export default class TransactionsByVendorsTableRows extends TransactionsByContactsTableRows { + /** + * Retrieve the table row of vendor details. + * @param {ITransactionsByVendorsVendor} vendor - + * @returns {ITableRow[]} + */ + private vendorDetails(vendor: ITransactionsByVendorsVendor) { + const columns = [{ key: 'vendorName', accessor: 'vendorName' }]; + + return { + ...tableRowMapper(vendor, columns, { rowTypes: [ROW_TYPE.VENDOR] }), + children: R.pipe( + R.append(this.contactOpeningBalance(vendor)), + R.concat(this.contactTransactions(vendor)), + R.append(this.contactClosingBalance(vendor)) + )([]), + }; + } + + /** + * Retrieve the table rows of the vendor section. + * @param {ITransactionsByVendorsVendor} vendor + * @returns {ITableRow[]} + */ + private vendorRowsMapper(vendor: ITransactionsByVendorsVendor) { + return R.pipe(R.append(this.vendorDetails(vendor))).bind(this)([]); + } + + /** + * Retrieve the table rows of transactions by vendors report. + * @param {ITransactionsByVendorsVendor[]} vendors + * @returns {ITableRow[]} + */ + public tableRows(vendors: ITransactionsByVendorsVendor[]): ITableRow[] { + return R.map(this.vendorRowsMapper.bind(this))(vendors); + } +}