diff --git a/packages/server-nest/src/modules/Accounts/models/AccountTransaction.model.ts b/packages/server-nest/src/modules/Accounts/models/AccountTransaction.model.ts index 141f2d790..3ec4b2994 100644 --- a/packages/server-nest/src/modules/Accounts/models/AccountTransaction.model.ts +++ b/packages/server-nest/src/modules/Accounts/models/AccountTransaction.model.ts @@ -1,5 +1,6 @@ import { Model, raw } from 'objection'; -import moment, { unitOfTime } from 'moment'; +import * as moment from 'moment'; +import { unitOfTime } from 'moment'; import { isEmpty, castArray } from 'lodash'; import { BaseModel } from '@/models/Model'; import { Account } from './Account.model'; @@ -10,6 +11,7 @@ export class AccountTransaction extends BaseModel { public readonly referenceId: number; public readonly accountId: number; public readonly contactId: number; + public readonly contactType: string; public readonly credit: number; public readonly debit: number; public readonly exchangeRate: number; diff --git a/packages/server-nest/src/modules/FinancialStatements/FinancialStatements.module.ts b/packages/server-nest/src/modules/FinancialStatements/FinancialStatements.module.ts index b035da052..c4d2ed713 100644 --- a/packages/server-nest/src/modules/FinancialStatements/FinancialStatements.module.ts +++ b/packages/server-nest/src/modules/FinancialStatements/FinancialStatements.module.ts @@ -3,6 +3,10 @@ import { PurchasesByItemsModule } from './modules/PurchasesByItems/PurchasesByIt import { CustomerBalanceSummaryModule } from './modules/CustomerBalanceSummary/CustomerBalanceSummary.module'; import { SalesByItemsModule } from './modules/SalesByItems/SalesByItems.module'; import { GeneralLedgerModule } from './modules/GeneralLedger/GeneralLedger.module'; +import { TransactionsByCustomerModule } from './modules/TransactionsByCustomer/TransactionsByCustomer.module'; +import { TrialBalanceSheetModule } from './modules/TrialBalanceSheet/TrialBalanceSheet.module'; +import { TransactionsByReferenceModule } from './modules/TransactionsByReference/TransactionByReference.module'; +import { TransactionsByVendorModule } from './modules/TransactionsByVendor/TransactionsByVendor.module'; // @Module({ providers: [], @@ -10,7 +14,14 @@ import { GeneralLedgerModule } from './modules/GeneralLedger/GeneralLedger.modul PurchasesByItemsModule, CustomerBalanceSummaryModule, SalesByItemsModule, - GeneralLedgerModule + GeneralLedgerModule, + TrialBalanceSheetModule, + TransactionsByCustomerModule, + TransactionsByVendorModule, + // TransactionsByReferenceModule, + // TransactionsByVendorModule, + // TransactionsByContactModule, ], }) export class FinancialStatementsModule {} + diff --git a/packages/server-nest/src/modules/FinancialStatements/common/FinancialSheet.ts b/packages/server-nest/src/modules/FinancialStatements/common/FinancialSheet.ts index 1da16b0c9..7661eef92 100644 --- a/packages/server-nest/src/modules/FinancialStatements/common/FinancialSheet.ts +++ b/packages/server-nest/src/modules/FinancialStatements/common/FinancialSheet.ts @@ -141,7 +141,7 @@ export class FinancialSheet { * @param {string} format * @returns */ - protected getDateMeta(date: Date, format = 'YYYY-MM-DD') { + protected getDateMeta(date: moment.MomentInput, format = 'YYYY-MM-DD') { return { formattedDate: moment(date).format(format), date: moment(date).toDate(), diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContact.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContact.ts new file mode 100644 index 000000000..88f8390b3 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContact.ts @@ -0,0 +1,194 @@ +import { sumBy, defaultTo } from 'lodash'; +import { + ITransactionsByContactsTransaction, + ITransactionsByContactsAmount, + ITransactionsByContactsFilter, + ITransactionsByContactsContact, +} from './TransactionsByContact.types'; +import { FinancialSheet } from '../../common/FinancialSheet'; +import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types'; +import { I18nService } from 'nestjs-i18n'; +import { allPassedConditionsPass } from '@/utils/all-conditions-passed'; +import { TransactionsByContactRepository } from './TransactionsByContactRepository'; + +export class TransactionsByContact extends FinancialSheet { + public readonly filter: ITransactionsByContactsFilter; + public readonly i18n: I18nService + public readonly repository: TransactionsByContactRepository; + + /** + * Customer transaction mapper. + * @param {ILedgerEntry} entry - Ledger entry. + * @return {Omit} + */ + protected contactTransactionMapper( + entry: ILedgerEntry + ): Omit { + const account = this.repository.accountsGraph.getNodeData(entry.accountId); + const currencyCode = this.baseCurrency; + + return { + credit: this.getContactAmount(entry.credit, currencyCode), + debit: this.getContactAmount(entry.debit, currencyCode), + accountName: account.name, + currencyCode: this.baseCurrency, + transactionNumber: entry.transactionNumber, + transactionType: this.i18n.t(entry.referenceTypeFormatted), + date: entry.date, + createdAt: entry.createdAt, + }; + } + + /** + * Customer transactions mapper with running balance. + * @param {number} openingBalance + * @param {ITransactionsByContactsTransaction[]} transactions + * @returns {ITransactionsByContactsTransaction[]} + */ + protected contactTransactionRunningBalance( + openingBalance: number, + accountNormal: 'credit' | 'debit', + transactions: Omit[] + ): any { + let _openingBalance = openingBalance; + + return transactions.map( + (transaction: ITransactionsByContactsTransaction) => { + _openingBalance += + accountNormal === 'debit' + ? transaction.debit.amount + : -1 * transaction.debit.amount; + + _openingBalance += + accountNormal === 'credit' + ? transaction.credit.amount + : -1 * transaction.credit.amount; + + const runningBalance = this.getTotalAmountMeta( + _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[], + contactNormal: 'credit' | 'debit', + openingBalance: number + ): number { + const closingBalance = openingBalance; + + const totalCredit = sumBy(customerTransactions, 'credit.amount'); + const totalDebit = sumBy(customerTransactions, 'debit.amount'); + + const total = + contactNormal === 'debit' + ? totalDebit - totalCredit + : totalCredit - totalDebit; + + return closingBalance + total; + } + + /** + * Retrieve the given customer opening balance from the given customer id. + * @param {number} customerId + * @returns {number} + */ + protected getContactOpeningBalance(customerId: number): number { + const openingBalanceLedger = this.repository.ledger + .whereContactId(customerId) + .whereToDate(this.filter.fromDate); + + // Retrieve the closing balance of the ledger. + const openingBalance = openingBalanceLedger.getClosingBalance(); + + return defaultTo(openingBalance, 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, + }; + } + + /** + * 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, + }; + } + + /** + * Filter customer section that has no transactions. + * @param {ITransactionsByCustomersCustomer} transactionsByCustomer + * @returns {boolean} + */ + private filterContactByNoneTransaction = ( + transactionsByContact: ITransactionsByContactsContact + ): boolean => { + return transactionsByContact.transactions.length > 0; + }; + + /** + * Filters customer section has zero closing balnace. + * @param {ITransactionsByCustomersCustomer} transactionsByCustomer + * @returns {boolean} + */ + private filterContactNoneZero = ( + transactionsByContact: ITransactionsByContactsContact + ): boolean => { + return transactionsByContact.closingBalance.amount !== 0; + }; + + /** + * Filters the given customer node; + * @param {ITransactionsByContactsContact} node - Contact node. + */ + private contactNodeFilter = (node: ITransactionsByContactsContact) => { + const { noneTransactions, noneZero } = this.filter; + + // Conditions pair filter detarminer. + const condsPairFilters = [ + [noneTransactions, this.filterContactByNoneTransaction], + [noneZero, this.filterContactNoneZero], + ]; + return allPassedConditionsPass(condsPairFilters)(node); + }; + + /** + * Filters the given customers nodes. + * @param {ICustomerBalanceSummaryCustomer[]} nodes + * @returns {ICustomerBalanceSummaryCustomer[]} + */ + protected contactsFilter = ( + nodes: ITransactionsByContactsContact[] + ): ITransactionsByContactsContact[] => { + return nodes.filter(this.contactNodeFilter); + }; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContact.types.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContact.types.ts new file mode 100644 index 000000000..957bc2828 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContact.types.ts @@ -0,0 +1,34 @@ +import { INumberFormatQuery } from "../../types/Report.types"; + + +export interface ITransactionsByContactsAmount { + amount: number; + formattedAmount: string; + currencyCode: string; +} + +export interface ITransactionsByContactsTransaction { + date: string|Date, + credit: ITransactionsByContactsAmount; + debit: ITransactionsByContactsAmount; + accountName: string, + runningBalance: ITransactionsByContactsAmount; + currencyCode: string; + transactionType: string; + transactionNumber: string; + createdAt: string|Date, +}; + +export interface ITransactionsByContactsContact { + openingBalance: ITransactionsByContactsAmount, + closingBalance: ITransactionsByContactsAmount, + transactions: ITransactionsByContactsTransaction[], +} + +export interface ITransactionsByContactsFilter { + fromDate: Date|string; + toDate: Date|string; + numberFormat: INumberFormatQuery; + noneTransactions: boolean; + noneZero: boolean; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContactRepository.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContactRepository.ts new file mode 100644 index 000000000..d4c490610 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContactRepository.ts @@ -0,0 +1,27 @@ +import { Ledger } from '@/modules/Ledger/Ledger'; +import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types'; + +export class TransactionsByContactRepository { + /** + * Base currency. + * @param {string} baseCurrency + */ + public baseCurrency: string; + + /** + * Report data. + */ + public accountsGraph: any; + + /** + * Report data. + * @param {Ledger} ledger + */ + public ledger: Ledger; + + /** + * Opening balance entries. + * @param {ILedgerEntry[]} openingBalanceEntries + */ + public openingBalanceEntries: ILedgerEntry[]; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContactTableRows.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContactTableRows.ts new file mode 100644 index 000000000..c4dfe80df --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByContact/TransactionsByContactTableRows.ts @@ -0,0 +1,91 @@ +import moment from 'moment'; +import * as R from 'ramda'; +import { ITransactionsByContactsContact } from './TransactionsByContact.types'; +import { ITableRow } from '../../types/Table.types'; +import { tableMapper, tableRowMapper } from '../../utils/Table.utils'; +import { I18nService } from 'nestjs-i18n'; + +enum ROW_TYPE { + OPENING_BALANCE = 'OPENING_BALANCE', + CLOSING_BALANCE = 'CLOSING_BALANCE', + TRANSACTION = 'TRANSACTION', + CUSTOMER = 'CUSTOMER', +} + +export class TransactionsByContactsTableRows { + public i18n: I18nService; + + public dateAccessor = (value): string => { + return moment(value.date).format('YYYY MMM DD'); + }; + + /** + * Retrieve the table rows of contact transactions. + * @param {ITransactionsByCustomersCustomer} contact + * @returns {ITableRow[]} + */ + public contactTransactions = ( + contact: ITransactionsByContactsContact, + ): ITableRow[] => { + const columns = [ + { key: 'date', accessor: this.dateAccessor }, + { key: 'account', accessor: 'accountName' }, + { key: 'transactionType', accessor: 'transactionType' }, + { 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], + }); + }; + + /** + * Retrieve the table row of contact opening balance. + * @param {ITransactionsByCustomersCustomer} contact + * @returns {ITableRow} + */ + public contactOpeningBalance = ( + contact: ITransactionsByContactsContact, + ): ITableRow => { + const columns = [ + { + key: 'openingBalanceLabel', + value: this.i18n.t('Opening balance') as string, + }, + ...R.repeat({ key: 'empty', value: '' }, 5), + { + 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} + */ + public contactClosingBalance = ( + contact: ITransactionsByContactsContact, + ): ITableRow => { + const columns = [ + { + key: 'closingBalanceLabel', + value: this.i18n.t('Closing balance') as string, + }, + ...R.repeat({ key: 'empty', value: '' }, 5), + { + key: 'closingBalanceValue', + accessor: 'closingBalance.formattedAmount', + }, + ]; + return tableRowMapper(contact, columns, { + rowTypes: [ROW_TYPE.CLOSING_BALANCE], + }); + }; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomer.module.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomer.module.ts new file mode 100644 index 000000000..cb5534a0a --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomer.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { TransactionsByCustomersExportInjectable } from './TransactionsByCustomersExportInjectable'; +import { TransactionsByCustomersPdf } from './TransactionsByCustomersPdf'; +import { TransactionsByCustomersRepository } from './TransactionsByCustomersRepository'; +import { TransactionsByCustomersSheet } from './TransactionsByCustomersService'; +import { TransactionsByCustomersTableInjectable } from './TransactionsByCustomersTableInjectable'; +import { TransactionsByCustomersMeta } from './TransactionsByCustomersMeta'; +import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; +import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module'; +import { AccountsModule } from '@/modules/Accounts/Accounts.module'; + +@Module({ + imports: [FinancialSheetCommonModule, AccountsModule], + providers: [ + TransactionsByCustomersRepository, + TransactionsByCustomersTableInjectable, + TransactionsByCustomersExportInjectable, + TransactionsByCustomersSheet, + TransactionsByCustomersPdf, + TransactionsByCustomersMeta, + TenancyContext + ], + controllers: [], +}) +export class TransactionsByCustomerModule {} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomer.types.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomer.types.ts new file mode 100644 index 000000000..ccaa3616f --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomer.types.ts @@ -0,0 +1,52 @@ + +import { IFinancialSheetCommonMeta } from '../../types/Report.types'; +import { IFinancialTable } from '../../types/Table.types'; +import { + ITransactionsByContactsAmount, + ITransactionsByContactsTransaction, + ITransactionsByContactsFilter, +} from '../TransactionsByContact/TransactionsByContact.types'; + +export interface ITransactionsByCustomersAmount + extends ITransactionsByContactsAmount {} + +export interface ITransactionsByCustomersTransaction + extends ITransactionsByContactsTransaction {} + +export interface ITransactionsByCustomersCustomer { + customerName: string; + openingBalance: ITransactionsByCustomersAmount; + closingBalance: ITransactionsByCustomersAmount; + transactions: ITransactionsByCustomersTransaction[]; +} + +export interface ITransactionsByCustomersFilter + extends ITransactionsByContactsFilter { + customersIds: number[]; +} + +export type ITransactionsByCustomersData = ITransactionsByCustomersCustomer[]; + +export interface ITransactionsByCustomersStatement { + data: ITransactionsByCustomersData; + query: ITransactionsByCustomersFilter; + meta: ITransactionsByCustomersMeta; +} + +export interface ITransactionsByCustomersTable extends IFinancialTable { + query: ITransactionsByCustomersFilter; + meta: ITransactionsByCustomersMeta; +} + +export interface ITransactionsByCustomersService { + transactionsByCustomers( + tenantId: number, + filter: ITransactionsByCustomersFilter + ): Promise; +} +export interface ITransactionsByCustomersMeta + extends IFinancialSheetCommonMeta { + formattedFromDate: string; + formattedToDate: string; + formattedDateRange: string; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomers.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomers.ts new file mode 100644 index 000000000..f85992b7f --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomers.ts @@ -0,0 +1,136 @@ +import * as R from 'ramda'; +import { isEmpty } from 'lodash'; +import { I18nService } from 'nestjs-i18n'; +import { ModelObject } from 'objection'; +import { + ITransactionsByCustomersTransaction, + ITransactionsByCustomersFilter, + ITransactionsByCustomersCustomer, + ITransactionsByCustomersData, +} from './TransactionsByCustomer.types'; +import { TransactionsByContact } from '../TransactionsByContact/TransactionsByContact'; +import { Customer } from '@/modules/Customers/models/Customer'; +import { INumberFormatQuery } from '../../types/Report.types'; +import { TransactionsByCustomersRepository } from './TransactionsByCustomersRepository'; + +const CUSTOMER_NORMAL = 'debit'; + +export class TransactionsByCustomers extends TransactionsByContact { + readonly filter: ITransactionsByCustomersFilter; + readonly numberFormat: INumberFormatQuery; + readonly repository: TransactionsByCustomersRepository; + readonly i18n: I18nService; + + /** + * Constructor method. + * @param {ICustomer} customers + * @param {Map} transactionsLedger + * @param {string} baseCurrency + */ + constructor( + filter: ITransactionsByCustomersFilter, + transactionsByCustomersRepository: TransactionsByCustomersRepository, + i18n: I18nService + ) { + super(); + + this.filter = filter; + this.repository = transactionsByCustomersRepository; + this.numberFormat = this.filter.numberFormat; + this.i18n = i18n; + } + + /** + * Retrieve the customer transactions from the given customer id and opening balance. + * @param {number} customerId - Customer id. + * @param {number} openingBalance - Opening balance amount. + * @returns {ITransactionsByCustomersTransaction[]} + */ + private customerTransactions( + customerId: number, + openingBalance: number + ): ITransactionsByCustomersTransaction[] { + const ledger = this.repository.ledger + .whereContactId(customerId) + .whereFromDate(this.filter.fromDate) + .whereToDate(this.filter.toDate); + + const ledgerEntries = ledger.getEntries(); + + return R.compose( + R.curry(this.contactTransactionRunningBalance)(openingBalance, 'debit'), + R.map(this.contactTransactionMapper.bind(this)) + ).bind(this)(ledgerEntries); + } + + /** + * Customer section mapper. + * @param {ModelObject} customer + * @returns {ITransactionsByCustomersCustomer} + */ + private customerMapper( + customer: ModelObject + ): ITransactionsByCustomersCustomer { + const openingBalance = this.getContactOpeningBalance(customer.id); + const transactions = this.customerTransactions(customer.id, openingBalance); + const closingBalance = this.getCustomerClosingBalance( + transactions, + openingBalance + ); + const currencyCode = this.baseCurrency; + + return { + customerName: customer.displayName, + openingBalance: this.getTotalAmountMeta(openingBalance, currencyCode), + closingBalance: this.getTotalAmountMeta(closingBalance, currencyCode), + transactions, + }; + } + + /** + * Retrieve the vendor closing balance from the given customer transactions. + * @param {ITransactionsByContactsTransaction[]} customerTransactions + * @param {number} openingBalance + * @returns + */ + private getCustomerClosingBalance( + customerTransactions: ITransactionsByCustomersTransaction[], + openingBalance: number + ): number { + return this.getContactClosingBalance( + customerTransactions, + CUSTOMER_NORMAL, + openingBalance + ); + } + + /** + * Detarmines whether the customers post filter is active. + * @returns {boolean} + */ + private isCustomersPostFilter = () => { + return isEmpty(this.filter.customersIds); + }; + + /** + * Retrieve the customers sections of the report. + * @param {ICustomer[]} customers + * @returns {ITransactionsByCustomersCustomer[]} + */ + private customersMapper( + customers: ModelObject[] + ): ITransactionsByCustomersCustomer[] { + return R.compose( + R.when(this.isCustomersPostFilter, this.contactsFilter), + R.map(this.customerMapper.bind(this)) + ).bind(this)(customers); + } + + /** + * Retrieve the report data. + * @returns {ITransactionsByCustomersData} + */ + public reportData(): ITransactionsByCustomersData { + return this.customersMapper(this.repository.customers); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersApplication.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersApplication.ts new file mode 100644 index 000000000..b26f9146e --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersApplication.ts @@ -0,0 +1,66 @@ +import { + ITransactionsByCustomersFilter, + ITransactionsByCustomersStatement, +} from './TransactionsByCustomer.types'; +import { TransactionsByCustomersTableInjectable } from './TransactionsByCustomersTableInjectable'; +import { TransactionsByCustomersExportInjectable } from './TransactionsByCustomersExportInjectable'; +import { TransactionsByCustomersSheet } from './TransactionsByCustomersService'; +import { TransactionsByCustomersPdf } from './TransactionsByCustomersPdf'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class TransactionsByCustomerApplication { + constructor( + private readonly transactionsByCustomersTable: TransactionsByCustomersTableInjectable, + private readonly transactionsByCustomersExport: TransactionsByCustomersExportInjectable, + private readonly transactionsByCustomersSheet: TransactionsByCustomersSheet, + private readonly transactionsByCustomersPdf: TransactionsByCustomersPdf, + ) {} + /** + * Retrieves the transactions by customers sheet in json format. + * @param {ITransactionsByCustomersFilter} query - Transactions by customers filter. + * @returns {Promise} + */ + public sheet( + query: ITransactionsByCustomersFilter, + ): Promise { + return this.transactionsByCustomersSheet.transactionsByCustomers(query); + } + + /** + * Retrieves the transactions by vendors sheet in table format. + * @param {ITransactionsByCustomersFilter} query - Transactions by customers filter. + * @returns {Promise} + */ + public table(query: ITransactionsByCustomersFilter) { + return this.transactionsByCustomersTable.table(query); + } + + /** + * Retrieves the transactions by vendors sheet in CSV format. + * @param {ITransactionsByCustomersFilter} query - Transactions by customers filter. + * @returns {Promise} + */ + public csv(query: ITransactionsByCustomersFilter): Promise { + return this.transactionsByCustomersExport.csv(query); + } + + /** + * Retrieves the transactions by vendors sheet in XLSX format. + * @param {number} tenantId + * @param {ITransactionsByCustomersFilter} query + * @returns {Promise} + */ + public xlsx(query: ITransactionsByCustomersFilter): Promise { + return this.transactionsByCustomersExport.xlsx(query); + } + + /** + * Retrieves the transactions by vendors sheet in PDF format. + * @param {ITransactionsByCustomersFilter} query - Transactions by customers filter. + * @returns {Promise} + */ + public pdf(query: ITransactionsByCustomersFilter): Promise { + return this.transactionsByCustomersPdf.pdf(query); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersExportInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersExportInjectable.ts new file mode 100644 index 000000000..36de2ff38 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersExportInjectable.ts @@ -0,0 +1,39 @@ +import { TableSheet } from '../../common/TableSheet'; +import { ITransactionsByCustomersFilter } from './TransactionsByCustomer.types'; +import { TransactionsByCustomersTableInjectable } from './TransactionsByCustomersTableInjectable'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class TransactionsByCustomersExportInjectable { + constructor( + private readonly transactionsByCustomerTable: TransactionsByCustomersTableInjectable, + ) {} + + /** + * Retrieves the cashflow sheet in XLSX format. + * @param {ITransactionsByCustomersFilter} query + * @returns {Promise} + */ + public async xlsx(query: ITransactionsByCustomersFilter): Promise { + const table = await this.transactionsByCustomerTable.table(query); + + const tableSheet = new TableSheet(table.table); + const tableCsv = tableSheet.convertToXLSX(); + + return tableSheet.convertToBuffer(tableCsv, 'xlsx'); + } + + /** + * Retrieves the cashflow sheet in CSV format. + * @param {ITransactionsByCustomersFilter} query + * @returns {Promise} + */ + public async csv(query: ITransactionsByCustomersFilter): Promise { + const table = await this.transactionsByCustomerTable.table(query); + + const tableSheet = new TableSheet(table.table); + const tableCsv = tableSheet.convertToCSV(); + + return tableCsv; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersMeta.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersMeta.ts new file mode 100644 index 000000000..04e16f1b2 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersMeta.ts @@ -0,0 +1,35 @@ +import moment from 'moment'; +import { Injectable } from '@nestjs/common'; +import { FinancialSheetMeta } from '../../common/FinancialSheetMeta'; +import { + ITransactionsByCustomersFilter, + ITransactionsByCustomersMeta, +} from './TransactionsByCustomer.types'; + +@Injectable() +export class TransactionsByCustomersMeta { + constructor(private readonly financialSheetMeta: FinancialSheetMeta) {} + + /** + * Retrieves the transactions by customers meta. + * @param {ITransactionsByCustomersFilter} query - Transactions by customers filter. + * @returns {ITransactionsByCustomersMeta} + */ + public async meta( + query: ITransactionsByCustomersFilter, + ): Promise { + const commonMeta = await this.financialSheetMeta.meta(); + + const formattedToDate = moment(query.toDate).format('YYYY/MM/DD'); + const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD'); + const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`; + + return { + ...commonMeta, + sheetName: 'Transactions By Customers', + formattedFromDate, + formattedToDate, + formattedDateRange, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersPdf.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersPdf.ts new file mode 100644 index 000000000..6f647fa6a --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersPdf.ts @@ -0,0 +1,25 @@ +import { TableSheetPdf } from '../../common/TableSheetPdf'; +import { ITransactionsByCustomersFilter } from './TransactionsByCustomer.types'; +import { TransactionsByCustomersTableInjectable } from './TransactionsByCustomersTableInjectable'; + +export class TransactionsByCustomersPdf { + constructor( + private readonly transactionsByCustomersTable: TransactionsByCustomersTableInjectable, + private readonly tableSheetPdf: TableSheetPdf, + ) {} + + /** + * Retrieves the transactions by customers in PDF format. + * @param {ITransactionsByCustomersFilter} query - Transactions by customers filter. + * @returns {Promise} + */ + public async pdf(query: ITransactionsByCustomersFilter): Promise { + const table = await this.transactionsByCustomersTable.table(query); + + return this.tableSheetPdf.convertToPdf( + table.table, + table.meta.sheetName, + table.meta.formattedDateRange, + ); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersRepository.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersRepository.ts new file mode 100644 index 000000000..b7fad1e1e --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersRepository.ts @@ -0,0 +1,251 @@ +import * as R from 'ramda'; +import { ACCOUNT_TYPE } from '@/constants/accounts'; +import { Account } from '@/modules/Accounts/models/Account.model'; +import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model'; +import { Customer } from '@/modules/Customers/models/Customer'; +import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types'; +import { Inject, Injectable, Scope } from '@nestjs/common'; +import { isEmpty, map } from 'lodash'; +import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository'; +import { Ledger } from '@/modules/Ledger/Ledger'; +import { ITransactionsByCustomersFilter } from './TransactionsByCustomer.types'; +import { ModelObject } from 'objection'; +import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; +import { TransactionsByContactRepository } from '../TransactionsByContact/TransactionsByContactRepository'; + +@Injectable({ scope: Scope.TRANSIENT }) +export class TransactionsByCustomersRepository extends TransactionsByContactRepository { + @Inject(Customer.name) + private readonly customerModel: typeof Customer; + + @Inject(Account.name) + private readonly accountModel: typeof Account; + + @Inject(AccountTransaction.name) + private readonly accountTransactionModel: typeof AccountTransaction; + + @Inject(AccountRepository) + private readonly accountRepository: AccountRepository; + + @Inject(TenancyContext) + private readonly tenancyContext: TenancyContext; + + /** + * Customers models. + * @param {ModelObject[]} customers + */ + public customers: ModelObject[]; + + /** + * Report filter. + * @param {ITransactionsByCustomersFilter} filter + */ + public filter: ITransactionsByCustomersFilter; + + /** + * Customers periods entries. + * @param {ILedgerEntry[]} customersPeriodsEntries + */ + public customersPeriodsEntries: ILedgerEntry[]; + + /** + * Initialize the report data. + * @param {ITransactionsByCustomersFilter} filter + */ + public async asyncInit(filter: ITransactionsByCustomersFilter) { + this.filter = filter; + + await this.initAccountsGraph(); + await this.initCustomers(); + await this.initOpeningBalanceEntries(); + await this.initCustomersPeriodsEntries(); + await this.initLedger(); + await this.initBaseCurrency(); + } + + /** + * Initialize the accounts graph. + */ + async initAccountsGraph() { + const accountsGraph = await this.accountRepository.getDependencyGraph(); + this.accountsGraph = accountsGraph; + } + + /** + * Initialize the customers. + */ + async initCustomers() { + // Retrieve the report customers. + const customers = await this.getCustomers(this.filter.customersIds); + this.customers = customers; + } + + /** + * Initialize the opening balance entries. + */ + async initOpeningBalanceEntries() { + const openingBalanceDate = moment(this.filter.fromDate) + .subtract(1, 'days') + .toDate(); + + // Retrieve all ledger transactions of the opening balance of. + const openingBalanceEntries = + await this.getCustomersOpeningBalanceEntries(openingBalanceDate); + + this.openingBalanceEntries = openingBalanceEntries; + } + + /** + * Initialize the customers periods entries. + */ + async initCustomersPeriodsEntries() { + // Retrieve all ledger transactions between opeing and closing period. + const customersTransactions = await this.getCustomersPeriodsEntries( + this.filter.fromDate, + this.filter.toDate, + ); + this.customersPeriodsEntries = customersTransactions; + } + + /** + * Initialize the ledger. + */ + async initLedger() { + // Concats the opening balance and period customer ledger transactions. + const journalTransactions = [ + ...this.openingBalanceEntries, + ...this.customersPeriodsEntries, + ]; + this.ledger = new Ledger(journalTransactions); + } + + async initBaseCurrency() { + const tenantMetadata = await this.tenancyContext.getTenantMetadata(); + this.baseCurrency = tenantMetadata.baseCurrency; + } + + /** + * Retrieve the customers opening balance ledger entries. + * @param {number} tenantId + * @param {Date} openingDate + * @param {number[]} customersIds + * @returns {Promise} + */ + private async getCustomersOpeningBalanceEntries( + openingDate: Date, + customersIds?: number[], + ): Promise { + const openingTransactions = + await this.getCustomersOpeningBalanceTransactions( + openingDate, + customersIds, + ); + return R.compose( + R.map(R.assoc('date', openingDate)), + R.map(R.assoc('accountNormal', 'debit')), + )(openingTransactions); + } + + /** + * Retrieve the customers periods ledger entries. + * @param {number} tenantId + * @param {Date} fromDate + * @param {Date} toDate + * @returns {Promise} + */ + private async getCustomersPeriodsEntries( + fromDate: Date | string, + toDate: Date | string, + ): Promise { + const transactions = + await this.getCustomersPeriodTransactions( + fromDate, + toDate, + ); + return R.compose( + R.map(R.assoc('accountNormal', 'debit')), + R.map((trans) => ({ + ...trans, + referenceTypeFormatted: trans.referenceTypeFormatted, + })), + )(transactions); + } + + /** + * Retrieve the report customers. + * @param {number[]} customersIds - Customers ids. + * @returns {Promise} + */ + public async getCustomers(customersIds?: number[]) { + return this.customerModel.query().onBuild((q) => { + q.orderBy('displayName'); + + if (!isEmpty(customersIds)) { + q.whereIn('id', customersIds); + } + }); + } + + /** + * Retrieve the accounts receivable. + * @returns {Promise} + */ + public async getReceivableAccounts(): Promise { + const accounts = await this.accountModel + .query() + .where('accountType', ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE); + return accounts; + } + + /** + * Retrieve the customers opening balance transactions. + * @param {number} openingDate - Opening date. + * @param {number} customersIds - Customers ids. + * @returns {Promise} + */ + public async getCustomersOpeningBalanceTransactions( + openingDate: Date, + customersIds?: number[], + ): Promise { + const receivableAccounts = await this.getReceivableAccounts(); + const receivableAccountsIds = map(receivableAccounts, 'id'); + + const openingTransactions = await this.accountTransactionModel + .query() + .modify( + 'contactsOpeningBalance', + openingDate, + receivableAccountsIds, + customersIds, + ); + return openingTransactions; + } + + /** + * Retrieve the customers periods transactions. + * @param {Date} fromDate - From date. + * @param {Date} toDate - To date. + * @return {Promise} + */ + public async getCustomersPeriodTransactions( + fromDate: Date, + toDate: Date, + ): Promise { + const receivableAccounts = await this.getReceivableAccounts(); + const receivableAccountsIds = map(receivableAccounts, 'id'); + + const transactions = await this.accountTransactionModel + .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 transactions; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersService.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersService.ts new file mode 100644 index 000000000..958181a43 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersService.ts @@ -0,0 +1,63 @@ +import { + ITransactionsByCustomersFilter, + ITransactionsByCustomersStatement, +} from './TransactionsByCustomer.types'; +import { TransactionsByCustomersRepository } from './TransactionsByCustomersRepository'; +import { TransactionsByCustomersMeta } from './TransactionsByCustomersMeta'; +import { getTransactionsByCustomerDefaultQuery } from './utils'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { TransactionsByCustomers } from './TransactionsByCustomers'; +import { events } from '@/common/events/events'; +import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class TransactionsByCustomersSheet { + constructor( + private readonly transactionsByCustomersMeta: TransactionsByCustomersMeta, + private readonly transactionsByCustomersRepository: TransactionsByCustomersRepository, + private readonly eventPublisher: EventEmitter2, + private readonly tenancyContext: TenancyContext, + ) {} + + /** + * Retrieve transactions by by the customers. + * @param {number} tenantId + * @param {ITransactionsByCustomersFilter} query + * @return {Promise} + */ + public async transactionsByCustomers( + query: ITransactionsByCustomersFilter, + ): Promise { + const tenantMetadata = await this.tenancyContext.getTenantMetadata(); + + const filter = { + ...getTransactionsByCustomerDefaultQuery(), + ...query, + }; + await this.transactionsByCustomersRepository.asyncInit(filter); + + // Transactions by customers data mapper. + const reportInstance = new TransactionsByCustomers( + filter, + this.transactionsByCustomersRepository, + this.i18n, + ); + + const meta = await this.transactionsByCustomersMeta.meta(filter); + + // Triggers `onCustomerTransactionsViewed` event. + await this.eventPublisher.emitAsync( + events.reports.onCustomerTransactionsViewed, + { + query, + }, + ); + + return { + data: reportInstance.reportData(), + query: filter, + meta, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersTable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersTable.ts new file mode 100644 index 000000000..dd78e725a --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersTable.ts @@ -0,0 +1,92 @@ +import * as R from 'ramda'; +import { ITransactionsByCustomersCustomer } from './TransactionsByCustomer.types'; +import { ITableRow, ITableColumn } from '../../types/Table.types'; +import { TransactionsByContactsTableRows } from '../TransactionsByContact/TransactionsByContactTableRows'; +import { tableRowMapper } from '../../utils/Table.utils'; + +enum ROW_TYPE { + OPENING_BALANCE = 'OPENING_BALANCE', + CLOSING_BALANCE = 'CLOSING_BALANCE', + TRANSACTION = 'TRANSACTION', + CUSTOMER = 'CUSTOMER', +} + +export class TransactionsByCustomersTable extends TransactionsByContactsTableRows { + private customersTransactions: ITransactionsByCustomersCustomer[]; + + /** + * Constructor method. + * @param {ITransactionsByCustomersCustomer[]} customersTransactions - Customers transactions. + */ + constructor(customersTransactions: ITransactionsByCustomersCustomer[], i18n) { + super(); + this.customersTransactions = customersTransactions; + this.i18n = i18n; + } + + /** + * Retrieve the table row of customer details. + * @param {ITransactionsByCustomersCustomer} customer - + * @returns {ITableRow[]} + */ + private customerDetails = (customer: ITransactionsByCustomersCustomer) => { + const columns = [ + { key: 'customerName', accessor: 'customerName' }, + ...R.repeat({ key: 'empty', value: '' }, 5), + { + key: 'closingBalanceValue', + accessor: 'closingBalance.formattedAmount', + }, + ]; + + return { + ...tableRowMapper(customer, columns, { rowTypes: [ROW_TYPE.CUSTOMER] }), + children: R.pipe( + R.when( + R.always(customer.transactions.length > 0), + R.pipe( + R.concat(this.contactTransactions(customer)), + R.prepend(this.contactOpeningBalance(customer)), + ), + ), + R.append(this.contactClosingBalance(customer)), + )([]), + }; + }; + + /** + * Retrieve the table rows of the customer section. + * @param {ITransactionsByCustomersCustomer} customer + * @returns {ITableRow[]} + */ + private customerRowsMapper = (customer: ITransactionsByCustomersCustomer) => { + return R.pipe(this.customerDetails)(customer); + }; + + /** + * Retrieve the table rows of transactions by customers report. + * @param {ITransactionsByCustomersCustomer[]} customers + * @returns {ITableRow[]} + */ + public tableRows = (): ITableRow[] => { + return R.map(this.customerRowsMapper.bind(this))( + this.customersTransactions, + ); + }; + + /** + * Retrieve the table columns of transactions by customers report. + * @returns {ITableColumn[]} + */ + public tableColumns = (): ITableColumn[] => { + return [ + { key: 'customer_name', label: 'Customer name' }, + { key: 'account_name', label: 'Account Name' }, + { key: 'ref_type', label: 'Reference Type' }, + { key: 'transaction_type', label: 'Transaction Type' }, + { key: 'credit', label: 'Credit' }, + { key: 'debit', label: 'Debit' }, + { key: 'running_balance', label: 'Running Balance' }, + ]; + }; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersTableInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersTableInjectable.ts new file mode 100644 index 000000000..a485ca03d --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/TransactionsByCustomersTableInjectable.ts @@ -0,0 +1,42 @@ +import { + ITransactionsByCustomersFilter, + ITransactionsByCustomersTable, +} from './TransactionsByCustomer.types'; +import { TransactionsByCustomersSheet } from './TransactionsByCustomersService'; +import { TransactionsByCustomersTable } from './TransactionsByCustomersTable'; +import { Injectable } from '@nestjs/common'; +import { I18nService } from 'nestjs-i18n'; + +@Injectable() +export class TransactionsByCustomersTableInjectable { + constructor( + private readonly transactionsByCustomerService: TransactionsByCustomersSheet, + private readonly i18n: I18nService, + ) {} + + /** + * Retrieves the transactions by customers sheet in table format. + * @param {number} tenantId + * @param {ITransactionsByCustomersFilter} filter + * @returns {Promise} + */ + public async table( + filter: ITransactionsByCustomersFilter, + ): Promise { + const customersTransactions = + await this.transactionsByCustomerService.transactionsByCustomers(filter); + + const table = new TransactionsByCustomersTable( + customersTransactions.data, + this.i18n, + ); + return { + table: { + rows: table.tableRows(), + columns: table.tableColumns(), + }, + query: customersTransactions.query, + meta: customersTransactions.meta, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/utils.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/utils.ts new file mode 100644 index 000000000..286615bda --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByCustomer/utils.ts @@ -0,0 +1,22 @@ + + + +export const getTransactionsByCustomerDefaultQuery = () => { + return { + fromDate: moment().startOf('month').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: true, + customersIds: [], + }; +}; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionByReference.module.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionByReference.module.ts new file mode 100644 index 000000000..600593574 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionByReference.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { TransactionsByReferenceApplication } from './TransactionsByReferenceApplication'; +import { TransactionsByReferenceRepository } from './TransactionsByReferenceRepository'; +import { TransactionsByReferenceService } from './TransactionsByReference.service'; +import { TransactionsByReferenceController } from './TransactionsByReference.controller'; + +@Module({ + providers: [ + TransactionsByReferenceRepository, + TransactionsByReferenceApplication, + TransactionsByReferenceService, + ], + controllers: [TransactionsByReferenceController], +}) +export class TransactionsByReferenceModule {} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReference.controller.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReference.controller.ts new file mode 100644 index 000000000..9f865ea3f --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReference.controller.ts @@ -0,0 +1,19 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { TransactionsByReferenceApplication } from './TransactionsByReferenceApplication'; +import { ITransactionsByReferenceQuery } from './TransactionsByReference.types'; + +@Controller('reports/transactions-by-reference') +export class TransactionsByReferenceController { + constructor( + private readonly transactionsByReferenceApp: TransactionsByReferenceApplication, + ) {} + + @Get() + async getTransactionsByReference( + @Query() query: ITransactionsByReferenceQuery, + ) { + const data = await this.transactionsByReferenceApp.getTransactions(query); + + return data; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReference.service.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReference.service.ts new file mode 100644 index 000000000..1d8d0daab --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReference.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@nestjs/common'; +import { + ITransactionsByReferencePojo, + ITransactionsByReferenceQuery, +} from './TransactionsByReference.types'; +import { TransactionsByReferenceRepository } from './TransactionsByReferenceRepository'; +import { TransactionsByReference } from './TransactionsByReferenceReport'; +import { getTransactionsByReferenceQuery } from './_utils'; +import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; + +@Injectable() +export class TransactionsByReferenceService { + constructor( + private readonly repository: TransactionsByReferenceRepository, + private readonly tenancyContext: TenancyContext + ) {} + + /** + * Retrieve accounts transactions by given reference id and type. + * @param {ITransactionsByReferenceQuery} filter - Transactions by reference query. + * @returns {Promise} + */ + public async getTransactionsByReference( + query: ITransactionsByReferenceQuery + ): Promise { + const filter = { + ...getTransactionsByReferenceQuery(), + ...query, + }; + const tenantMetadata = await this.tenancyContext.getTenantMetadata(); + + // Retrieve the accounts transactions of the given reference. + const transactions = await this.repository.getTransactions( + Number(filter.referenceId), + filter.referenceType + ); + // Transactions by reference report. + const report = new TransactionsByReference( + transactions, + filter, + tenantMetadata.baseCurrency + ); + + return { + transactions: report.reportData(), + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReference.types.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReference.types.ts new file mode 100644 index 000000000..146a4cb74 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReference.types.ts @@ -0,0 +1,34 @@ +export interface ITransactionsByReferenceQuery { + referenceType: string; + referenceId: string; +} + +export interface ITransactionsByReferenceAmount { + amount: number; + formattedAmount: string; + currencyCode: string; +} + +export interface ITransactionsByReferenceTransaction { + credit: ITransactionsByReferenceAmount; + debit: ITransactionsByReferenceAmount; + + contactType: string; + formattedContactType: string; + + contactId: number; + + referenceType: string; + formattedReferenceType: string; + + referenceId: number; + + accountName: string; + accountCode: string; + accountId: number; +} + + +export interface ITransactionsByReferencePojo { + transactions: ITransactionsByReferenceTransaction[]; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReferenceApplication.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReferenceApplication.ts new file mode 100644 index 000000000..671f5b5c6 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReferenceApplication.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { TransactionsByReferenceService } from './TransactionsByReference.service'; +import { ITransactionsByReferenceQuery } from './TransactionsByReference.types'; + +@Injectable() +export class TransactionsByReferenceApplication { + constructor( + private readonly transactionsByReferenceService: TransactionsByReferenceService, + ) {} + + /** + * Retrieve accounts transactions by given reference id and type. + * @param {ITransactionsByReferenceQuery} query - Transactions by reference query. + * @returns {Promise} + */ + public async getTransactions(query: ITransactionsByReferenceQuery) { + return this.transactionsByReferenceService.getTransactionsByReference( + query, + ); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReferenceReport.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReferenceReport.ts new file mode 100644 index 000000000..e89483480 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReferenceReport.ts @@ -0,0 +1,81 @@ +import { + ITransactionsByReferenceQuery, + ITransactionsByReferenceTransaction, +} from './TransactionsByReference.types'; +import { FinancialSheet } from '../../common/FinancialSheet'; +import { ModelObject } from 'objection'; +import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model'; +import { INumberFormatQuery } from '../../types/Report.types'; + +export class TransactionsByReference extends FinancialSheet { + readonly transactions: ModelObject[]; + readonly query: ITransactionsByReferenceQuery; + readonly baseCurrency: string; + readonly numberFormat: INumberFormatQuery; + + /** + * Constructor method. + * @param {ModelObject[]} transactions + * @param {ITransactionsByReferenceQuery} query + * @param {string} baseCurrency + */ + constructor( + transactions: ModelObject[], + query: ITransactionsByReferenceQuery, + baseCurrency: string + ) { + super(); + + this.transactions = transactions; + this.query = query; + this.baseCurrency = baseCurrency; + this.numberFormat = this.query.numberFormat; + } + + /** + * Mappes the given account transaction to report transaction. + * @param {IAccountTransaction} transaction + * @returns {ITransactionsByReferenceTransaction} + */ + private transactionMapper = ( + transaction: ModelObject + ): ITransactionsByReferenceTransaction => { + return { + date: this.getDateMeta(transaction.date), + + credit: this.getAmountMeta(transaction.credit, { money: false }), + debit: this.getAmountMeta(transaction.debit, { money: false }), + + referenceTypeFormatted: transaction.referenceTypeFormatted, + referenceType: transaction.referenceType, + referenceId: transaction.referenceId, + + contactId: transaction.contactId, + contactType: transaction.contactType, + contactTypeFormatted: transaction.contactType, + + accountName: transaction.account.name, + accountCode: transaction.account.code, + accountId: transaction.accountId, + }; + }; + + /** + * Mappes the given accounts transactions to report transactions. + * @param {IAccountTransaction} transaction + * @returns {ITransactionsByReferenceTransaction} + */ + private transactionsMapper = ( + transactions: ModelObject[] + ): ITransactionsByReferenceTransaction[] => { + return transactions.map(this.transactionMapper); + }; + + /** + * Retrieve the report data. + * @returns {ITransactionsByReferenceTransaction} + */ + public reportData(): ITransactionsByReferenceTransaction[] { + return this.transactionsMapper(this.transactions); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReferenceRepository.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReferenceRepository.ts new file mode 100644 index 000000000..6f515ad73 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/TransactionsByReferenceRepository.ts @@ -0,0 +1,28 @@ +import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model'; +import { Inject, Injectable } from '@nestjs/common'; +import { ModelObject } from 'objection'; + +@Injectable() +export class TransactionsByReferenceRepository { + constructor( + @Inject(AccountTransaction.name) + private readonly accountTransactionModel: typeof AccountTransaction, + ) {} + + /** + * Retrieve the accounts transactions of the givne reference id and type. + * @param {number} tenantId - + * @param {number} referenceId - Reference id. + * @param {string} referenceType - Reference type. + * @return {Promise} + */ + public async getTransactions( + referenceId: number, + referenceType: string, + ): Promise>> { + return this.accountTransactionModel.query() + .where('reference_id', referenceId) + .where('reference_type', referenceType) + .withGraphFetched('account'); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/_utils.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/_utils.ts new file mode 100644 index 000000000..ab88109cc --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByReference/_utils.ts @@ -0,0 +1,12 @@ + + + +export const getTransactionsByReferenceQuery = () => ({ + numberFormat: { + precision: 2, + divideOn1000: false, + showZero: false, + formatMoney: 'total', + negativeFormat: 'mines', + }, +}) \ No newline at end of file diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendor.controller.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendor.controller.ts new file mode 100644 index 000000000..9d293065a --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendor.controller.ts @@ -0,0 +1,59 @@ +import { Controller, Get, Headers, Query, Res } from '@nestjs/common'; +import { ITransactionsByVendorsFilter } from './TransactionsByVendor.types'; +import { AcceptType } from '@/constants/accept-type'; +import { Response } from 'express'; +import { TransactionsByVendorApplication } from './TransactionsByVendorApplication'; +import { ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { PublicRoute } from '@/modules/Auth/Jwt.guard'; + +@Controller('/reports/transactions-by-vendors') +@PublicRoute() +export class TransactionsByVendorController { + constructor( + private readonly transactionsByVendorsApp: TransactionsByVendorApplication, + ) {} + + @Get() + @ApiOperation({ summary: 'Get transactions by vendor' }) + @ApiResponse({ status: 200, description: 'Transactions by vendor' }) + async transactionsByVendor( + @Query() filter: ITransactionsByVendorsFilter, + @Res() res: Response, + @Headers('accept') acceptHeader: string, + ) { + // Retrieves the xlsx format. + if (acceptHeader.includes(AcceptType.ApplicationXlsx)) { + const buffer = await this.transactionsByVendorsApp.xlsx(filter); + + res.setHeader('Content-Type', 'application/vnd.openxmlformats'); + res.setHeader('Content-Disposition', 'attachment; filename=report.xlsx'); + + return res.send(buffer); + // Retrieves the csv format. + } else if (acceptHeader.includes(AcceptType.ApplicationCsv)) { + const buffer = await this.transactionsByVendorsApp.csv(filter); + + res.setHeader('Content-Type', 'text/csv'); + res.setHeader('Content-Disposition', 'attachment; filename=report.csv'); + + return res.send(buffer); + // Retrieves the json table format. + } else if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) { + const table = await this.transactionsByVendorsApp.table(filter); + + return res.status(200).send(table); + // Retrieves the pdf format. + } else if (acceptHeader.includes(AcceptType.ApplicationPdf)) { + const pdfContent = await this.transactionsByVendorsApp.pdf(filter); + res.set({ + 'Content-Type': 'application/pdf', + 'Content-Length': pdfContent.length, + }); + return res.send(pdfContent); + // Retrieves the json format. + } else { + const sheet = await this.transactionsByVendorsApp.sheet(filter); + return res.status(200).send(sheet); + } + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendor.module.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendor.module.ts new file mode 100644 index 000000000..d5c2c0e84 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendor.module.ts @@ -0,0 +1,29 @@ +import { Module } from '@nestjs/common'; +import { TransactionsByVendorController } from './TransactionsByVendor.controller'; +import { TransactionsByVendorsInjectable } from './TransactionsByVendorInjectable'; +import { TransactionsByVendorMeta } from './TransactionsByVendorMeta'; +import { TransactionsByVendorRepository } from './TransactionsByVendorRepository'; +import { TransactionsByVendorTableInjectable } from './TransactionsByVendorTableInjectable'; +import { TransactionsByVendorExportInjectable } from './TransactionsByVendorExportInjectable'; +import { TransactionsByVendorsPdf } from './TransactionsByVendorPdf'; +import { TransactionsByVendorApplication } from './TransactionsByVendorApplication'; +import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module'; +import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; +import { AccountsModule } from '@/modules/Accounts/Accounts.module'; + +@Module({ + imports: [FinancialSheetCommonModule, AccountsModule], + controllers: [TransactionsByVendorController,], + providers: [ + TransactionsByVendorsInjectable, + TransactionsByVendorRepository, + TransactionsByVendorMeta, + TransactionsByVendorTableInjectable, + TransactionsByVendorExportInjectable, + TransactionsByVendorsPdf, + TransactionsByVendorApplication, + TenancyContext + ], + exports: [TransactionsByVendorApplication], +}) +export class TransactionsByVendorModule {} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendor.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendor.ts new file mode 100644 index 000000000..b2514749c --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendor.ts @@ -0,0 +1,136 @@ +import * as R from 'ramda'; +import { isEmpty } from 'lodash'; +import { ModelObject } from 'objection'; +import { + ITransactionsByVendorsFilter, + ITransactionsByVendorsTransaction, + ITransactionsByVendorsVendor, + ITransactionsByVendorsData, +} from './TransactionsByVendor.types'; +import { TransactionsByContact } from '../TransactionsByContact/TransactionsByContact'; +import { Vendor } from '@/modules/Vendors/models/Vendor'; +import { INumberFormatQuery } from '../../types/Report.types'; +import { I18nService } from 'nestjs-i18n'; +import { TransactionsByVendorRepository } from './TransactionsByVendorRepository'; + +const VENDOR_NORMAL = 'credit'; + +export class TransactionsByVendor extends TransactionsByContact { + public readonly repository: TransactionsByVendorRepository; + public readonly filter: ITransactionsByVendorsFilter; + public readonly numberFormat: INumberFormatQuery; + public readonly i18n: I18nService; + + /** + * Constructor method. + * @param {TransactionsByVendorRepository} transactionsByVendorRepository - Transactions by vendor repository. + * @param {ITransactionsByVendorsFilter} filter - Transactions by vendors filter. + * @param {I18nService} i18n - Internationalization service. + */ + constructor( + transactionsByVendorRepository: TransactionsByVendorRepository, + filter: ITransactionsByVendorsFilter, + i18n: I18nService, + ) { + super(); + + this.repository = transactionsByVendorRepository; + this.filter = filter; + this.numberFormat = this.filter.numberFormat; + this.i18n = i18n; + } + + /** + * 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 openingBalanceLedger = this.repository.journal + .whereContactId(vendorId) + .whereFromDate(this.filter.fromDate) + .whereToDate(this.filter.toDate); + + const openingEntries = openingBalanceLedger.getEntries(); + + return R.compose( + R.curry(this.contactTransactionRunningBalance)(openingBalance, 'credit'), + R.map(this.contactTransactionMapper.bind(this)), + ).bind(this)(openingEntries); + } + + /** + * Vendor section mapper. + * @param {IVendor} vendor + * @returns {ITransactionsByVendorsVendor} + */ + private vendorMapper( + vendor: ModelObject, + ): ITransactionsByVendorsVendor { + const openingBalance = this.getContactOpeningBalance(vendor.id); + const transactions = this.vendorTransactions(vendor.id, openingBalance); + const closingBalance = this.getVendorClosingBalance( + transactions, + openingBalance, + ); + const currencyCode = this.baseCurrency; + + return { + vendorName: vendor.displayName, + openingBalance: this.getTotalAmountMeta(openingBalance, currencyCode), + closingBalance: this.getTotalAmountMeta(closingBalance, currencyCode), + transactions, + }; + } + + /** + * Retrieve the vendor closing balance from the given customer transactions. + * @param {ITransactionsByContactsTransaction[]} customerTransactions + * @param {number} openingBalance + * @returns + */ + private getVendorClosingBalance( + vendorTransactions: ITransactionsByVendorsTransaction[], + openingBalance: number, + ) { + return this.getContactClosingBalance( + vendorTransactions, + VENDOR_NORMAL, + openingBalance, + ); + } + + /** + * Detarmines whether the vendors post filter is active. + * @returns {boolean} + */ + private isVendorsPostFilter = (): boolean => { + return isEmpty(this.filter.vendorsIds); + }; + + /** + * Retrieve the vendors sections of the report. + * @param {IVendor[]} vendors + * @returns {ITransactionsByVendorsVendor[]} + */ + private vendorsMapper( + vendors: ModelObject[], + ): ITransactionsByVendorsVendor[] { + return R.compose( + R.when(this.isVendorsPostFilter, this.contactsFilter), + R.map(this.vendorMapper.bind(this)), + ).bind(this)(vendors); + } + + /** + * Retrieve the report data. + * @returns {ITransactionsByVendorsData} + */ + public reportData(): ITransactionsByVendorsData { + return this.vendorsMapper(this.repository.vendors); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendor.types.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendor.types.ts new file mode 100644 index 000000000..c291722d1 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendor.types.ts @@ -0,0 +1,52 @@ + +import { IFinancialSheetCommonMeta } from '../../types/Report.types'; +import { IFinancialTable } from '../../types/Table.types'; +import { + ITransactionsByContactsAmount, + ITransactionsByContactsTransaction, + ITransactionsByContactsFilter, +} from '../TransactionsByContact/TransactionsByContact.types'; + +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 { + vendorsIds: number[]; +} + +export type ITransactionsByVendorsData = ITransactionsByVendorsVendor[]; + +export interface ITransactionsByVendorsStatement { + data: ITransactionsByVendorsData; + query: ITransactionsByVendorsFilter; + meta: ITransactionsByVendorMeta; +} + +export interface ITransactionsByVendorsService { + transactionsByVendors( + tenantId: number, + filter: ITransactionsByVendorsFilter + ): Promise; +} + +export interface ITransactionsByVendorTable extends IFinancialTable { + query: ITransactionsByVendorsFilter; + meta: ITransactionsByVendorMeta; +} + +export interface ITransactionsByVendorMeta extends IFinancialSheetCommonMeta { + formattedFromDate: string; + formattedToDate: string; + formattedDateRange: string; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorApplication.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorApplication.ts new file mode 100644 index 000000000..25874d64e --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorApplication.ts @@ -0,0 +1,77 @@ +import { + ITransactionsByVendorTable, + ITransactionsByVendorsFilter, + ITransactionsByVendorsStatement, +} from './TransactionsByVendor.types'; +import { TransactionsByVendorExportInjectable } from './TransactionsByVendorExportInjectable'; +import { TransactionsByVendorTableInjectable } from './TransactionsByVendorTableInjectable'; +import { TransactionsByVendorsInjectable } from './TransactionsByVendorInjectable'; +import { TransactionsByVendorsPdf } from './TransactionsByVendorPdf'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class TransactionsByVendorApplication { + constructor( + private readonly transactionsByVendorTable: TransactionsByVendorTableInjectable, + private readonly transactionsByVendorExport: TransactionsByVendorExportInjectable, + private readonly transactionsByVendorSheet: TransactionsByVendorsInjectable, + private readonly transactionsByVendorPdf: TransactionsByVendorsPdf, + ) {} + + /** + * Retrieves the transactions by vendor in sheet format. + * @param {ITransactionsByVendorsFilter} query - The filter query. + * @returns {Promise} + */ + public sheet( + query: ITransactionsByVendorsFilter + ): Promise { + return this.transactionsByVendorSheet.transactionsByVendors( + query + ); + } + + /** + * Retrieves the transactions by vendor in table format. + * @param {ITransactionsByVendorsFilter} query + * @returns {Promise} + */ + public table( + query: ITransactionsByVendorsFilter + ): Promise { + return this.transactionsByVendorTable.table(query); + } + + /** + * Retrieves the transactions by vendor in CSV format. + * @param {ITransactionsByVendorsFilter} query + * @returns {Promise} + */ + public csv( + query: ITransactionsByVendorsFilter + ): Promise { + return this.transactionsByVendorExport.csv(query); + } + + /** + * Retrieves the transactions by vendor in XLSX format. + * @param {number} tenantId + * @param {ITransactionsByVendorsFilter} query + * @returns {Promise} + */ + public xlsx( + query: ITransactionsByVendorsFilter + ): Promise { + return this.transactionsByVendorExport.xlsx(query); + } + + /** + * Retrieves the transactions by vendor in PDF format. + * @param {number} tenantId + * @param {ITransactionsByVendorsFilter} query + * @returns {Promise} + */ + public pdf(query: ITransactionsByVendorsFilter) { + return this.transactionsByVendorPdf.pdf(query); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorExportInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorExportInjectable.ts new file mode 100644 index 000000000..89a7b11e4 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorExportInjectable.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@nestjs/common'; +import { ITransactionsByVendorsFilter } from './TransactionsByVendor.types'; +import { TransactionsByVendorTableInjectable } from './TransactionsByVendorTableInjectable'; +import { TableSheet } from '../../common/TableSheet'; + +@Injectable() +export class TransactionsByVendorExportInjectable { + constructor( + private readonly transactionsByVendorTable: TransactionsByVendorTableInjectable, + ) {} + + /** + * Retrieves the cashflow sheet in XLSX format. + * @param {ITransactionsByVendorsFilter} query + * @returns {Promise} + */ + public async xlsx( + query: ITransactionsByVendorsFilter + ): Promise { + const table = await this.transactionsByVendorTable.table(query); + + const tableSheet = new TableSheet(table.table); + const tableCsv = tableSheet.convertToXLSX(); + + return tableSheet.convertToBuffer(tableCsv, 'xlsx'); + } + + /** + * Retrieves the cashflow sheet in CSV format. + * @param {ICashFlowStatementQuery} query + * @returns {Promise} + */ + public async csv( + query: ITransactionsByVendorsFilter + ): Promise { + const table = await this.transactionsByVendorTable.table(query); + + const tableSheet = new TableSheet(table.table); + const tableCsv = tableSheet.convertToCSV(); + + return tableCsv; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorInjectable.ts new file mode 100644 index 000000000..59b83a3e3 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorInjectable.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@nestjs/common'; +import { I18nService } from 'nestjs-i18n'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { + ITransactionsByVendorsFilter, + ITransactionsByVendorsStatement, +} from './TransactionsByVendor.types'; +import { TransactionsByVendor } from './TransactionsByVendor'; +import { TransactionsByVendorRepository } from './TransactionsByVendorRepository'; +import { TransactionsByVendorMeta } from './TransactionsByVendorMeta'; +import { getTransactionsByVendorDefaultQuery } from './utils'; +import { events } from '@/common/events/events'; + +@Injectable() +export class TransactionsByVendorsInjectable { + constructor( + private readonly transactionsByVendorRepository: TransactionsByVendorRepository, + private readonly transactionsByVendorMeta: TransactionsByVendorMeta, + private readonly eventPublisher: EventEmitter2, + private readonly i18n: I18nService, + ) {} + + /** + * Retrieve transactions by by the customers. + * @param {ITransactionsByVendorsFilter} query - Transactions by vendors filter. + * @return {Promise} + */ + public async transactionsByVendors( + query: ITransactionsByVendorsFilter, + ): Promise { + const filter = { ...getTransactionsByVendorDefaultQuery(), ...query }; + + // Transactions by customers data mapper. + const reportInstance = new TransactionsByVendor( + this.transactionsByVendorRepository, + filter, + this.i18n, + ); + const meta = await this.transactionsByVendorMeta.meta(filter); + + // Triggers `onVendorTransactionsViewed` event. + await this.eventPublisher.emitAsync( + events.reports.onVendorTransactionsViewed, + { query }, + ); + + return { + data: reportInstance.reportData(), + query: filter, + meta, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorMeta.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorMeta.ts new file mode 100644 index 000000000..654d972ac --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorMeta.ts @@ -0,0 +1,38 @@ +import * as moment from 'moment'; +import { + ITransactionsByVendorMeta, + ITransactionsByVendorsFilter, +} from './TransactionsByVendor.types'; +import { Injectable } from '@nestjs/common'; +import { FinancialSheetMeta } from '../../common/FinancialSheetMeta'; + +@Injectable() +export class TransactionsByVendorMeta { + constructor( + private readonly financialSheetMeta: FinancialSheetMeta, + ) {} + + /** + * Retrieves the transactions by vendor meta. + * @returns {Promise} + */ + public async meta( + query: ITransactionsByVendorsFilter + ): Promise { + const commonMeta = await this.financialSheetMeta.meta(); + + const formattedToDate = moment(query.toDate).format('YYYY/MM/DD'); + const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD'); + const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`; + + const sheetName = 'Transactions By Vendor'; + + return { + ...commonMeta, + sheetName, + formattedFromDate, + formattedToDate, + formattedDateRange, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorPdf.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorPdf.ts new file mode 100644 index 000000000..cfc75a6fe --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorPdf.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@nestjs/common'; +import { TableSheetPdf } from '../../common/TableSheetPdf'; +import { ITransactionsByVendorsFilter } from './TransactionsByVendor.types'; +import { TransactionsByVendorTableInjectable } from './TransactionsByVendorTableInjectable'; +import { HtmlTableCustomCss } from './constants'; + +@Injectable() +export class TransactionsByVendorsPdf { + constructor( + private readonly transactionsByVendorTable: TransactionsByVendorTableInjectable, + private readonly tableSheetPdf: TableSheetPdf, + ) {} + + /** + * Converts the given balance sheet table to pdf. + * @param {IBalanceSheetQuery} query - Balance sheet query. + * @returns {Promise} + */ + public async pdf(query: ITransactionsByVendorsFilter): Promise { + const table = await this.transactionsByVendorTable.table(query); + + return this.tableSheetPdf.convertToPdf( + table.table, + table.meta.sheetName, + table.meta.formattedDateRange, + HtmlTableCustomCss, + ); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorRepository.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorRepository.ts new file mode 100644 index 000000000..633b4e2b4 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorRepository.ts @@ -0,0 +1,263 @@ +import * as R from 'ramda'; +import { isEmpty, map } from 'lodash'; +import { Inject, Injectable } from '@nestjs/common'; +import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model'; +import { Account } from '@/modules/Accounts/models/Account.model'; +import { Vendor } from '@/modules/Vendors/models/Vendor'; +import { ACCOUNT_TYPE } from '@/constants/accounts'; +import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types'; +import { TransactionsByContactRepository } from '../TransactionsByContact/TransactionsByContactRepository'; +import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; +import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository'; +import { Ledger } from '@/modules/Ledger/Ledger'; +import { ModelObject } from 'objection'; +import { ITransactionsByVendorsFilter } from './TransactionsByVendor.types'; + +@Injectable() +export class TransactionsByVendorRepository extends TransactionsByContactRepository { + @Inject(TenancyContext) + public readonly tenancyContext: TenancyContext; + + @Inject(AccountRepository) + public readonly accountRepository: AccountRepository; + + @Inject(Vendor.name) + public readonly vendorModel: typeof Vendor; + + @Inject(Account.name) + public readonly accountModel: typeof Account; + + @Inject(AccountTransaction.name) + public readonly accountTransactionModel: typeof AccountTransaction; + + /** + * Ledger. + * @param {Ledger} ledger + */ + public ledger: Ledger; + + /** + * Vendors. + * @param {ModelObject[]} vendors + */ + public vendors: ModelObject[]; + + /** + * Accounts graph. + * @param {any} accountsGraph + */ + public accountsGraph: any; + + /** + * Base currency. + * @param {string} baseCurrency + */ + public baseCurrency: string; + + /** + * Report filter. + * @param {ITransactionsByVendorsFilter} filter + */ + public filter: ITransactionsByVendorsFilter; + + /** + * Report entries. + * @param {ILedgerEntry[]} reportEntries + */ + public reportEntries: ILedgerEntry[]; + + /** + * Journal. + * @param {Ledger} journal + */ + public journal: Ledger; + + async asyncInit() { + await this.initBaseCurrency(); + await this.initVendors(); + await this.initAccountsGraph(); + await this.initPeriodEntries(); + await this.initLedger(); + } + + /** + * Retrieve the base currency. + */ + async initBaseCurrency() { + const tenantMetadata = await this.tenancyContext.getTenantMetadata(); + this.baseCurrency = tenantMetadata.baseCurrency; + } + + /** + * Retrieve the vendors. + */ + async initVendors() { + const vendors = await this.getVendors(this.filter.vendorsIds); + this.vendors = vendors; + } + + /** + * Retrieve the accounts graph. + */ + async initAccountsGraph() { + this.accountsGraph = await this.accountRepository.getDependencyGraph(); + } + + /** + * Retrieve the report entries. + */ + async initPeriodEntries() { + this.reportEntries = await this.getReportEntries( + this.filter.fromDate, + this.filter.toDate, + ); + } + + async initLedger() { + this.journal = new Ledger(this.reportEntries); + } + + /** + * Retrieve the vendors opening balance transactions. + * @param {Date} openingDate - The opening date. + * @param {number[]} customersIds - The customers ids. + * @returns {Promise} + */ + public async getVendorsOpeningBalanceEntries( + openingDate: Date, + customersIds?: number[], + ): Promise { + const openingTransactions = await this.getVendorsOpeningBalance( + openingDate, + customersIds, + ); + + // @ts-ignore + return R.compose( + R.map(R.assoc('date', openingDate)), + R.map(R.assoc('accountNormal', 'credit')), + )(openingTransactions); + } + + /** + * Retrieve the vendors period transactions. + * @param {Date|string} openingDate + * @param {number[]} customersIds + */ + public async getVendorsPeriodEntries( + fromDate: moment.MomentInput, + toDate: moment.MomentInput, + ): Promise { + const transactions = await this.getVendorsPeriodTransactions( + fromDate, + toDate, + ); + // @ts-ignore + return R.compose( + R.map(R.assoc('accountNormal', 'credit')), + R.map((trans) => ({ + // @ts-ignore + ...trans, + // @ts-ignore + referenceTypeFormatted: trans.referenceTypeFormatted, + })), + )(transactions); + } + + /** + * Retrieve the report ledger entries from repository. + * @param {number} tenantId + * @param {Date} fromDate + * @param {Date} toDate + * @returns {Promise} + */ + public async getReportEntries( + fromDate: moment.MomentInput, + toDate: moment.MomentInput, + ): Promise { + const openingBalanceDate = moment(fromDate).subtract(1, 'days').toDate(); + + return [ + ...(await this.getVendorsOpeningBalanceEntries(openingBalanceDate)), + ...(await this.getVendorsPeriodEntries(fromDate, toDate)), + ]; + } + + /** + * Retrieve the report vendors. + * @param {number[]} vendorsIds - The vendors IDs. + * @returns {Promise} + */ + public async getVendors(vendorsIds?: number[]): Promise { + return await this.vendorModel.query().onBuild((q) => { + q.orderBy('displayName'); + + if (!isEmpty(vendorsIds)) { + q.whereIn('id', vendorsIds); + } + }); + } + + /** + * Retrieve the accounts receivable. + * @returns {Promise} + */ + public async getPayableAccounts(): Promise { + const accounts = await this.accountModel + .query() + .where('accountType', ACCOUNT_TYPE.ACCOUNTS_PAYABLE); + return accounts; + } + + /** + * Retrieve the customers opening balance transactions. + * @param {Date} openingDate - The opening date. + * @param {number[]} customersIds - The customers IDs. + * @returns {Promise} + */ + public async getVendorsOpeningBalance( + openingDate: Date, + customersIds?: number[], + ): Promise { + const payableAccounts = await this.getPayableAccounts(); + const payableAccountsIds = map(payableAccounts, 'id'); + + const openingTransactions = await this.accountTransactionModel + .query() + .modify( + 'contactsOpeningBalance', + openingDate, + payableAccountsIds, + customersIds, + ); + return openingTransactions; + } + + /** + * Retrieve vendors periods transactions. + * @param {Date} fromDate - The from date. + * @param {Date} toDate - The to date. + * @returns {Promise} + */ + public async getVendorsPeriodTransactions( + fromDate: moment.MomentInput, + toDate: moment.MomentInput, + ): Promise { + const receivableAccounts = await this.getPayableAccounts(); + const receivableAccountsIds = map(receivableAccounts, 'id'); + + const transactions = await this.accountTransactionModel + .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 transactions; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorTable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorTable.ts new file mode 100644 index 000000000..93088f49f --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorTable.ts @@ -0,0 +1,91 @@ +import * as R from 'ramda'; +import { ITransactionsByVendorsVendor } from './TransactionsByVendor.types'; +import { TransactionsByContactsTableRows } from '../TransactionsByContact/TransactionsByContactTableRows'; +import { tableRowMapper } from '../../utils/Table.utils'; +import { ITableRow, ITableColumn } from '../../types/Table.types'; + +enum ROW_TYPE { + OPENING_BALANCE = 'OPENING_BALANCE', + CLOSING_BALANCE = 'CLOSING_BALANCE', + TRANSACTION = 'TRANSACTION', + VENDOR = 'VENDOR', +} + +export class TransactionsByVendorsTable extends TransactionsByContactsTableRows { + private vendorsTransactions: ITransactionsByVendorsVendor[]; + + /** + * Constructor method. + * @param {ITransactionsByVendorsVendor[]} vendorsTransactions - + * @param {any} i18n + */ + constructor(vendorsTransactions: ITransactionsByVendorsVendor[], i18n) { + super(); + + this.vendorsTransactions = vendorsTransactions; + this.i18n = i18n; + } + + /** + * Retrieve the table row of vendor details. + * @param {ITransactionsByVendorsVendor} vendor - + * @returns {ITableRow[]} + */ + private vendorDetails = (vendor: ITransactionsByVendorsVendor) => { + const columns = [ + { key: 'vendorName', accessor: 'vendorName' }, + ...R.repeat({ key: 'empty', value: '' }, 5), + { + key: 'closingBalanceValue', + accessor: 'closingBalance.formattedAmount', + }, + ]; + return { + ...tableRowMapper(vendor, columns, { rowTypes: [ROW_TYPE.VENDOR] }), + children: R.pipe( + R.when( + R.always(vendor.transactions.length > 0), + R.pipe( + R.concat(this.contactTransactions(vendor)), + R.prepend(this.contactOpeningBalance(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(this.vendorDetails)(vendor); + }; + + /** + * Retrieve the table rows of transactions by vendors report. + * @param {ITransactionsByVendorsVendor[]} vendors + * @returns {ITableRow[]} + */ + public tableRows = (): ITableRow[] => { + return R.map(this.vendorRowsMapper)(this.vendorsTransactions); + }; + + /** + * Retrieve the table columns of transactions by vendors report. + * @returns {ITableColumn[]} + */ + public tableColumns = (): ITableColumn[] => { + return [ + { key: 'vendor_name', label: 'Vendor name' }, + { key: 'account_name', label: 'Account Name' }, + { key: 'ref_type', label: 'Reference Type' }, + { key: 'transaction_type', label: 'Transaction Type' }, + { key: 'credit', label: 'Credit' }, + { key: 'debit', label: 'Debit' }, + { key: 'running_balance', label: 'Running Balance' }, + ]; + }; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorTableInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorTableInjectable.ts new file mode 100644 index 000000000..9ccfa47b2 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/TransactionsByVendorTableInjectable.ts @@ -0,0 +1,39 @@ +import { TransactionsByVendorsTable } from './TransactionsByVendorTable'; +import { + ITransactionsByVendorTable, + ITransactionsByVendorsFilter, +} from './TransactionsByVendor.types'; +import { TransactionsByVendorsInjectable } from './TransactionsByVendorInjectable'; +import { Injectable } from '@nestjs/common'; +import { I18nService } from 'nestjs-i18n'; + +@Injectable() +export class TransactionsByVendorTableInjectable { + constructor( + private readonly transactionsByVendor: TransactionsByVendorsInjectable, + private readonly i18n: I18nService + ) {} + + /** + * Retrieves the transactions by vendor in table format. + * @param {ITransactionsByReferenceQuery} query - The filter query. + * @returns {Promise} + */ + public async table( + query: ITransactionsByVendorsFilter + ): Promise { + const sheet = await this.transactionsByVendor.transactionsByVendors( + query + ); + const table = new TransactionsByVendorsTable(sheet.data, this.i18n); + + return { + table: { + rows: table.tableRows(), + columns: table.tableColumns(), + }, + query, + meta: sheet.meta, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/constants.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/constants.ts new file mode 100644 index 000000000..d35a2f105 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/constants.ts @@ -0,0 +1,23 @@ +export const HtmlTableCustomCss = ` +table tr td:not(:first-child) { + border-left: 1px solid #ececec; +} +table tr:last-child td { + border-bottom: 1px solid #ececec; +} +table .cell--credit, +table .cell--debit, +table .column--credit, +table .column--debit, +table .column--running_balance, +table .cell--running_balance{ + text-align: right; +} +table tr.row-type--closing-balance td, +table tr.row-type--opening-balance td { + font-weight: 600; +} +table tr.row-type--vendor:not(:first-child) td { + border-top: 1px solid #ddd; +} +`; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/utils.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/utils.ts new file mode 100644 index 000000000..4789e3bd3 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TransactionsByVendor/utils.ts @@ -0,0 +1,22 @@ + +import * as moment from 'moment'; + +export const getTransactionsByVendorDefaultQuery = () => { + return { + fromDate: moment().startOf('month').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: true, + vendorsIds: [], + }; +} \ No newline at end of file diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceExportInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceExportInjectable.ts new file mode 100644 index 000000000..2e65841bb --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceExportInjectable.ts @@ -0,0 +1,53 @@ +import { TrialBalanceSheetTableInjectable } from './TrialBalanceSheetTableInjectable'; +import { TrialBalanceSheetPdfInjectable } from './TrialBalanceSheetPdfInjectsable'; +import { Injectable } from '@nestjs/common'; +import { TableSheet } from '../../common/TableSheet'; +import { ITrialBalanceSheetQuery } from './TrialBalanceSheet.types'; + +@Injectable() +export class TrialBalanceExportInjectable { + constructor( + private readonly trialBalanceSheetTable: TrialBalanceSheetTableInjectable, + private readonly trialBalanceSheetPdf: TrialBalanceSheetPdfInjectable, + ) {} + + /** + * Retrieves the trial balance sheet in XLSX format. + * @param {number} tenantId + * @param {ITrialBalanceSheetQuery} query + * @returns {Promise} + */ + public async xlsx(query: ITrialBalanceSheetQuery) { + const table = await this.trialBalanceSheetTable.table(query); + + const tableSheet = new TableSheet(table.table); + const tableCsv = tableSheet.convertToXLSX(); + + return tableSheet.convertToBuffer(tableCsv, 'xlsx'); + } + + /** + * Retrieves the trial balance sheet in CSV format. + * @param {number} tenantId + * @param {ITrialBalanceSheetQuery} query + * @returns {Promise} + */ + public async csv(query: ITrialBalanceSheetQuery): Promise { + const table = await this.trialBalanceSheetTable.table(query); + + const tableSheet = new TableSheet(table.table); + const tableCsv = tableSheet.convertToCSV(); + + return tableCsv; + } + + /** + * Retrieves the trial balance sheet in PDF format. + * @param {number} tenantId + * @param {ITrialBalanceSheetQuery} query + * @returns {Promise} + */ + public async pdf(query: ITrialBalanceSheetQuery): Promise { + return this.trialBalanceSheetPdf.pdf(query); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheet.controller.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheet.controller.ts new file mode 100644 index 000000000..b2ce353ad --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheet.controller.ts @@ -0,0 +1,75 @@ +import { + Controller, + Get, + Headers, + Query, + Res, +} from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { castArray } from 'lodash'; +import { Response } from 'express'; +import { ITrialBalanceSheetQuery } from './TrialBalanceSheet.types'; +import { AcceptType } from '@/constants/accept-type'; +import { TrialBalanceSheetApplication } from './TrialBalanceSheetApplication'; +import { PublicRoute } from '@/modules/Auth/Jwt.guard'; + +@Controller('reports/trial-balance-sheet') +@ApiTags('reports') +@PublicRoute() +export class TrialBalanceSheetController { + constructor( + private readonly trialBalanceSheetApp: TrialBalanceSheetApplication, + ) {} + + @Get() + @ApiOperation({ summary: 'Get trial balance sheet' }) + @ApiResponse({ status: 200, description: 'Trial balance sheet' }) + async getTrialBalanceSheet( + @Query() query: ITrialBalanceSheetQuery, + @Res() res: Response, + @Headers('accept') acceptHeader: string, + ) { + const filter = { + ...query, + accountIds: castArray(query.accountIds), + }; + // Retrieves in json table format. + if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) { + const { table, meta, query } = await this.trialBalanceSheetApp.table( + filter, + ); + return res.status(200).send({ table, meta, query }); + // Retrieves in xlsx format + } else if (acceptHeader.includes(AcceptType.ApplicationXlsx)) { + const buffer = await this.trialBalanceSheetApp.xlsx(filter); + res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx'); + res.setHeader( + 'Content-Type', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ); + return res.send(buffer); + // Retrieves in csv format. + } else if (acceptHeader.includes(AcceptType.ApplicationCsv)) { + const buffer = await this.trialBalanceSheetApp.csv(filter); + + res.setHeader('Content-Disposition', 'attachment; filename=output.csv'); + res.setHeader('Content-Type', 'text/csv'); + + return res.send(buffer); + // Retrieves in pdf format. + } else if (acceptHeader.includes(AcceptType.ApplicationPdf)) { + const pdfContent = await this.trialBalanceSheetApp.pdf(filter); + res.set({ + 'Content-Type': 'application/pdf', + 'Content-Length': pdfContent.length, + }); + res.send(pdfContent); + // Retrieves in json format. + } else { + const { data, query, meta } = await this.trialBalanceSheetApp.sheet( + filter, + ); + return res.status(200).send({ data, query, meta }); + } + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheet.module.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheet.module.ts new file mode 100644 index 000000000..bfca7a955 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheet.module.ts @@ -0,0 +1,28 @@ +import { Module } from '@nestjs/common'; +import { TrialBalanceExportInjectable } from './TrialBalanceExportInjectable'; +import { TrialBalanceSheetController } from './TrialBalanceSheet.controller'; +import { TrialBalanceSheetApplication } from './TrialBalanceSheetApplication'; +import { TrialBalanceSheetService } from './TrialBalanceSheetInjectable'; +import { TrialBalanceSheetTableInjectable } from './TrialBalanceSheetTableInjectable'; +import { TrialBalanceSheetMeta } from './TrialBalanceSheetMeta'; +import { TrialBalanceSheetRepository } from './TrialBalanceSheetRepository'; +import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; +import { TrialBalanceSheetPdfInjectable } from './TrialBalanceSheetPdfInjectsable'; +import { FinancialSheetCommonModule } from '../../common/FinancialSheetCommon.module'; +import { AccountsModule } from '@/modules/Accounts/Accounts.module'; + +@Module({ + imports: [FinancialSheetCommonModule, AccountsModule], + providers: [ + TrialBalanceSheetApplication, + TrialBalanceSheetService, + TrialBalanceSheetTableInjectable, + TrialBalanceExportInjectable, + TrialBalanceSheetMeta, + TrialBalanceSheetRepository, + TenancyContext, + TrialBalanceSheetPdfInjectable + ], + controllers: [TrialBalanceSheetController], +}) +export class TrialBalanceSheetModule {} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheet.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheet.ts new file mode 100644 index 000000000..9110920fc --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheet.ts @@ -0,0 +1,262 @@ +import { sumBy } from 'lodash'; +import * as R from 'ramda'; +import { + ITrialBalanceSheetQuery, + ITrialBalanceAccount, + ITrialBalanceTotal, + ITrialBalanceSheetData, +} from './TrialBalanceSheet.types'; +import { TrialBalanceSheetRepository } from './TrialBalanceSheetRepository'; +import { FinancialSheet } from '../../common/FinancialSheet'; +import { Account } from '@/modules/Accounts/models/Account.model'; +import { allPassedConditionsPass } from '@/utils/all-conditions-passed'; +import { ModelObject } from 'objection'; +import { flatToNestedArray } from '@/utils/flat-to-nested-array'; + +export class TrialBalanceSheet extends FinancialSheet { + /** + * Trial balance sheet query. + * @param {ITrialBalanceSheetQuery} query + */ + public query: ITrialBalanceSheetQuery; + + /** + * Trial balance sheet repository. + * @param {TrialBalanceSheetRepository} + */ + public repository: TrialBalanceSheetRepository; + + /** + * Organization base currency. + * @param {string} + */ + public baseCurrency: string; + + /** + * Constructor method. + * @param {number} tenantId + * @param {ITrialBalanceSheetQuery} query + * @param {IAccount[]} accounts + * @param journalFinancial + */ + constructor( + query: ITrialBalanceSheetQuery, + repository: TrialBalanceSheetRepository, + baseCurrency: string + ) { + super(); + + this.query = query; + this.repository = repository; + this.numberFormat = this.query.numberFormat; + this.baseCurrency = baseCurrency; + } + + /** + * Retrieves the closing credit of the given account. + * @param {number} accountId + * @returns {number} + */ + public getClosingAccountCredit(accountId: number) { + const depsAccountsIds = + this.repository.accountsDepGraph.dependenciesOf(accountId); + + return this.repository.totalAccountsLedger + .whereAccountsIds([accountId, ...depsAccountsIds]) + .getClosingCredit(); + } + + /** + * Retrieves the closing debit of the given account. + * @param {number} accountId + * @returns {number} + */ + public getClosingAccountDebit(accountId: number) { + const depsAccountsIds = + this.repository.accountsDepGraph.dependenciesOf(accountId); + + return this.repository.totalAccountsLedger + .whereAccountsIds([accountId, ...depsAccountsIds]) + .getClosingDebit(); + } + + /** + * Retrieves the closing total of the given account. + * @param {number} accountId + * @returns {number} + */ + public getClosingAccountTotal(accountId: number) { + const credit = this.getClosingAccountCredit(accountId); + const debit = this.getClosingAccountDebit(accountId); + + return debit - credit; + } + + /** + * Account mapper. + * @param {IAccount} account + * @return {ITrialBalanceAccount} + */ + private accountTransformer = ( + account: Account + ): ITrialBalanceAccount => { + const debit = this.getClosingAccountDebit(account.id); + const credit = this.getClosingAccountCredit(account.id); + const balance = this.getClosingAccountTotal(account.id); + + return { + id: account.id, + parentAccountId: account.parentAccountId, + name: account.name, + formattedName: account.code + ? `${account.name} - ${account.code}` + : `${account.name}`, + code: account.code, + accountNormal: account.accountNormal, + + credit, + debit, + balance, + currencyCode: this.baseCurrency, + + formattedCredit: this.formatNumber(credit), + formattedDebit: this.formatNumber(debit), + formattedBalance: this.formatNumber(balance), + }; + }; + + /** + * Filters trial balance sheet accounts nodes based on the given report query. + * @param {ITrialBalanceAccount} accountNode + * @returns {boolean} + */ + private accountFilter = (accountNode: ITrialBalanceAccount): boolean => { + const { noneTransactions, noneZero, onlyActive } = this.query; + + // Conditions pair filter detarminer. + const condsPairFilters = [ + [noneTransactions, this.filterNoneTransactions], + [noneZero, this.filterNoneZero], + [onlyActive, this.filterActiveOnly], + ]; + return allPassedConditionsPass(condsPairFilters)(accountNode); + }; + + /** + * Fitlers the accounts nodes. + * @param {ITrialBalanceAccount[]} accountsNodes + * @returns {ITrialBalanceAccount[]} + */ + private accountsFilter = ( + accountsNodes: ITrialBalanceAccount[] + ): ITrialBalanceAccount[] => { + return accountsNodes.filter(this.accountFilter); + }; + + /** + * Mappes the given account object to trial balance account node. + * @param {IAccount[]} accountsNodes + * @returns {ITrialBalanceAccount[]} + */ + private accountsMapper = ( + accountsNodes: ModelObject[] + ): ITrialBalanceAccount[] => { + return accountsNodes.map(this.accountTransformer); + }; + + /** + * Detarmines whether the given account node is not none transactions. + * @param {ITrialBalanceAccount} accountNode + * @returns {boolean} + */ + private filterNoneTransactions = ( + accountNode: ITrialBalanceAccount + ): boolean => { + return false === this.repository.totalAccountsLedger.isEmpty(); + }; + + /** + * Detarmines whether the given account none zero. + * @param {ITrialBalanceAccount} accountNode + * @returns {boolean} + */ + private filterNoneZero = (accountNode: ITrialBalanceAccount): boolean => { + return accountNode.balance !== 0; + }; + + /** + * Detarmines whether the given account is active. + * @param {ITrialBalanceAccount} accountNode + * @returns {boolean} + */ + private filterActiveOnly = (accountNode: ITrialBalanceAccount): boolean => { + return accountNode.credit !== 0 || accountNode.debit !== 0; + }; + + /** + * Transformes the flatten nodes to nested nodes. + * @param {ITrialBalanceAccount[]} flattenAccounts + * @returns {ITrialBalanceAccount[]} + */ + private nestedAccountsNode = ( + flattenAccounts: ITrialBalanceAccount[] + ): ITrialBalanceAccount[] => { + return flatToNestedArray(flattenAccounts, { + id: 'id', + parentId: 'parentAccountId', + }); + }; + + /** + * Retrieve trial balance total section. + * @param {ITrialBalanceAccount[]} accountsBalances + * @return {ITrialBalanceTotal} + */ + private tatalSection( + accountsBalances: ITrialBalanceAccount[] + ): ITrialBalanceTotal { + const credit = sumBy(accountsBalances, 'credit'); + const debit = sumBy(accountsBalances, 'debit'); + const balance = sumBy(accountsBalances, 'balance'); + const currencyCode = this.baseCurrency; + + return { + credit, + debit, + balance, + currencyCode, + formattedCredit: this.formatTotalNumber(credit), + formattedDebit: this.formatTotalNumber(debit), + formattedBalance: this.formatTotalNumber(balance), + }; + } + + /** + * Retrieve accounts section of trial balance report. + * @param {IAccount[]} accounts + * @returns {ITrialBalanceAccount[]} + */ + private accountsSection(accounts: ModelObject[]) { + return R.compose( + this.nestedAccountsNode, + this.accountsFilter, + this.accountsMapper + )(accounts); + } + + /** + * Retrieve trial balance sheet statement data. + * Note: Retruns null in case there is no transactions between the given date periods. + * + * @return {ITrialBalanceSheetData} + */ + public reportData(): ITrialBalanceSheetData { + // Retrieve accounts nodes. + const accounts = this.accountsSection(this.repository.accounts); + + // Retrieve account node. + const total = this.tatalSection(accounts); + + return { accounts, total }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheet.types.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheet.types.ts new file mode 100644 index 000000000..5c663e1d9 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheet.types.ts @@ -0,0 +1,56 @@ +import { IFinancialSheetCommonMeta, INumberFormatQuery } from "../../types/Report.types"; +import { IFinancialTable } from "../../types/Table.types"; + +export interface ITrialBalanceSheetQuery { + fromDate: Date | string; + toDate: Date | string; + numberFormat: INumberFormatQuery; + basis: 'cash' | 'accrual'; + noneZero: boolean; + noneTransactions: boolean; + onlyActive: boolean; + accountIds: number[]; + branchesIds?: number[]; +} + +export interface ITrialBalanceTotal { + credit: number; + debit: number; + balance: number; + currencyCode: string; + + formattedCredit: string; + formattedDebit: string; + formattedBalance: string; +} + +export interface ITrialBalanceSheetMeta extends IFinancialSheetCommonMeta { + formattedFromDate: string; + formattedToDate: string; + formattedDateRange: string; +} + +export interface ITrialBalanceAccount extends ITrialBalanceTotal { + id: number; + parentAccountId: number; + name: string; + formattedName: string; + code: string; + accountNormal: string; +} + +export type ITrialBalanceSheetData = { + accounts: ITrialBalanceAccount[]; + total: ITrialBalanceTotal; +}; + +export interface ITrialBalanceStatement { + data: ITrialBalanceSheetData; + query: ITrialBalanceSheetQuery; + meta: ITrialBalanceSheetMeta; +} + +export interface ITrialBalanceSheetTable extends IFinancialTable { + meta: ITrialBalanceSheetMeta; + query: ITrialBalanceSheetQuery; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetApplication.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetApplication.ts new file mode 100644 index 000000000..e658a7b23 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetApplication.ts @@ -0,0 +1,61 @@ +import { TrialBalanceSheetTableInjectable } from './TrialBalanceSheetTableInjectable'; +import { TrialBalanceExportInjectable } from './TrialBalanceExportInjectable'; +import { ITrialBalanceSheetQuery, ITrialBalanceStatement } from './TrialBalanceSheet.types'; +import { TrialBalanceSheetService } from './TrialBalanceSheetInjectable'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class TrialBalanceSheetApplication { + constructor( + private readonly sheetService: TrialBalanceSheetService, + private readonly tablable: TrialBalanceSheetTableInjectable, + private readonly exportable: TrialBalanceExportInjectable, + ) {} + + /** + * Retrieves the trial balance sheet. + * @param {ITrialBalanceSheetQuery} query + * @returns {Promise} + */ + public sheet( + query: ITrialBalanceSheetQuery, + ): Promise { + return this.sheetService.trialBalanceSheet(query); + } + + /** + * Retrieves the trial balance sheet in table format. + * @param {ITrialBalanceSheetQuery} query + * @returns {Promise} + */ + public table(query: ITrialBalanceSheetQuery) { + return this.tablable.table(query); + } + + /** + * Retrieve the trial balance sheet in CSV format. + * @param {ITrialBalanceSheetQuery} query + * @returns {Promise} + */ + public csv(query: ITrialBalanceSheetQuery) { + return this.exportable.csv(query); + } + + /** + * Retrieve the trial balance sheet in XLSX format. + * @param {ITrialBalanceSheetQuery} query + * @returns {Promise} + */ + public async xlsx(query: ITrialBalanceSheetQuery) { + return this.exportable.xlsx(query); + } + + /** + * Retrieve the trial balance sheet in pdf format. + * @param {ITrialBalanceSheetQuery} query + * @returns {Promise} + */ + public async pdf(query: ITrialBalanceSheetQuery) { + return this.exportable.pdf(query); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetInjectable.ts new file mode 100644 index 000000000..7f1a2a290 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetInjectable.ts @@ -0,0 +1,68 @@ +import { + ITrialBalanceSheetQuery, + ITrialBalanceStatement, +} from './TrialBalanceSheet.types'; +import { TrialBalanceSheet } from './TrialBalanceSheet'; +import { TrialBalanceSheetRepository } from './TrialBalanceSheetRepository'; +import { TrialBalanceSheetMeta } from './TrialBalanceSheetMeta'; +import { getTrialBalanceSheetDefaultQuery } from './_utils'; +import { Injectable } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { events } from '@/common/events/events'; +import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; + +@Injectable() +export class TrialBalanceSheetService { + constructor( + private readonly trialBalanceSheetMetaService: TrialBalanceSheetMeta, + private readonly eventPublisher: EventEmitter2, + private readonly trialBalanceSheetRepository: TrialBalanceSheetRepository, + private readonly tenancyContext: TenancyContext, + ) {} + + /** + * Retrieve trial balance sheet statement. + * @param {ITrialBalanceSheetQuery} query - Trial balance sheet query. + * @return {ITrialBalanceStatement} + */ + public async trialBalanceSheet( + query: ITrialBalanceSheetQuery, + ): Promise { + const filter = { + ...getTrialBalanceSheetDefaultQuery(), + ...query, + }; + this.trialBalanceSheetRepository.setQuery(filter); + + const tenantMetadata = await this.tenancyContext.getTenantMetadata(); + + // Loads the resources. + await this.trialBalanceSheetRepository.asyncInitialize(); + + // Trial balance report instance. + const trialBalanceInstance = new TrialBalanceSheet( + filter, + this.trialBalanceSheetRepository, + tenantMetadata.baseCurrency, + ); + // Trial balance sheet data. + const trialBalanceSheetData = trialBalanceInstance.reportData(); + + // Trial balance sheet meta. + const meta = await this.trialBalanceSheetMetaService.meta(filter); + + // Triggers `onTrialBalanceSheetViewed` event. + await this.eventPublisher.emitAsync( + events.reports.onTrialBalanceSheetView, + { + query, + }, + ); + + return { + data: trialBalanceSheetData, + query: filter, + meta, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetMeta.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetMeta.ts new file mode 100644 index 000000000..92801143c --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetMeta.ts @@ -0,0 +1,33 @@ +import * as moment from 'moment'; +import { ITrialBalanceSheetMeta, ITrialBalanceSheetQuery } from './TrialBalanceSheet.types'; +import { Injectable } from '@nestjs/common'; +import { FinancialSheetMeta } from '../../common/FinancialSheetMeta'; +@Injectable() +export class TrialBalanceSheetMeta { + constructor(private readonly financialSheetMeta: FinancialSheetMeta) {} + + /** + * Retrieves the trial balance sheet meta. + * @param {ITrialBalanceSheetQuery} query + * @returns {Promise} + */ + public async meta( + query: ITrialBalanceSheetQuery + ): Promise { + const commonMeta = await this.financialSheetMeta.meta(); + + const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD'); + const formattedToDate = moment(query.toDate).format('YYYY/MM/DD'); + const formattedDateRange = `From ${formattedFromDate} to ${formattedToDate}`; + + const sheetName = 'Trial Balance Sheet'; + + return { + ...commonMeta, + sheetName, + formattedFromDate, + formattedToDate, + formattedDateRange, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetPdfInjectsable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetPdfInjectsable.ts new file mode 100644 index 000000000..cf55e5532 --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetPdfInjectsable.ts @@ -0,0 +1,29 @@ +import { TableSheetPdf } from '../../common/TableSheetPdf'; +import { ITrialBalanceSheetQuery } from './TrialBalanceSheet.types'; +import { TrialBalanceSheetTableInjectable } from './TrialBalanceSheetTableInjectable'; +import { HtmlTableCustomCss } from './_constants'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class TrialBalanceSheetPdfInjectable { + constructor( + private readonly trialBalanceSheetTable: TrialBalanceSheetTableInjectable, + private readonly tableSheetPdf: TableSheetPdf, + ) {} + + /** + * Converts the given trial balance sheet table to pdf. + * @param {ITrialBalanceSheetQuery} query - Trial balance sheet query. + * @returns {Promise} + */ + public async pdf(query: ITrialBalanceSheetQuery): Promise { + const table = await this.trialBalanceSheetTable.table(query); + + return this.tableSheetPdf.convertToPdf( + table.table, + table.meta.sheetName, + table.meta.formattedDateRange, + HtmlTableCustomCss, + ); + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetRepository.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetRepository.ts new file mode 100644 index 000000000..7ff33856b --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetRepository.ts @@ -0,0 +1,112 @@ +import { Knex } from 'knex'; +import { isEmpty } from 'lodash'; +import { ModelObject } from 'objection'; +import { Account } from '@/modules/Accounts/models/Account.model'; +import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model'; +import { Inject, Injectable, Scope } from '@nestjs/common'; +import { ITrialBalanceSheetQuery } from './TrialBalanceSheet.types'; +import { Ledger } from '@/modules/Ledger/Ledger'; +import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository'; + +@Injectable({ scope: Scope.TRANSIENT }) +export class TrialBalanceSheetRepository { + private query: ITrialBalanceSheetQuery; + + @Inject(Account.name) + private accountModel: typeof Account; + + @Inject(AccountTransaction.name) + private accountTransactionModel: typeof AccountTransaction; + + @Inject(AccountRepository) + private accountRepository: AccountRepository; + + public accountsDepGraph: any; + public accounts: Array>; + + /** + * Total closing accounts ledger. + * @param {Ledger} + */ + public totalAccountsLedger: Ledger; + + /** + * Set query. + * @param {ITrialBalanceSheetQuery} query + */ + public setQuery(query: ITrialBalanceSheetQuery) { + this.query = query; + } + + /** + * Async initialize. + * @returns {Promise} + */ + public asyncInitialize = async () => { + await this.initAccounts(); + await this.initAccountsClosingTotalLedger(); + }; + + // ---------------------------- + // # Accounts + // ---------------------------- + /** + * Initialize accounts. + * @returns {Promise} + */ + public initAccounts = async () => { + const accounts = await this.getAccounts(); + const accountsDepGraph = await this.accountRepository.getDependencyGraph(); + + this.accountsDepGraph = accountsDepGraph; + this.accounts = accounts; + }; + + /** + * Initialize all accounts closing total ledger. + * @return {Promise} + */ + public initAccountsClosingTotalLedger = async (): Promise => { + const totalByAccounts = await this.closingAccountsTotal(this.query.toDate); + + this.totalAccountsLedger = Ledger.fromTransactions(totalByAccounts); + }; + + /** + * Retrieve accounts of the report. + * @return {Promise} + */ + private getAccounts = () => { + return this.accountModel.query(); + }; + + /** + * Retrieve the opening balance transactions of the report. + * @param {Date|string} openingDate - + */ + public closingAccountsTotal = async (openingDate: Date | string) => { + return this.accountTransactionModel.query().onBuild((query) => { + query.sum('credit as credit'); + query.sum('debit as debit'); + query.groupBy('accountId'); + query.select(['accountId']); + + query.modify('filterDateRange', null, openingDate); + query.withGraphFetched('account'); + + // @ts-ignore + this.commonFilterBranchesQuery(query); + }); + }; + + /** + * Common branches filter query. + * @param {Knex.QueryBuilder} query + */ + private commonFilterBranchesQuery = (query: Knex.QueryBuilder) => { + if (!isEmpty(this.query.branchesIds)) { + // @ts-ignore + query.modify('filterByBranches', this.query.branchesIds); + } + }; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetTable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetTable.ts new file mode 100644 index 000000000..b8690b94d --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetTable.ts @@ -0,0 +1,151 @@ +import * as R from 'ramda'; +import { FinancialSheet } from '../../common/FinancialSheet'; +import { FinancialTable } from '../../common/FinancialTable'; +import { + ITrialBalanceAccount, + ITrialBalanceSheetData, + ITrialBalanceSheetQuery, + ITrialBalanceTotal, +} from './TrialBalanceSheet.types'; +import { + ITableColumn, + ITableColumnAccessor, + ITableRow, +} from '../../types/Table.types'; +import { FinancialSheetStructure } from '../../common/FinancialSheetStructure'; +import { I18nService } from 'nestjs-i18n'; +import { tableRowMapper } from '../../utils/Table.utils'; +import { IROW_TYPE } from './_constants'; + +export class TrialBalanceSheetTable extends R.compose( + FinancialTable, + FinancialSheetStructure, +)(FinancialSheet) { + /** + * Trial balance sheet data. + * @param {ITrialBalanceSheetData} + */ + public data: ITrialBalanceSheetData; + + /** + * Trial balance sheet query. + * @param {ITrialBalanceSheetQuery} + */ + public query: ITrialBalanceSheetQuery; + + public i18n: I18nService; + + /** + * Constructor method. + * @param {IBalanceSheetStatementData} reportData - + * @param {ITrialBalanceSheetQuery} query - + */ + constructor( + data: ITrialBalanceSheetData, + query: ITrialBalanceSheetQuery, + i18n: I18nService, + ) { + super(); + + this.data = data; + this.query = query; + this.i18n = i18n; + } + + /** + * Retrieve the common columns for all report nodes. + * @param {ITableColumnAccessor[]} + */ + private commonColumnsAccessors = (): ITableColumnAccessor[] => { + return [ + { key: 'account', accessor: 'formattedName' }, + { key: 'debit', accessor: 'formattedDebit' }, + { key: 'credit', accessor: 'formattedCredit' }, + { key: 'total', accessor: 'formattedBalance' }, + ]; + }; + + /** + * Maps the account node to table row. + * @param {ITrialBalanceAccount} node - + * @returns {ITableRow} + */ + private accountNodeTableRowsMapper = ( + node: ITrialBalanceAccount, + ): ITableRow => { + const columns = this.commonColumnsAccessors(); + const meta = { + rowTypes: [IROW_TYPE.ACCOUNT], + id: node.id, + }; + return tableRowMapper(node, columns, meta); + }; + + /** + * Maps the total node to table row. + * @param {ITrialBalanceTotal} node - + * @returns {ITableRow} + */ + private totalNodeTableRowsMapper = (node: ITrialBalanceTotal): ITableRow => { + const columns = this.commonColumnsAccessors(); + const meta = { + rowTypes: [IROW_TYPE.TOTAL], + id: 'total', + }; + return tableRowMapper(node, columns, meta); + }; + + /** + * Mappes the given report sections to table rows. + * @param {IBalanceSheetDataNode[]} nodes - + * @return {ITableRow} + */ + private accountsToTableRowsMap = ( + nodes: ITrialBalanceAccount[], + ): ITableRow[] => { + return this.mapNodesDeep(nodes, this.accountNodeTableRowsMapper); + }; + + /** + * Retrieves the accounts table rows of the given report data. + * @returns {ITableRow[]} + */ + private accountsTableRows = (): ITableRow[] => { + return this.accountsToTableRowsMap(this.data.accounts); + }; + + /** + * Maps the given total node to table row. + * @returns {ITableRow} + */ + private totalTableRow = (): ITableRow => { + return this.totalNodeTableRowsMapper(this.data.total); + }; + + /** + * Retrieves the table rows. + * @returns {ITableRow[]} + */ + public tableRows = (): ITableRow[] => { + return R.compose( + R.unless(R.isEmpty, R.append(this.totalTableRow())), + R.concat(this.accountsTableRows()), + )([]); + }; + + /** + * Retrrieves the table columns. + * @returns {ITableColumn[]} + */ + public tableColumns = (): ITableColumn[] => { + return R.compose( + this.tableColumnsCellIndexing, + R.concat([ + { key: 'account', label: 'Account' }, + { key: 'debit', label: 'Debit' }, + { key: 'credit', label: 'Credit' }, + { key: 'total', label: 'Total' }, + ]), + )([]); + }; +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetTableInjectable.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetTableInjectable.ts new file mode 100644 index 000000000..6d5aa6e8c --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/TrialBalanceSheetTableInjectable.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common'; +import { + ITrialBalanceSheetQuery, + ITrialBalanceSheetTable, +} from './TrialBalanceSheet.types'; +import { TrialBalanceSheetTable } from './TrialBalanceSheetTable'; +import { TrialBalanceSheetService } from './TrialBalanceSheetInjectable'; +import { I18nService } from 'nestjs-i18n'; + +@Injectable() +export class TrialBalanceSheetTableInjectable { + constructor( + private readonly sheet: TrialBalanceSheetService, + private readonly i18n: I18nService, + ) {} + + /** + * Retrieves the trial balance sheet table. + * @param {ITrialBalanceSheetQuery} query + * @returns {Promise} + */ + public async table( + query: ITrialBalanceSheetQuery, + ): Promise { + const trialBalance = await this.sheet.trialBalanceSheet(query); + const table = new TrialBalanceSheetTable( + trialBalance.data, + query, + this.i18n, + ); + + return { + table: { + columns: table.tableColumns(), + rows: table.tableRows(), + }, + meta: trialBalance.meta, + query: trialBalance.query, + }; + } +} diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/_constants.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/_constants.ts new file mode 100644 index 000000000..7a52e631b --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/_constants.ts @@ -0,0 +1,25 @@ +export enum IROW_TYPE { + ACCOUNT = 'ACCOUNT', + TOTAL = 'TOTAL', +} + +export const HtmlTableCustomCss = ` +table tr.row-type--total td{ + border-top: 1px solid #bbb; + font-weight: 600; + border-bottom: 3px double #000; +} + +table .column--account { + width: 400px; +} + +table .column--debit, +table .column--credit, +table .column--total, +table .cell--debit, +table .cell--credit, +table .cell--total{ + text-align: right; +} +`; diff --git a/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/_utils.ts b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/_utils.ts new file mode 100644 index 000000000..075a2e5dd --- /dev/null +++ b/packages/server-nest/src/modules/FinancialStatements/modules/TrialBalanceSheet/_utils.ts @@ -0,0 +1,18 @@ +import * as moment from 'moment'; + +export const getTrialBalanceSheetDefaultQuery = () => ({ + fromDate: moment().startOf('year').format('YYYY-MM-DD'), + toDate: moment().format('YYYY-MM-DD'), + numberFormat: { + divideOn1000: false, + negativeFormat: 'mines', + showZero: false, + formatMoney: 'total', + precision: 2, + }, + basis: 'accrual', + noneZero: false, + noneTransactions: true, + onlyActive: false, + accountIds: [], +}); diff --git a/packages/server-nest/src/modules/FinancialStatements/types/Table.types.ts b/packages/server-nest/src/modules/FinancialStatements/types/Table.types.ts index 99fef5dbd..1fe8f3552 100644 --- a/packages/server-nest/src/modules/FinancialStatements/types/Table.types.ts +++ b/packages/server-nest/src/modules/FinancialStatements/types/Table.types.ts @@ -1,6 +1,6 @@ export interface IColumnMapperMeta { key: string; - accessor?: string; + accessor?: string | ((value: any) => string); value?: string; } @@ -11,7 +11,7 @@ export interface ITableCell { export type ITableRow = { cells: ITableCell[]; - rowTypes?: Array + rowTypes?: Array; id?: string; }; @@ -42,7 +42,7 @@ export interface IFinancialTable { } export interface IFinancialTableTotal { - amount: number; - formattedAmount: string; - currencyCode: string; -} \ No newline at end of file + amount: number; + formattedAmount: string; + currencyCode: string; +}