diff --git a/packages/server-nest/src/modules/Accounts/Accounts.module.ts b/packages/server-nest/src/modules/Accounts/Accounts.module.ts index 49bc23240..86f40b5e7 100644 --- a/packages/server-nest/src/modules/Accounts/Accounts.module.ts +++ b/packages/server-nest/src/modules/Accounts/Accounts.module.ts @@ -33,5 +33,8 @@ import { GetAccountTransactionsService } from './GetAccountTransactions.service' GetAccountTypesService, GetAccountTransactionsService, ], + exports: [ + AccountRepository + ] }) export class AccountsModule {} 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 9ae4f9c6b..68f995993 100644 --- a/packages/server-nest/src/modules/Accounts/models/AccountTransaction.model.ts +++ b/packages/server-nest/src/modules/Accounts/models/AccountTransaction.model.ts @@ -1,20 +1,37 @@ import { Model, raw } from 'objection'; -import moment from 'moment'; +import moment, { unitOfTime } from 'moment'; import { isEmpty, castArray } from 'lodash'; import { BaseModel } from '@/models/Model'; +import { Account } from './Account.model'; // import { getTransactionTypeLabel } from '@/utils/transactions-types'; export class AccountTransaction extends BaseModel { referenceType: string; referenceId: number; + accountId: number; + contactId: number; credit: number; debit: number; exchangeRate: number; taxRate: number; - date: string; + date: Date | string; transactionType: string; currencyCode: string; referenceTypeFormatted: string; + transactionNumber!: string; + referenceNumber!: string; + note!: string; + + index!: number; + indexGroup!: number; + + taxRateId!: number; + + branchId!: number; + userId!: number; + itemId!: number; + projectId!: number; + account: Account; /** * Table name @@ -61,153 +78,184 @@ export class AccountTransaction extends BaseModel { // return getTransactionTypeLabel(this.referenceType, this.transactionType); // } - // /** - // * Model modifiers. - // */ - // static get modifiers() { - // return { - // /** - // * Filters accounts by the given ids. - // * @param {Query} query - // * @param {number[]} accountsIds - // */ - // filterAccounts(query, accountsIds) { - // if (Array.isArray(accountsIds) && accountsIds.length > 0) { - // query.whereIn('account_id', accountsIds); - // } - // }, - // filterTransactionTypes(query, types) { - // if (Array.isArray(types) && types.length > 0) { - // query.whereIn('reference_type', types); - // } else if (typeof types === 'string') { - // query.where('reference_type', types); - // } - // }, - // filterDateRange(query, startDate, endDate, type = 'day') { - // const dateFormat = 'YYYY-MM-DD'; - // const fromDate = moment(startDate).startOf(type).format(dateFormat); - // const toDate = moment(endDate).endOf(type).format(dateFormat); + /** + * Model modifiers. + */ + static get modifiers() { + return { + /** + * Filters accounts by the given ids. + * @param {Query} query + * @param {number[]} accountsIds + */ + filterAccounts(query, accountsIds) { + if (Array.isArray(accountsIds) && accountsIds.length > 0) { + query.whereIn('account_id', accountsIds); + } + }, - // if (startDate) { - // query.where('date', '>=', fromDate); - // } - // if (endDate) { - // query.where('date', '<=', toDate); - // } - // }, - // filterAmountRange(query, fromAmount, toAmount) { - // if (fromAmount) { - // query.andWhere((q) => { - // q.where('credit', '>=', fromAmount); - // q.orWhere('debit', '>=', fromAmount); - // }); - // } - // if (toAmount) { - // query.andWhere((q) => { - // q.where('credit', '<=', toAmount); - // q.orWhere('debit', '<=', toAmount); - // }); - // } - // }, - // sumationCreditDebit(query) { - // query.select(['accountId']); + /** + * Filters the transaction types. + * @param {Query} query + * @param {string[]} types + */ + filterTransactionTypes(query, types) { + if (Array.isArray(types) && types.length > 0) { + query.whereIn('reference_type', types); + } else if (typeof types === 'string') { + query.where('reference_type', types); + } + }, - // query.sum('credit as credit'); - // query.sum('debit as debit'); - // query.groupBy('account_id'); - // }, - // filterContactType(query, contactType) { - // query.where('contact_type', contactType); - // }, - // filterContactIds(query, contactIds) { - // query.whereIn('contact_id', contactIds); - // }, - // openingBalance(query, fromDate) { - // query.modify('filterDateRange', null, fromDate); - // query.modify('sumationCreditDebit'); - // }, - // closingBalance(query, toDate) { - // query.modify('filterDateRange', null, toDate); - // query.modify('sumationCreditDebit'); - // }, - // contactsOpeningBalance( - // query, - // openingDate, - // receivableAccounts, - // customersIds - // ) { - // // Filter by date. - // query.modify('filterDateRange', null, openingDate); + /** + * Filters the date range. + * @param {Query} query + * @param {moment.MomentInput} startDate + * @param {moment.MomentInput} endDate + * @param {unitOfTime.StartOf} type + */ + filterDateRange( + query, + startDate: moment.MomentInput, + endDate: moment.MomentInput, + type: unitOfTime.StartOf = 'day', + ) { + const dateFormat = 'YYYY-MM-DD'; + const fromDate = moment(startDate).startOf(type).format(dateFormat); + const toDate = moment(endDate).endOf(type).format(dateFormat); - // // Filter by customers. - // query.whereNot('contactId', null); - // query.whereIn('accountId', castArray(receivableAccounts)); + if (startDate) { + query.where('date', '>=', fromDate); + } + if (endDate) { + query.where('date', '<=', toDate); + } + }, - // if (!isEmpty(customersIds)) { - // query.whereIn('contactId', castArray(customersIds)); - // } - // // Group by the contact transactions. - // query.groupBy('contactId'); - // query.sum('credit as credit'); - // query.sum('debit as debit'); - // query.select('contactId'); - // }, - // creditDebitSummation(query) { - // query.sum('credit as credit'); - // query.sum('debit as debit'); - // }, - // groupByDateFormat(query, groupType = 'month') { - // const groupBy = { - // day: '%Y-%m-%d', - // month: '%Y-%m', - // year: '%Y', - // }; - // const dateFormat = groupBy[groupType]; + /** + * Filters the amount range. + * @param {Query} query + * @param {number} fromAmount + * @param {number} toAmount + */ + filterAmountRange(query, fromAmount, toAmount) { + if (fromAmount) { + query.andWhere((q) => { + q.where('credit', '>=', fromAmount); + q.orWhere('debit', '>=', fromAmount); + }); + } + if (toAmount) { + query.andWhere((q) => { + q.where('credit', '<=', toAmount); + q.orWhere('debit', '<=', toAmount); + }); + } + }, + sumationCreditDebit(query) { + query.select(['accountId']); - // query.select(raw(`DATE_FORMAT(DATE, '${dateFormat}')`).as('date')); - // query.groupByRaw(`DATE_FORMAT(DATE, '${dateFormat}')`); - // }, + query.sum('credit as credit'); + query.sum('debit as debit'); + query.groupBy('account_id'); + }, + filterContactType(query, contactType) { + query.where('contact_type', contactType); + }, + filterContactIds(query, contactIds) { + query.whereIn('contact_id', contactIds); + }, + openingBalance(query, fromDate) { + query.modify('filterDateRange', null, fromDate); + query.modify('sumationCreditDebit'); + }, + closingBalance(query, toDate) { + query.modify('filterDateRange', null, toDate); + query.modify('sumationCreditDebit'); + }, + contactsOpeningBalance( + query, + openingDate, + receivableAccounts, + customersIds, + ) { + // Filter by date. + query.modify('filterDateRange', null, openingDate); - // filterByBranches(query, branchesIds) { - // const formattedBranchesIds = castArray(branchesIds); + // Filter by customers. + query.whereNot('contactId', null); + query.whereIn('accountId', castArray(receivableAccounts)); - // query.whereIn('branchId', formattedBranchesIds); - // }, + if (!isEmpty(customersIds)) { + query.whereIn('contactId', castArray(customersIds)); + } + // Group by the contact transactions. + query.groupBy('contactId'); + query.sum('credit as credit'); + query.sum('debit as debit'); + query.select('contactId'); + }, + creditDebitSummation(query) { + query.sum('credit as credit'); + query.sum('debit as debit'); + }, + groupByDateFormat(query, groupType = 'month') { + const groupBy = { + day: '%Y-%m-%d', + month: '%Y-%m', + year: '%Y', + }; + const dateFormat = groupBy[groupType]; - // filterByProjects(query, projectsIds) { - // const formattedProjectsIds = castArray(projectsIds); + query.select(raw(`DATE_FORMAT(DATE, '${dateFormat}')`).as('date')); + query.groupByRaw(`DATE_FORMAT(DATE, '${dateFormat}')`); + }, - // query.whereIn('projectId', formattedProjectsIds); - // }, - // }; - // } + filterByBranches(query, branchesIds) { + const formattedBranchesIds = castArray(branchesIds); - // /** - // * Relationship mapping. - // */ - // static get relationMappings() { - // const Account = require('models/Account'); - // const Contact = require('models/Contact'); + query.whereIn('branchId', formattedBranchesIds); + }, - // return { - // account: { - // relation: Model.BelongsToOneRelation, - // modelClass: Account.default, - // join: { - // from: 'accounts_transactions.accountId', - // to: 'accounts.id', - // }, - // }, - // contact: { - // relation: Model.BelongsToOneRelation, - // modelClass: Contact.default, - // join: { - // from: 'accounts_transactions.contactId', - // to: 'contacts.id', - // }, - // }, - // }; - // } + filterByProjects(query, projectsIds) { + const formattedProjectsIds = castArray(projectsIds); + + query.whereIn('projectId', formattedProjectsIds); + }, + + filterByReference(query, referenceId: number, referenceType: string) { + query.where('reference_id', referenceId); + query.where('reference_type', referenceType); + }, + }; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const { Account } = require('./Account.model'); + const { Contact } = require('../../Contacts/models/Contact'); + + return { + account: { + relation: Model.BelongsToOneRelation, + modelClass: Account, + join: { + from: 'accounts_transactions.accountId', + to: 'accounts.id', + }, + }, + contact: { + relation: Model.BelongsToOneRelation, + modelClass: Contact, + join: { + from: 'accounts_transactions.contactId', + to: 'contacts.id', + }, + }, + }; + } /** * Prevents mutate base currency since the model is not empty. diff --git a/packages/server-nest/src/modules/Accounts/repositories/Account.repository.ts b/packages/server-nest/src/modules/Accounts/repositories/Account.repository.ts index 3d7dbe33b..0ce448ddd 100644 --- a/packages/server-nest/src/modules/Accounts/repositories/Account.repository.ts +++ b/packages/server-nest/src/modules/Accounts/repositories/Account.repository.ts @@ -3,19 +3,26 @@ import { Inject, Injectable, Scope } from '@nestjs/common'; import { TenantRepository } from '@/common/repository/TenantRepository'; import { TENANCY_DB_CONNECTION } from '@/modules/Tenancy/TenancyDB/TenancyDB.constants'; import { Account } from '../models/Account.model'; -// import { TenantMetadata } from '@/modules/System/models/TenantMetadataModel'; -// import { IAccount } from '../Accounts.types'; -// import { -// PrepardExpenses, -// StripeClearingAccount, -// TaxPayableAccount, -// UnearnedRevenueAccount, -// } from '../Accounts.constants'; +import { I18nService } from 'nestjs-i18n'; +import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; +import { + PrepardExpenses, + StripeClearingAccount, + TaxPayableAccount, + UnearnedRevenueAccount, +} from '../Accounts.constants'; @Injectable({ scope: Scope.REQUEST }) export class AccountRepository extends TenantRepository { - @Inject(TENANCY_DB_CONNECTION) - private readonly tenantDBKnex: Knex; + constructor( + private readonly i18n: I18nService, + private readonly tenancyContext: TenancyContext, + + @Inject(TENANCY_DB_CONNECTION) + private readonly tenantDBKnex: Knex, + ) { + super(); + } /** * Gets the repository's model. @@ -107,185 +114,274 @@ export class AccountRepository extends TenantRepository { return results; } - // /** - // * - // * @param {string} currencyCode - // * @param extraAttrs - // * @param trx - // * @returns - // */ - // findOrCreateAccountReceivable = async ( - // currencyCode: string = '', - // extraAttrs = {}, - // trx?: Knex.Transaction, - // ) => { - // let result = await this.model - // .query(trx) - // .onBuild((query) => { - // if (currencyCode) { - // query.where('currencyCode', currencyCode); - // } - // query.where('accountType', 'accounts-receivable'); - // }) - // .first(); + /** + * + * @param {string} currencyCode + * @param extraAttrs + * @param trx + * @returns + */ + findOrCreateAccountReceivable = async ( + currencyCode: string = '', + extraAttrs = {}, + trx?: Knex.Transaction, + ) => { + let result = await this.model + .query(trx) + .onBuild((query) => { + if (currencyCode) { + query.where('currencyCode', currencyCode); + } + query.where('accountType', 'accounts-receivable'); + }) + .first(); - // if (!result) { - // result = await this.model.query(trx).insertAndFetch({ - // name: this.i18n.__('account.accounts_receivable.currency', { - // currency: currencyCode, - // }), - // accountType: 'accounts-receivable', - // currencyCode, - // active: 1, - // ...extraAttrs, - // }); - // } - // return result; - // }; + if (!result) { + result = await this.model.query(trx).insertAndFetch({ + name: this.i18n.t('account.accounts_receivable.currency', { + args: { currency: currencyCode }, + }), + accountType: 'accounts-receivable', + currencyCode, + active: 1, + ...extraAttrs, + }); + } + return result; + }; - // /** - // * Find or create tax payable account. - // * @param {Record}extraAttrs - // * @param {Knex.Transaction} trx - // * @returns - // */ - // async findOrCreateTaxPayable( - // extraAttrs: Record = {}, - // trx?: Knex.Transaction, - // ) { - // let result = await this.model - // .query(trx) - // .findOne({ slug: TaxPayableAccount.slug, ...extraAttrs }); + /** + * Find or create tax payable account. + * @param {Record}extraAttrs + * @param {Knex.Transaction} trx + * @returns + */ + async findOrCreateTaxPayable( + extraAttrs: Record = {}, + trx?: Knex.Transaction, + ) { + let result = await this.model + .query(trx) + .findOne({ slug: TaxPayableAccount.slug, ...extraAttrs }); - // if (!result) { - // result = await this.model.query(trx).insertAndFetch({ - // ...TaxPayableAccount, - // ...extraAttrs, - // }); - // } - // return result; - // } + if (!result) { + result = await this.model.query(trx).insertAndFetch({ + ...TaxPayableAccount, + ...extraAttrs, + }); + } + return result; + } - // findOrCreateAccountsPayable = async ( - // currencyCode: string = '', - // extraAttrs = {}, - // trx?: Knex.Transaction, - // ) => { - // let result = await this.model - // .query(trx) - // .onBuild((query) => { - // if (currencyCode) { - // query.where('currencyCode', currencyCode); - // } - // query.where('accountType', 'accounts-payable'); - // }) - // .first(); + findOrCreateAccountsPayable = async ( + currencyCode: string = '', + extraAttrs = {}, + trx?: Knex.Transaction, + ) => { + let result = await this.model + .query(trx) + .onBuild((query) => { + if (currencyCode) { + query.where('currencyCode', currencyCode); + } + query.where('accountType', 'accounts-payable'); + }) + .first(); - // if (!result) { - // result = await this.model.query(trx).insertAndFetch({ - // name: this.i18n.__('account.accounts_payable.currency', { - // currency: currencyCode, - // }), - // accountType: 'accounts-payable', - // currencyCode, - // active: 1, - // ...extraAttrs, - // }); - // } - // return result; - // }; + if (!result) { + result = await this.model.query(trx).insertAndFetch({ + name: this.i18n.t('account.accounts_payable.currency', { + args: { currency: currencyCode }, + }), + accountType: 'accounts-payable', + currencyCode, + active: 1, + ...extraAttrs, + }); + } + return result; + }; - // /** - // * Finds or creates the unearned revenue. - // * @param {Record} extraAttrs - // * @param {Knex.Transaction} trx - // * @returns - // */ - // public async findOrCreateUnearnedRevenue( - // extraAttrs: Record = {}, - // trx?: Knex.Transaction, - // ) { - // // Retrieves the given tenant metadata. - // const tenantMeta = await TenantMetadata.query().findOne({ - // tenantId: this.tenantId, - // }); - // const _extraAttrs = { - // currencyCode: tenantMeta.baseCurrency, - // ...extraAttrs, - // }; - // let result = await this.model - // .query(trx) - // .findOne({ slug: UnearnedRevenueAccount.slug, ..._extraAttrs }); + /** + * Finds or creates the unearned revenue. + * @param {Record} extraAttrs + * @param {Knex.Transaction} trx + * @returns + */ + public async findOrCreateUnearnedRevenue( + extraAttrs: Record = {}, + trx?: Knex.Transaction, + ) { + const tenantMeta = await this.tenancyContext.getTenantMetadata(); + const _extraAttrs = { + currencyCode: tenantMeta.baseCurrency, + ...extraAttrs, + }; + let result = await this.model + .query(trx) + .findOne({ slug: UnearnedRevenueAccount.slug, ..._extraAttrs }); - // if (!result) { - // result = await this.model.query(trx).insertAndFetch({ - // ...UnearnedRevenueAccount, - // ..._extraAttrs, - // }); - // } - // return result; - // } + if (!result) { + result = await this.model.query(trx).insertAndFetch({ + ...UnearnedRevenueAccount, + ..._extraAttrs, + }); + } + return result; + } - // /** - // * Finds or creates the prepard expenses account. - // * @param {Record} extraAttrs - // * @param {Knex.Transaction} trx - // * @returns - // */ - // public async findOrCreatePrepardExpenses( - // extraAttrs: Record = {}, - // trx?: Knex.Transaction, - // ) { - // // Retrieves the given tenant metadata. - // const tenantMeta = await TenantMetadata.query().findOne({ - // tenantId: this.tenantId, - // }); - // const _extraAttrs = { - // currencyCode: tenantMeta.baseCurrency, - // ...extraAttrs, - // }; + /** + * Finds or creates the prepard expenses account. + * @param {Record} extraAttrs + * @param {Knex.Transaction} trx + * @returns + */ + public async findOrCreatePrepardExpenses( + extraAttrs: Record = {}, + trx?: Knex.Transaction, + ) { + const tenantMeta = await this.tenancyContext.getTenantMetadata(); + const _extraAttrs = { + currencyCode: tenantMeta.baseCurrency, + ...extraAttrs, + }; - // let result = await this.model - // .query(trx) - // .findOne({ slug: PrepardExpenses.slug, ..._extraAttrs }); + let result = await this.model + .query(trx) + .findOne({ slug: PrepardExpenses.slug, ..._extraAttrs }); - // if (!result) { - // result = await this.model.query(trx).insertAndFetch({ - // ...PrepardExpenses, - // ..._extraAttrs, - // }); - // } - // return result; - // } + if (!result) { + result = await this.model.query(trx).insertAndFetch({ + ...PrepardExpenses, + ..._extraAttrs, + }); + } + return result; + } - // /** - // * Finds or creates the stripe clearing account. - // * @param {Record} extraAttrs - // * @param {Knex.Transaction} trx - // * @returns - // */ - // public async findOrCreateStripeClearing( - // extraAttrs: Record = {}, - // trx?: Knex.Transaction, - // ) { - // // Retrieves the given tenant metadata. - // const tenantMeta = await TenantMetadata.query().findOne({ - // tenantId: this.tenantId, - // }); - // const _extraAttrs = { - // currencyCode: tenantMeta.baseCurrency, - // ...extraAttrs, - // }; - // let result = await this.model - // .query(trx) - // .findOne({ slug: StripeClearingAccount.slug, ..._extraAttrs }); + /** + * Finds or creates the stripe clearing account. + * @param {Record} extraAttrs + * @param {Knex.Transaction} trx + * @returns + */ + public async findOrCreateStripeClearing( + extraAttrs: Record = {}, + trx?: Knex.Transaction, + ) { + const tenantMeta = await this.tenancyContext.getTenantMetadata(); + const _extraAttrs = { + currencyCode: tenantMeta.baseCurrency, + ...extraAttrs, + }; + let result = await this.model + .query(trx) + .findOne({ slug: StripeClearingAccount.slug, ..._extraAttrs }); - // if (!result) { - // result = await this.model.query(trx).insertAndFetch({ - // ...StripeClearingAccount, - // ..._extraAttrs, - // }); - // } - // return result; - // } + if (!result) { + result = await this.model.query(trx).insertAndFetch({ + ...StripeClearingAccount, + ..._extraAttrs, + }); + } + return result; + } + + /** + * Finds or creates the discount expense account. + * @param {Record} extraAttrs + * @param {Knex.Transaction} trx + * @returns + */ + public async findOrCreateDiscountAccount( + extraAttrs: Record = {}, + trx?: Knex.Transaction, + ) { + const tenantMeta = await this.tenancyContext.getTenantMetadata(); + const _extraAttrs = { + currencyCode: tenantMeta.baseCurrency, + ...extraAttrs, + }; + + let result = await this.model + .query(trx) + .findOne({ slug: DiscountExpenseAccount.slug, ..._extraAttrs }); + + if (!result) { + result = await this.model.query(trx).insertAndFetch({ + ...DiscountExpenseAccount, + ..._extraAttrs, + }); + } + return result; + } + + public async findOrCreatePurchaseDiscountAccount( + extraAttrs: Record = {}, + trx?: Knex.Transaction, + ) { + const tenantMeta = await this.tenancyContext.getTenantMetadata(); + const _extraAttrs = { + currencyCode: tenantMeta.baseCurrency, + ...extraAttrs, + }; + + let result = await this.model + .query(trx) + .findOne({ slug: PurchaseDiscountAccount.slug, ..._extraAttrs }); + + if (!result) { + result = await this.model.query(trx).insertAndFetch({ + ...PurchaseDiscountAccount, + ..._extraAttrs, + }); + } + return result; + } + + public async findOrCreateOtherChargesAccount( + extraAttrs: Record = {}, + trx?: Knex.Transaction, + ) { + const tenantMeta = await this.tenancyContext.getTenantMetadata(); + const _extraAttrs = { + currencyCode: tenantMeta.baseCurrency, + ...extraAttrs, + }; + + let result = await this.model + .query(trx) + .findOne({ slug: OtherChargesAccount.slug, ..._extraAttrs }); + + if (!result) { + result = await this.model.query(trx).insertAndFetch({ + ...OtherChargesAccount, + ..._extraAttrs, + }); + } + return result; + } + + public async findOrCreateOtherExpensesAccount( + extraAttrs: Record = {}, + trx?: Knex.Transaction, + ) { + const tenantMeta = await this.tenancyContext.getTenantMetadata(); + const _extraAttrs = { + currencyCode: tenantMeta.baseCurrency, + ...extraAttrs, + }; + + let result = await this.model + .query(trx) + .findOne({ slug: OtherExpensesAccount.slug, ..._extraAttrs }); + + if (!result) { + result = await this.model.query(trx).insertAndFetch({ + ...OtherExpensesAccount, + ..._extraAttrs, + }); + } + return result; + } } diff --git a/packages/server-nest/src/modules/App/App.module.ts b/packages/server-nest/src/modules/App/App.module.ts index 3a82f702b..f95c76ef5 100644 --- a/packages/server-nest/src/modules/App/App.module.ts +++ b/packages/server-nest/src/modules/App/App.module.ts @@ -53,6 +53,7 @@ import { VendorCreditsRefundModule } from '../VendorCreditsRefund/VendorCreditsR import { CreditNoteRefundsModule } from '../CreditNoteRefunds/CreditNoteRefunds.module'; import { BillPaymentsModule } from '../BillPayments/BillPayments.module'; import { PaymentsReceivedModule } from '../PaymentReceived/PaymentsReceived.module'; +import { LedgerModule } from '../Ledger/Ledger.module'; @Module({ imports: [ @@ -130,6 +131,7 @@ import { PaymentsReceivedModule } from '../PaymentReceived/PaymentsReceived.modu CreditNoteRefundsModule, BillPaymentsModule, PaymentsReceivedModule, + LedgerModule, ], controllers: [AppController], providers: [ diff --git a/packages/server-nest/src/modules/BillPayments/commands/BillPaymentGL.ts b/packages/server-nest/src/modules/BillPayments/commands/BillPaymentGL.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/server-nest/src/modules/Bills/Bills.module.ts b/packages/server-nest/src/modules/Bills/Bills.module.ts index a55f49285..c945c71c4 100644 --- a/packages/server-nest/src/modules/Bills/Bills.module.ts +++ b/packages/server-nest/src/modules/Bills/Bills.module.ts @@ -17,6 +17,7 @@ import { ItemEntriesTaxTransactions } from '../TaxRates/ItemEntriesTaxTransactio import { TenancyContext } from '../Tenancy/TenancyContext.service'; import { BillsController } from './Bills.controller'; import { BillLandedCostsModule } from '../BillLandedCosts/BillLandedCosts.module'; +import { BillGLEntriesSubscriber } from './subscribers/BillGLEntriesSubscriber'; @Module({ imports: [BillLandedCostsModule], @@ -36,7 +37,8 @@ import { BillLandedCostsModule } from '../BillLandedCosts/BillLandedCosts.module DeleteBill, BillDTOTransformer, BillsValidators, - ItemsEntriesService + ItemsEntriesService, + BillGLEntriesSubscriber ], controllers: [BillsController], }) diff --git a/packages/server-nest/src/modules/Bills/commands/BillGLEntries.ts b/packages/server-nest/src/modules/Bills/commands/BillGLEntries.ts deleted file mode 100644 index 2a20814ad..000000000 --- a/packages/server-nest/src/modules/Bills/commands/BillGLEntries.ts +++ /dev/null @@ -1,288 +0,0 @@ -// import moment from 'moment'; -// import { sumBy } from 'lodash'; -// import { Knex } from 'knex'; -// import { Inject, Service } from 'typedi'; -// import * as R from 'ramda'; -// import { AccountNormal, IBill, IItemEntry, ILedgerEntry } from '@/interfaces'; -// import HasTenancyService from '@/services/Tenancy/TenancyService'; -// import Ledger from '@/services/Accounting/Ledger'; -// import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; -// import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; - -// @Service() -// export class BillGLEntries { -// @Inject() -// private tenancy: HasTenancyService; - -// @Inject() -// private ledgerStorage: LedgerStorageService; - -// @Inject() -// private itemsEntriesService: ItemsEntriesService; - -// /** -// * Creates bill GL entries. -// * @param {number} tenantId - -// * @param {number} billId - -// * @param {Knex.Transaction} trx - -// */ -// public writeBillGLEntries = async ( -// tenantId: number, -// billId: number, -// trx?: Knex.Transaction -// ) => { -// const { accountRepository } = this.tenancy.repositories(tenantId); -// const { Bill } = this.tenancy.models(tenantId); - -// // Retrieves bill with associated entries and landed costs. -// const bill = await Bill.query(trx) -// .findById(billId) -// .withGraphFetched('entries.item') -// .withGraphFetched('entries.allocatedCostEntries') -// .withGraphFetched('locatedLandedCosts.allocateEntries'); - -// // Finds or create a A/P account based on the given currency. -// const APAccount = await accountRepository.findOrCreateAccountsPayable( -// bill.currencyCode, -// {}, -// trx -// ); -// // Find or create tax payable account. -// const taxPayableAccount = await accountRepository.findOrCreateTaxPayable( -// {}, -// trx -// ); -// const billLedger = this.getBillLedger( -// bill, -// APAccount.id, -// taxPayableAccount.id -// ); -// // Commit the GL enties on the storage. -// await this.ledgerStorage.commit(tenantId, billLedger, trx); -// }; - -// /** -// * Reverts the given bill GL entries. -// * @param {number} tenantId -// * @param {number} billId -// * @param {Knex.Transaction} trx -// */ -// public revertBillGLEntries = async ( -// tenantId: number, -// billId: number, -// trx?: Knex.Transaction -// ) => { -// await this.ledgerStorage.deleteByReference(tenantId, billId, 'Bill', trx); -// }; - -// /** -// * Rewrites the given bill GL entries. -// * @param {number} tenantId -// * @param {number} billId -// * @param {Knex.Transaction} trx -// */ -// public rewriteBillGLEntries = async ( -// tenantId: number, -// billId: number, -// trx?: Knex.Transaction -// ) => { -// // Reverts the bill GL entries. -// await this.revertBillGLEntries(tenantId, billId, trx); - -// // Writes the bill GL entries. -// await this.writeBillGLEntries(tenantId, billId, trx); -// }; - -// /** -// * Retrieves the bill common entry. -// * @param {IBill} bill -// * @returns {ILedgerEntry} -// */ -// private getBillCommonEntry = (bill: IBill) => { -// return { -// debit: 0, -// credit: 0, -// currencyCode: bill.currencyCode, -// exchangeRate: bill.exchangeRate || 1, - -// transactionId: bill.id, -// transactionType: 'Bill', - -// date: moment(bill.billDate).format('YYYY-MM-DD'), -// userId: bill.userId, - -// referenceNumber: bill.referenceNo, -// transactionNumber: bill.billNumber, - -// branchId: bill.branchId, -// projectId: bill.projectId, - -// createdAt: bill.createdAt, -// }; -// }; - -// /** -// * Retrieves the bill item inventory/cost entry. -// * @param {IBill} bill - -// * @param {IItemEntry} entry - -// * @param {number} index - -// */ -// private getBillItemEntry = R.curry( -// (bill: IBill, entry: IItemEntry, index: number): ILedgerEntry => { -// const commonJournalMeta = this.getBillCommonEntry(bill); - -// const localAmount = bill.exchangeRate * entry.amountExludingTax; -// const landedCostAmount = sumBy(entry.allocatedCostEntries, 'cost'); - -// return { -// ...commonJournalMeta, -// debit: localAmount + landedCostAmount, -// accountId: -// ['inventory'].indexOf(entry.item.type) !== -1 -// ? entry.item.inventoryAccountId -// : entry.costAccountId, -// index: index + 1, -// indexGroup: 10, -// itemId: entry.itemId, -// itemQuantity: entry.quantity, -// accountNormal: AccountNormal.DEBIT, -// }; -// } -// ); - -// /** -// * Retrieves the bill landed cost entry. -// * @param {IBill} bill - -// * @param {} landedCost - -// * @param {number} index - -// */ -// private getBillLandedCostEntry = R.curry( -// (bill: IBill, landedCost, index: number): ILedgerEntry => { -// const commonJournalMeta = this.getBillCommonEntry(bill); - -// return { -// ...commonJournalMeta, -// credit: landedCost.amount, -// accountId: landedCost.costAccountId, -// accountNormal: AccountNormal.DEBIT, -// index: 1, -// indexGroup: 20, -// }; -// } -// ); - -// /** -// * Retrieves the bill payable entry. -// * @param {number} payableAccountId -// * @param {IBill} bill -// * @returns {ILedgerEntry} -// */ -// private getBillPayableEntry = ( -// payableAccountId: number, -// bill: IBill -// ): ILedgerEntry => { -// const commonJournalMeta = this.getBillCommonEntry(bill); - -// return { -// ...commonJournalMeta, -// credit: bill.totalLocal, -// accountId: payableAccountId, -// contactId: bill.vendorId, -// accountNormal: AccountNormal.CREDIT, -// index: 1, -// indexGroup: 5, -// }; -// }; - -// /** -// * Retrieves the bill tax GL entry. -// * @param {IBill} bill - -// * @param {number} taxPayableAccountId - -// * @param {IItemEntry} entry - -// * @param {number} index - -// * @returns {ILedgerEntry} -// */ -// private getBillTaxEntry = R.curry( -// ( -// bill: IBill, -// taxPayableAccountId: number, -// entry: IItemEntry, -// index: number -// ): ILedgerEntry => { -// const commonJournalMeta = this.getBillCommonEntry(bill); - -// return { -// ...commonJournalMeta, -// debit: entry.taxAmount, -// index, -// indexGroup: 30, -// accountId: taxPayableAccountId, -// accountNormal: AccountNormal.CREDIT, -// taxRateId: entry.taxRateId, -// taxRate: entry.taxRate, -// }; -// } -// ); - -// /** -// * Retrieves the bill tax GL entries. -// * @param {IBill} bill -// * @param {number} taxPayableAccountId -// * @returns {ILedgerEntry[]} -// */ -// private getBillTaxEntries = (bill: IBill, taxPayableAccountId: number) => { -// // Retrieves the non-zero tax entries. -// const nonZeroTaxEntries = this.itemsEntriesService.getNonZeroEntries( -// bill.entries -// ); -// const transformTaxEntry = this.getBillTaxEntry(bill, taxPayableAccountId); - -// return nonZeroTaxEntries.map(transformTaxEntry); -// }; - -// /** -// * Retrieves the given bill GL entries. -// * @param {IBill} bill -// * @param {number} payableAccountId -// * @returns {ILedgerEntry[]} -// */ -// private getBillGLEntries = ( -// bill: IBill, -// payableAccountId: number, -// taxPayableAccountId: number -// ): ILedgerEntry[] => { -// const payableEntry = this.getBillPayableEntry(payableAccountId, bill); - -// const itemEntryTransformer = this.getBillItemEntry(bill); -// const landedCostTransformer = this.getBillLandedCostEntry(bill); - -// const itemsEntries = bill.entries.map(itemEntryTransformer); -// const landedCostEntries = bill.locatedLandedCosts.map( -// landedCostTransformer -// ); -// const taxEntries = this.getBillTaxEntries(bill, taxPayableAccountId); - -// // Allocate cost entries journal entries. -// return [payableEntry, ...itemsEntries, ...landedCostEntries, ...taxEntries]; -// }; - -// /** -// * Retrieves the given bill ledger. -// * @param {IBill} bill -// * @param {number} payableAccountId -// * @returns {Ledger} -// */ -// private getBillLedger = ( -// bill: IBill, -// payableAccountId: number, -// taxPayableAccountId: number -// ) => { -// const entries = this.getBillGLEntries( -// bill, -// payableAccountId, -// taxPayableAccountId -// ); - -// return new Ledger(entries); -// }; -// } diff --git a/packages/server-nest/src/modules/Bills/commands/BillGLEntriesSubscriber.ts b/packages/server-nest/src/modules/Bills/commands/BillGLEntriesSubscriber.ts deleted file mode 100644 index 504684565..000000000 --- a/packages/server-nest/src/modules/Bills/commands/BillGLEntriesSubscriber.ts +++ /dev/null @@ -1,75 +0,0 @@ -// import { Inject, Service } from 'typedi'; -// import events from '@/subscribers/events'; -// import { -// IBillCreatedPayload, -// IBillEditedPayload, -// IBIllEventDeletedPayload, -// IBillOpenedPayload, -// } from '@/interfaces'; -// import { BillGLEntries } from './BillGLEntries'; - -// @Service() -// export class BillGLEntriesSubscriber { -// @Inject() -// private billGLEntries: BillGLEntries; - -// /** -// * Attaches events with handles. -// */ -// public attach(bus) { -// bus.subscribe( -// events.bill.onCreated, -// this.handlerWriteJournalEntriesOnCreate -// ); -// bus.subscribe( -// events.bill.onOpened, -// this.handlerWriteJournalEntriesOnCreate -// ); -// bus.subscribe( -// events.bill.onEdited, -// this.handleOverwriteJournalEntriesOnEdit -// ); -// bus.subscribe(events.bill.onDeleted, this.handlerDeleteJournalEntries); -// } - -// /** -// * Handles writing journal entries once bill created. -// * @param {IBillCreatedPayload} payload - -// */ -// private handlerWriteJournalEntriesOnCreate = async ({ -// tenantId, -// bill, -// trx, -// }: IBillCreatedPayload | IBillOpenedPayload) => { -// if (!bill.openedAt) return null; - -// await this.billGLEntries.writeBillGLEntries(tenantId, bill.id, trx); -// }; - -// /** -// * Handles the overwriting journal entries once bill edited. -// * @param {IBillEditedPayload} payload - -// */ -// private handleOverwriteJournalEntriesOnEdit = async ({ -// tenantId, -// billId, -// bill, -// trx, -// }: IBillEditedPayload) => { -// if (!bill.openedAt) return null; - -// await this.billGLEntries.rewriteBillGLEntries(tenantId, billId, trx); -// }; - -// /** -// * Handles revert journal entries on bill deleted. -// * @param {IBIllEventDeletedPayload} payload - -// */ -// private handlerDeleteJournalEntries = async ({ -// tenantId, -// billId, -// trx, -// }: IBIllEventDeletedPayload) => { -// await this.billGLEntries.revertBillGLEntries(tenantId, billId, trx); -// }; -// } diff --git a/packages/server-nest/src/modules/Bills/commands/BillsGL.ts b/packages/server-nest/src/modules/Bills/commands/BillsGL.ts new file mode 100644 index 000000000..d05f0d93d --- /dev/null +++ b/packages/server-nest/src/modules/Bills/commands/BillsGL.ts @@ -0,0 +1,243 @@ +import { sumBy } from 'lodash'; +import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types'; +import { ItemEntry } from '@/modules/Items/models/ItemEntry'; +import { Bill } from '../models/Bill'; +import { AccountNormal } from '@/modules/Accounts/Accounts.types'; +import { Ledger } from '@/modules/Ledger/Ledger'; +import { BillLandedCost } from '@/modules/BillLandedCosts/models/BillLandedCost'; + +export class BillGL { + private bill: Bill; + private payableAccountId: number; + private taxPayableAccountId: number; + private purchaseDiscountAccountId: number; + private otherExpensesAccountId: number; + + constructor(bill: Bill) { + this.bill = bill; + } + + setPayableAccountId(payableAccountId: number) { + this.payableAccountId = payableAccountId; + return this; + } + + setTaxPayableAccountId(taxPayableAccountId: number) { + this.taxPayableAccountId = taxPayableAccountId; + return this; + } + + setPurchaseDiscountAccountId(purchaseDiscountAccountId: number) { + this.purchaseDiscountAccountId = purchaseDiscountAccountId; + return this; + } + + setOtherExpensesAccountId(otherExpensesAccountId: number) { + this.otherExpensesAccountId = otherExpensesAccountId; + return this; + } + + /** + * Retrieves the bill common entry. + * @returns {ILedgerEntry} + */ + private get billCommonEntry() { + return { + debit: 0, + credit: 0, + + currencyCode: this.bill.currencyCode, + exchangeRate: this.bill.exchangeRate || 1, + + transactionId: this.bill.id, + transactionType: 'Bill', + + date: moment(this.bill.billDate).format('YYYY-MM-DD'), + userId: this.bill.userId, + + referenceNumber: this.bill.referenceNo, + transactionNumber: this.bill.billNumber, + + branchId: this.bill.branchId, + projectId: this.bill.projectId, + + createdAt: this.bill.createdAt, + }; + } + + /** + * Retrieves the bill item inventory/cost entry. + * @param {ItemEntry} entry - + * @param {number} index - + */ + private getBillItemEntry(entry: ItemEntry, index: number): ILedgerEntry { + const commonJournalMeta = this.billCommonEntry; + const totalLocal = this.bill.exchangeRate * entry.totalExcludingTax; + const landedCostAmount = sumBy(entry.allocatedCostEntries, 'cost'); + + return { + ...commonJournalMeta, + debit: totalLocal + landedCostAmount, + accountId: + ['inventory'].indexOf(entry.item.type) !== -1 + ? entry.item.inventoryAccountId + : entry.costAccountId, + index: index + 1, + indexGroup: 10, + itemId: entry.itemId, + itemQuantity: entry.quantity, + accountNormal: AccountNormal.DEBIT, + }; + } + + /** + * Retrieves the bill landed cost entry. + * @param {BillLandedCost} landedCost - Landed cost + * @param {number} index - Index + */ + private getBillLandedCostEntry( + landedCost: BillLandedCost, + index: number, + ): ILedgerEntry { + const commonJournalMeta = this.billCommonEntry; + + return { + ...commonJournalMeta, + credit: landedCost.amount, + accountId: landedCost.costAccountId, + accountNormal: AccountNormal.DEBIT, + index: 1, + indexGroup: 20, + }; + } + + /** + * Retrieves the bill payable entry. + * @returns {ILedgerEntry} + */ + private get billPayableEntry(): ILedgerEntry { + const commonJournalMeta = this.billCommonEntry; + + return { + ...commonJournalMeta, + credit: this.bill.totalLocal, + accountId: this.payableAccountId, + contactId: this.bill.vendorId, + accountNormal: AccountNormal.CREDIT, + index: 1, + indexGroup: 5, + }; + } + + /** + * Retrieves the bill tax GL entry. + * @param {IBill} bill - + * @param {number} taxPayableAccountId - + * @param {IItemEntry} entry - + * @param {number} index - + * @returns {ILedgerEntry} + */ + private getBillTaxEntry(entry: ItemEntry, index: number): ILedgerEntry { + const commonJournalMeta = this.billCommonEntry; + + return { + ...commonJournalMeta, + debit: entry.taxAmount, + index, + indexGroup: 30, + accountId: this.taxPayableAccountId, + accountNormal: AccountNormal.CREDIT, + taxRateId: entry.taxRateId, + taxRate: entry.taxRate, + }; + } + + /** + * Retrieves the bill tax GL entries. + * @param {IBill} bill + * @param {number} taxPayableAccountId + * @returns {ILedgerEntry[]} + */ + // private getBillTaxEntries = () => { + // // Retrieves the non-zero tax entries. + // const nonZeroTaxEntries = this.itemsEntriesService.getNonZeroEntries( + // this.bill.entries, + // ); + // const transformTaxEntry = this.getBillTaxEntry( + // this.bill, + // this.taxPayableAccountId, + // ); + + // return nonZeroTaxEntries.map(transformTaxEntry); + // }; + + /** + * Retrieves the purchase discount GL entry. + * @returns {ILedgerEntry} + */ + private get purchaseDiscountEntry(): ILedgerEntry { + const commonEntry = this.billCommonEntry; + + return { + ...commonEntry, + credit: this.bill.discountAmountLocal, + accountId: this.purchaseDiscountAccountId, + accountNormal: AccountNormal.DEBIT, + index: 1, + indexGroup: 40, + }; + } + + /** + * Retrieves the purchase other charges GL entry. + * @returns {ILedgerEntry} + */ + private get adjustmentEntry(): ILedgerEntry { + const commonEntry = this.billCommonEntry; + const adjustmentAmount = Math.abs(this.bill.adjustmentLocal); + + return { + ...commonEntry, + debit: this.bill.adjustmentLocal > 0 ? adjustmentAmount : 0, + credit: this.bill.adjustmentLocal < 0 ? adjustmentAmount : 0, + accountId: this.otherExpensesAccountId, + accountNormal: AccountNormal.DEBIT, + index: 1, + indexGroup: 40, + }; + } + + /** + * Retrieves the given bill GL entries. + * @returns {ILedgerEntry[]} + */ + private getBillGLEntries = (): ILedgerEntry[] => { + const payableEntry = this.billPayableEntry; + + const itemsEntries = this.bill.entries.map((entry, index) => + this.getBillItemEntry(entry, index), + ); + const landedCostEntries = this.bill.locatedLandedCosts.map( + (landedCost, index) => this.getBillLandedCostEntry(landedCost, index), + ); + + // Allocate cost entries journal entries. + return [ + payableEntry, + ...itemsEntries, + ...landedCostEntries, + this.purchaseDiscountEntry, + this.adjustmentEntry, + ]; + }; + + /** + * Retrieves the given bill ledger. + * @returns {Ledger} + */ + public getBillLedger = () => { + const entries = this.getBillGLEntries(); + + return new Ledger(entries); + }; +} diff --git a/packages/server-nest/src/modules/Bills/commands/BillsGLEntries.ts b/packages/server-nest/src/modules/Bills/commands/BillsGLEntries.ts new file mode 100644 index 000000000..964f0ad3c --- /dev/null +++ b/packages/server-nest/src/modules/Bills/commands/BillsGLEntries.ts @@ -0,0 +1,97 @@ +import { Knex } from 'knex'; +import { LedgerStorageService } from '@/modules/Ledger/LedgerStorage.service'; +import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository'; +import { Bill } from '../models/Bill'; +import { Injectable } from '@nestjs/common'; +import { BillGL } from './BillsGL'; + +@Injectable() +export class BillGLEntries { + /** + * @param {LedgerStorageService} ledgerStorage - Ledger storage service. + * @param {AccountRepository} accountRepository - Account repository. + * @param {typeof Bill} billModel - Bill model. + */ + constructor( + private readonly ledgerStorage: LedgerStorageService, + private readonly accountRepository: AccountRepository, + private readonly billModel: typeof Bill, + ) {} + + /** + * Creates bill GL entries. + * @param {number} billId - Bill id. + * @param {Knex.Transaction} trx - Knex transaction. + */ + public writeBillGLEntries = async ( + billId: number, + trx?: Knex.Transaction, + ) => { + // Retrieves bill with associated entries and landed costs. + const bill = await this.billModel + .query(trx) + .findById(billId) + .withGraphFetched('entries.item') + .withGraphFetched('entries.allocatedCostEntries') + .withGraphFetched('locatedLandedCosts.allocateEntries'); + + // Finds or create a A/P account based on the given currency. + const APAccount = await this.accountRepository.findOrCreateAccountsPayable( + bill.currencyCode, + {}, + trx, + ); + // Find or create tax payable account. + const taxPayableAccount = + await this.accountRepository.findOrCreateTaxPayable({}, trx); + + // Find or create other expenses account. + const otherExpensesAccount = + await this.accountRepository.findOrCreateOtherExpensesAccount({}, trx); + + // Find or create purchase discount account. + const purchaseDiscountAccount = + await this.accountRepository.findOrCreatePurchaseDiscountAccount({}, trx); + + // Retrieves the bill ledger. + const billLedger = new BillGL(bill) + .setPayableAccountId(APAccount.id) + .setTaxPayableAccountId(taxPayableAccount.id) + .setPurchaseDiscountAccountId(purchaseDiscountAccount.id) + .setOtherExpensesAccountId(otherExpensesAccount.id) + .getBillLedger(); + + // Commit the GL enties on the storage. + await this.ledgerStorage.commit(billLedger, trx); + }; + + /** + * Reverts the given bill GL entries. + * @param {number} tenantId + * @param {number} billId + * @param {Knex.Transaction} trx + */ + public revertBillGLEntries = async ( + billId: number, + trx?: Knex.Transaction, + ) => { + await this.ledgerStorage.deleteByReference(billId, 'Bill', trx); + }; + + /** + * Rewrites the given bill GL entries. + * @param {number} tenantId + * @param {number} billId + * @param {Knex.Transaction} trx + */ + public rewriteBillGLEntries = async ( + billId: number, + trx?: Knex.Transaction, + ) => { + // Reverts the bill GL entries. + await this.revertBillGLEntries(billId, trx); + + // Writes the bill GL entries. + await this.writeBillGLEntries(billId, trx); + }; +} diff --git a/packages/server-nest/src/modules/Bills/models/Bill.ts b/packages/server-nest/src/modules/Bills/models/Bill.ts index 2c4b13d40..f087c0748 100644 --- a/packages/server-nest/src/modules/Bills/models/Bill.ts +++ b/packages/server-nest/src/modules/Bills/models/Bill.ts @@ -9,6 +9,7 @@ import moment from 'moment'; // import ModelSearchable from './ModelSearchable'; import { BaseModel } from '@/models/Model'; import { ItemEntry } from '@/modules/Items/models/ItemEntry'; +import { BillLandedCost } from '@/modules/BillLandedCosts/models/BillLandedCost'; export class Bill extends BaseModel{ public amount: number; @@ -34,12 +35,13 @@ export class Bill extends BaseModel{ public branchId: number; public warehouseId: number; + public projectId: number; public createdAt: Date; public updatedAt: Date | null; public entries?: ItemEntry[]; - + public locatedLandedCosts?: BillLandedCost[]; /** * Timestamps columns. */ diff --git a/packages/server-nest/src/modules/Bills/subscribers/BillGLEntriesSubscriber.ts b/packages/server-nest/src/modules/Bills/subscribers/BillGLEntriesSubscriber.ts new file mode 100644 index 000000000..b5752580f --- /dev/null +++ b/packages/server-nest/src/modules/Bills/subscribers/BillGLEntriesSubscriber.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@nestjs/common'; +import { + IBillCreatedPayload, + IBillEditedPayload, + IBIllEventDeletedPayload, + IBillOpenedPayload, +} from '../Bills.types'; +import { BillGLEntries } from '../commands/BillsGLEntries'; +import { events } from '@/common/events/events'; +import { OnEvent } from '@nestjs/event-emitter'; + +@Injectable() +export class BillGLEntriesSubscriber { + /** + * @param {BillGLEntries} billGLEntries - Bill GL entries command. + */ + constructor(private billGLEntries: BillGLEntries) {} + + /** + * Handles writing journal entries once bill created. + * @param {IBillCreatedPayload} payload - + */ + @OnEvent(events.bill.onCreated) + @OnEvent(events.bill.onOpened) + public async handlerWriteJournalEntriesOnCreate({ + bill, + trx, + }: IBillCreatedPayload | IBillOpenedPayload) { + if (!bill.openedAt) return null; + + await this.billGLEntries.writeBillGLEntries(bill.id, trx); + }; + + /** + * Handles the overwriting journal entries once bill edited. + * @param {IBillEditedPayload} payload - + */ + @OnEvent(events.bill.onEdited) + public async handleOverwriteJournalEntriesOnEdit({ + bill, + trx, + }: IBillEditedPayload) { + if (!bill.openedAt) return null; + + await this.billGLEntries.rewriteBillGLEntries(bill.id, trx); + }; + + /** + * Handles revert journal entries on bill deleted. + * @param {IBIllEventDeletedPayload} payload - + */ + @OnEvent(events.bill.onDeleted) + public async handlerDeleteJournalEntries({ + oldBill, + trx, + }: IBIllEventDeletedPayload) { + await this.billGLEntries.revertBillGLEntries(oldBill.id, trx); + }; +} diff --git a/packages/server-nest/src/modules/CreditNotes/CreditNotes.module.ts b/packages/server-nest/src/modules/CreditNotes/CreditNotes.module.ts index 5b1850e88..8f50385c4 100644 --- a/packages/server-nest/src/modules/CreditNotes/CreditNotes.module.ts +++ b/packages/server-nest/src/modules/CreditNotes/CreditNotes.module.ts @@ -18,6 +18,8 @@ import { TemplateInjectableModule } from '../TemplateInjectable/TemplateInjectab import { GetCreditNote } from './queries/GetCreditNote.service'; import { CreditNoteBrandingTemplate } from './queries/CreditNoteBrandingTemplate.service'; import { AutoIncrementOrdersModule } from '../AutoIncrementOrders/AutoIncrementOrders.module'; +import CreditNoteGLEntries from './commands/CreditNoteGLEntries'; +import CreditNoteGLEntriesSubscriber from './subscribers/CreditNoteGLEntriesSubscriber'; @Module({ imports: [ @@ -40,7 +42,9 @@ import { AutoIncrementOrdersModule } from '../AutoIncrementOrders/AutoIncrementO CreditNoteAutoIncrementService, GetCreditNoteState, CreditNoteApplication, - CreditNoteBrandingTemplate + CreditNoteBrandingTemplate, + CreditNoteGLEntries, + CreditNoteGLEntriesSubscriber ], exports: [ CreateCreditNoteService, diff --git a/packages/server-nest/src/modules/CreditNotes/commands/CreditNoteGL.ts b/packages/server-nest/src/modules/CreditNotes/commands/CreditNoteGL.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/server-nest/src/modules/CreditNotes/commands/CreditNoteGLEntries.ts b/packages/server-nest/src/modules/CreditNotes/commands/CreditNoteGLEntries.ts index d7662438d..95586ee2e 100644 --- a/packages/server-nest/src/modules/CreditNotes/commands/CreditNoteGLEntries.ts +++ b/packages/server-nest/src/modules/CreditNotes/commands/CreditNoteGLEntries.ts @@ -1,297 +1,293 @@ -// import { Inject, Service } from 'typedi'; -// import { Knex } from 'knex'; -// import * as R from 'ramda'; -// import { -// AccountNormal, -// IItemEntry, -// ILedgerEntry, -// ICreditNote, -// ILedger, -// ICreditNoteGLCommonEntry, -// } from '@/interfaces'; -// import HasTenancyService from '@/services/Tenancy/TenancyService'; -// import Ledger from '@/services/Accounting/Ledger'; -// import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; -// import { SaleReceipt } from '@/models'; +import { Inject, Service } from 'typedi'; +import { Knex } from 'knex'; +import * as R from 'ramda'; +import { + AccountNormal, + IItemEntry, + ILedgerEntry, + ICreditNote, + ILedger, + ICreditNoteGLCommonEntry, +} from '@/interfaces'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import Ledger from '@/services/Accounting/Ledger'; +import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; +import { SaleReceipt } from '@/models'; -// @Service() -// export default class CreditNoteGLEntries { -// @Inject() -// private tenancy: HasTenancyService; +@Service() +export default class CreditNoteGLEntries { + @Inject() + private tenancy: HasTenancyService; -// @Inject() -// private ledgerStorage: LedgerStorageService; + @Inject() + private ledgerStorage: LedgerStorageService; -// /** -// * Retrieves the credit note GL. -// * @param {ICreditNote} creditNote -// * @param {number} receivableAccount -// * @returns {Ledger} -// */ -// private getCreditNoteGLedger = ( -// creditNote: ICreditNote, -// receivableAccount: number, -// discountAccount: number, -// adjustmentAccount: number -// ): Ledger => { -// const ledgerEntries = this.getCreditNoteGLEntries( -// creditNote, -// receivableAccount, -// discountAccount, -// adjustmentAccount -// ); -// return new Ledger(ledgerEntries); -// }; + /** + * Retrieves the credit note GL. + * @param {ICreditNote} creditNote + * @param {number} receivableAccount + * @returns {Ledger} + */ + private getCreditNoteGLedger = ( + creditNote: ICreditNote, + receivableAccount: number, + discountAccount: number, + adjustmentAccount: number + ): Ledger => { + const ledgerEntries = this.getCreditNoteGLEntries( + creditNote, + receivableAccount, + discountAccount, + adjustmentAccount + ); + return new Ledger(ledgerEntries); + }; -// /** -// * Saves credit note GL entries. -// * @param {number} tenantId - -// * @param {ICreditNote} creditNote - Credit note model. -// * @param {number} payableAccount - Payable account id. -// * @param {Knex.Transaction} trx -// */ -// public saveCreditNoteGLEntries = async ( -// tenantId: number, -// creditNote: ICreditNote, -// payableAccount: number, -// discountAccount: number, -// adjustmentAccount: number, -// trx?: Knex.Transaction -// ): Promise => { -// const ledger = this.getCreditNoteGLedger( -// creditNote, -// payableAccount, -// discountAccount, -// adjustmentAccount -// ); + /** + * Saves credit note GL entries. + * @param {number} tenantId - + * @param {ICreditNote} creditNote - Credit note model. + * @param {number} payableAccount - Payable account id. + * @param {Knex.Transaction} trx + */ + public saveCreditNoteGLEntries = async ( + tenantId: number, + creditNote: ICreditNote, + payableAccount: number, + discountAccount: number, + adjustmentAccount: number, + trx?: Knex.Transaction + ): Promise => { + const ledger = this.getCreditNoteGLedger( + creditNote, + payableAccount, + discountAccount, + adjustmentAccount + ); -// await this.ledgerStorage.commit(tenantId, ledger, trx); -// }; + await this.ledgerStorage.commit(tenantId, ledger, trx); + }; -// /** -// * Reverts the credit note associated GL entries. -// * @param {number} tenantId -// * @param {number} vendorCreditId -// * @param {Knex.Transaction} trx -// */ -// public revertVendorCreditGLEntries = async ( -// tenantId: number, -// creditNoteId: number, -// trx?: Knex.Transaction -// ): Promise => { -// await this.ledgerStorage.deleteByReference( -// tenantId, -// creditNoteId, -// 'CreditNote', -// trx -// ); -// }; + /** + * Reverts the credit note associated GL entries. + * @param {number} tenantId + * @param {number} vendorCreditId + * @param {Knex.Transaction} trx + */ + public revertVendorCreditGLEntries = async ( + creditNoteId: number, + trx?: Knex.Transaction + ): Promise => { + await this.ledgerStorage.deleteByReference( + creditNoteId, + 'CreditNote', + trx + ); + }; -// /** -// * Writes vendor credit associated GL entries. -// * @param {number} tenantId - Tenant id. -// * @param {number} creditNoteId - Credit note id. -// * @param {Knex.Transaction} trx - Knex transactions. -// */ -// public createVendorCreditGLEntries = async ( -// tenantId: number, -// creditNoteId: number, -// trx?: Knex.Transaction -// ): Promise => { -// const { CreditNote } = this.tenancy.models(tenantId); -// const { accountRepository } = this.tenancy.repositories(tenantId); + /** + * Writes vendor credit associated GL entries. + * @param {number} tenantId - Tenant id. + * @param {number} creditNoteId - Credit note id. + * @param {Knex.Transaction} trx - Knex transactions. + */ + public createVendorCreditGLEntries = async ( + creditNoteId: number, + trx?: Knex.Transaction + ): Promise => { + const { CreditNote } = this.tenancy.models(tenantId); + const { accountRepository } = this.tenancy.repositories(tenantId); -// // Retrieve the credit note with associated entries and items. -// const creditNoteWithItems = await CreditNote.query(trx) -// .findById(creditNoteId) -// .withGraphFetched('entries.item'); + // Retrieve the credit note with associated entries and items. + const creditNoteWithItems = await CreditNote.query(trx) + .findById(creditNoteId) + .withGraphFetched('entries.item'); -// // Retreive the the `accounts receivable` account based on the given currency. -// const ARAccount = await accountRepository.findOrCreateAccountReceivable( -// creditNoteWithItems.currencyCode -// ); -// const discountAccount = await accountRepository.findOrCreateDiscountAccount( -// {} -// ); -// const adjustmentAccount = -// await accountRepository.findOrCreateOtherChargesAccount({}); -// // Saves the credit note GL entries. -// await this.saveCreditNoteGLEntries( -// tenantId, -// creditNoteWithItems, -// ARAccount.id, -// discountAccount.id, -// adjustmentAccount.id, -// trx -// ); -// }; + // Retreive the the `accounts receivable` account based on the given currency. + const ARAccount = await accountRepository.findOrCreateAccountReceivable( + creditNoteWithItems.currencyCode + ); + const discountAccount = await accountRepository.findOrCreateDiscountAccount( + {} + ); + const adjustmentAccount = + await accountRepository.findOrCreateOtherChargesAccount({}); + // Saves the credit note GL entries. + await this.saveCreditNoteGLEntries( + tenantId, + creditNoteWithItems, + ARAccount.id, + discountAccount.id, + adjustmentAccount.id, + trx + ); + }; -// /** -// * Edits vendor credit associated GL entries. -// * @param {number} tenantId -// * @param {number} creditNoteId -// * @param {Knex.Transaction} trx -// */ -// public editVendorCreditGLEntries = async ( -// tenantId: number, -// creditNoteId: number, -// trx?: Knex.Transaction -// ): Promise => { -// // Reverts vendor credit GL entries. -// await this.revertVendorCreditGLEntries(tenantId, creditNoteId, trx); + /** + * Edits vendor credit associated GL entries. + * @param {number} tenantId + * @param {number} creditNoteId + * @param {Knex.Transaction} trx + */ + public editVendorCreditGLEntries = async ( + creditNoteId: number, + trx?: Knex.Transaction + ): Promise => { + // Reverts vendor credit GL entries. + await this.revertVendorCreditGLEntries(creditNoteId, trx); -// // Creates vendor credit Gl entries. -// await this.createVendorCreditGLEntries(tenantId, creditNoteId, trx); -// }; + // Creates vendor credit Gl entries. + await this.createVendorCreditGLEntries(creditNoteId, trx); + }; -// /** -// * Retrieve the credit note common entry. -// * @param {ICreditNote} creditNote - -// * @returns {ICreditNoteGLCommonEntry} -// */ -// private getCreditNoteCommonEntry = ( -// creditNote: ICreditNote -// ): ICreditNoteGLCommonEntry => { -// return { -// date: creditNote.creditNoteDate, -// userId: creditNote.userId, -// currencyCode: creditNote.currencyCode, -// exchangeRate: creditNote.exchangeRate, + /** + * Retrieve the credit note common entry. + * @param {ICreditNote} creditNote - + * @returns {ICreditNoteGLCommonEntry} + */ + private getCreditNoteCommonEntry = ( + creditNote: ICreditNote + ): ICreditNoteGLCommonEntry => { + return { + date: creditNote.creditNoteDate, + userId: creditNote.userId, + currencyCode: creditNote.currencyCode, + exchangeRate: creditNote.exchangeRate, -// transactionType: 'CreditNote', -// transactionId: creditNote.id, + transactionType: 'CreditNote', + transactionId: creditNote.id, -// transactionNumber: creditNote.creditNoteNumber, -// referenceNumber: creditNote.referenceNo, + transactionNumber: creditNote.creditNoteNumber, + referenceNumber: creditNote.referenceNo, -// createdAt: creditNote.createdAt, -// indexGroup: 10, + createdAt: creditNote.createdAt, + indexGroup: 10, -// credit: 0, -// debit: 0, + credit: 0, + debit: 0, -// branchId: creditNote.branchId, -// }; -// }; + branchId: creditNote.branchId, + }; + }; -// /** -// * Retrieves the creidt note A/R entry. -// * @param {ICreditNote} creditNote - -// * @param {number} ARAccountId - -// * @returns {ILedgerEntry} -// */ -// private getCreditNoteAREntry = ( -// creditNote: ICreditNote, -// ARAccountId: number -// ): ILedgerEntry => { -// const commonEntry = this.getCreditNoteCommonEntry(creditNote); + /** + * Retrieves the creidt note A/R entry. + * @param {ICreditNote} creditNote - + * @param {number} ARAccountId - + * @returns {ILedgerEntry} + */ + private getCreditNoteAREntry = ( + creditNote: ICreditNote, + ARAccountId: number + ): ILedgerEntry => { + const commonEntry = this.getCreditNoteCommonEntry(creditNote); -// return { -// ...commonEntry, -// credit: creditNote.totalLocal, -// accountId: ARAccountId, -// contactId: creditNote.customerId, -// index: 1, -// accountNormal: AccountNormal.DEBIT, -// }; -// }; + return { + ...commonEntry, + credit: creditNote.totalLocal, + accountId: ARAccountId, + contactId: creditNote.customerId, + index: 1, + accountNormal: AccountNormal.DEBIT, + }; + }; -// /** -// * Retrieve the credit note item entry. -// * @param {ICreditNote} creditNote -// * @param {IItemEntry} entry -// * @param {number} index -// * @returns {ILedgerEntry} -// */ -// private getCreditNoteItemEntry = R.curry( -// ( -// creditNote: ICreditNote, -// entry: IItemEntry, -// index: number -// ): ILedgerEntry => { -// const commonEntry = this.getCreditNoteCommonEntry(creditNote); -// const totalLocal = entry.totalExcludingTax * creditNote.exchangeRate; + /** + * Retrieve the credit note item entry. + * @param {ICreditNote} creditNote + * @param {IItemEntry} entry + * @param {number} index + * @returns {ILedgerEntry} + */ + private getCreditNoteItemEntry = R.curry( + ( + creditNote: ICreditNote, + entry: IItemEntry, + index: number + ): ILedgerEntry => { + const commonEntry = this.getCreditNoteCommonEntry(creditNote); + const totalLocal = entry.totalExcludingTax * creditNote.exchangeRate; -// return { -// ...commonEntry, -// debit: totalLocal, -// accountId: entry.sellAccountId || entry.item.sellAccountId, -// note: entry.description, -// index: index + 2, -// itemId: entry.itemId, -// itemQuantity: entry.quantity, -// accountNormal: AccountNormal.CREDIT, -// }; -// } -// ); + return { + ...commonEntry, + debit: totalLocal, + accountId: entry.sellAccountId || entry.item.sellAccountId, + note: entry.description, + index: index + 2, + itemId: entry.itemId, + itemQuantity: entry.quantity, + accountNormal: AccountNormal.CREDIT, + }; + } + ); -// /** -// * Retrieves the credit note discount entry. -// * @param {ICreditNote} creditNote -// * @param {number} discountAccountId -// * @returns {ILedgerEntry} -// */ -// private getDiscountEntry = ( -// creditNote: ICreditNote, -// discountAccountId: number -// ): ILedgerEntry => { -// const commonEntry = this.getCreditNoteCommonEntry(creditNote); + /** + * Retrieves the credit note discount entry. + * @param {ICreditNote} creditNote + * @param {number} discountAccountId + * @returns {ILedgerEntry} + */ + private getDiscountEntry = ( + creditNote: ICreditNote, + discountAccountId: number + ): ILedgerEntry => { + const commonEntry = this.getCreditNoteCommonEntry(creditNote); -// return { -// ...commonEntry, -// credit: creditNote.discountAmountLocal, -// accountId: discountAccountId, -// index: 1, -// accountNormal: AccountNormal.CREDIT, -// }; -// }; + return { + ...commonEntry, + credit: creditNote.discountAmountLocal, + accountId: discountAccountId, + index: 1, + accountNormal: AccountNormal.CREDIT, + }; + }; -// /** -// * Retrieves the credit note adjustment entry. -// * @param {ICreditNote} creditNote -// * @param {number} adjustmentAccountId -// * @returns {ILedgerEntry} -// */ -// private getAdjustmentEntry = ( -// creditNote: ICreditNote, -// adjustmentAccountId: number -// ): ILedgerEntry => { -// const commonEntry = this.getCreditNoteCommonEntry(creditNote); -// const adjustmentAmount = Math.abs(creditNote.adjustmentLocal); + /** + * Retrieves the credit note adjustment entry. + * @param {ICreditNote} creditNote + * @param {number} adjustmentAccountId + * @returns {ILedgerEntry} + */ + private getAdjustmentEntry = ( + creditNote: ICreditNote, + adjustmentAccountId: number + ): ILedgerEntry => { + const commonEntry = this.getCreditNoteCommonEntry(creditNote); + const adjustmentAmount = Math.abs(creditNote.adjustmentLocal); -// return { -// ...commonEntry, -// credit: creditNote.adjustmentLocal < 0 ? adjustmentAmount : 0, -// debit: creditNote.adjustmentLocal > 0 ? adjustmentAmount : 0, -// accountId: adjustmentAccountId, -// accountNormal: AccountNormal.CREDIT, -// index: 1, -// }; -// }; + return { + ...commonEntry, + credit: creditNote.adjustmentLocal < 0 ? adjustmentAmount : 0, + debit: creditNote.adjustmentLocal > 0 ? adjustmentAmount : 0, + accountId: adjustmentAccountId, + accountNormal: AccountNormal.CREDIT, + index: 1, + }; + }; -// /** -// * Retrieve the credit note GL entries. -// * @param {ICreditNote} creditNote - Credit note. -// * @param {IAccount} receivableAccount - Receviable account. -// * @returns {ILedgerEntry[]} - Ledger entries. -// */ -// public getCreditNoteGLEntries = ( -// creditNote: ICreditNote, -// ARAccountId: number, -// discountAccountId: number, -// adjustmentAccountId: number -// ): ILedgerEntry[] => { -// const AREntry = this.getCreditNoteAREntry(creditNote, ARAccountId); + /** + * Retrieve the credit note GL entries. + * @param {ICreditNote} creditNote - Credit note. + * @param {IAccount} receivableAccount - Receviable account. + * @returns {ILedgerEntry[]} - Ledger entries. + */ + public getCreditNoteGLEntries = ( + creditNote: ICreditNote, + ARAccountId: number, + discountAccountId: number, + adjustmentAccountId: number + ): ILedgerEntry[] => { + const AREntry = this.getCreditNoteAREntry(creditNote, ARAccountId); -// const getItemEntry = this.getCreditNoteItemEntry(creditNote); -// const itemsEntries = creditNote.entries.map(getItemEntry); + const getItemEntry = this.getCreditNoteItemEntry(creditNote); + const itemsEntries = creditNote.entries.map(getItemEntry); -// const discountEntry = this.getDiscountEntry(creditNote, discountAccountId); -// const adjustmentEntry = this.getAdjustmentEntry( -// creditNote, -// adjustmentAccountId -// ); + const discountEntry = this.getDiscountEntry(creditNote, discountAccountId); + const adjustmentEntry = this.getAdjustmentEntry( + creditNote, + adjustmentAccountId + ); -// return [AREntry, discountEntry, adjustmentEntry, ...itemsEntries]; -// }; -// } + return [AREntry, discountEntry, adjustmentEntry, ...itemsEntries]; + }; +} diff --git a/packages/server-nest/src/modules/CreditNotes/subscribers/CreditNoteGLEntriesSubscriber.ts b/packages/server-nest/src/modules/CreditNotes/subscribers/CreditNoteGLEntriesSubscriber.ts index 649e47d82..4e9be8048 100644 --- a/packages/server-nest/src/modules/CreditNotes/subscribers/CreditNoteGLEntriesSubscriber.ts +++ b/packages/server-nest/src/modules/CreditNotes/subscribers/CreditNoteGLEntriesSubscriber.ts @@ -1,113 +1,78 @@ -// import { Service, Inject } from 'typedi'; -// import events from '@/subscribers/events'; -// import { -// ICreditNoteCreatedPayload, -// ICreditNoteDeletedPayload, -// ICreditNoteEditedPayload, -// ICreditNoteOpenedPayload, -// } from '@/interfaces'; -// import CreditNoteGLEntries from '../commands/CreditNoteGLEntries'; +import { + ICreditNoteCreatedPayload, + ICreditNoteDeletedPayload, + ICreditNoteEditedPayload, + ICreditNoteOpenedPayload, +} from '../types/CreditNotes.types'; +import CreditNoteGLEntries from '../commands/CreditNoteGLEntries'; +import { OnEvent } from '@nestjs/event-emitter'; +import { Injectable } from '@nestjs/common'; +import { events } from '@/common/events/events'; -// @Service() -// export default class CreditNoteGLEntriesSubscriber { -// @Inject() -// private creditNoteGLEntries: CreditNoteGLEntries; +@Injectable() +export default class CreditNoteGLEntriesSubscriber { + constructor(private readonly creditNoteGLEntries: CreditNoteGLEntries) {} -// /** -// * Attaches events with handlers. -// * @param bus -// */ -// public attach(bus) { -// bus.subscribe( -// events.creditNote.onCreated, -// this.writeGlEntriesOnceCreditNoteCreated -// ); -// bus.subscribe( -// events.creditNote.onOpened, -// this.writeGLEntriesOnceCreditNoteOpened -// ); -// bus.subscribe( -// events.creditNote.onEdited, -// this.editVendorCreditGLEntriesOnceEdited -// ); -// bus.subscribe( -// events.creditNote.onDeleted, -// this.revertGLEntriesOnceCreditNoteDeleted -// ); -// } + /** + * Writes the GL entries once the credit note transaction created or open. + * @param {ICreditNoteCreatedPayload|ICreditNoteOpenedPayload} payload - + */ + @OnEvent(events.creditNote.onCreated) + public async writeGlEntriesOnceCreditNoteCreated({ + creditNote, + trx, + }: ICreditNoteCreatedPayload | ICreditNoteOpenedPayload) { + // Can't continue if the credit note is not published yet. + if (!creditNote.isPublished) return; -// /** -// * Writes the GL entries once the credit note transaction created or open. -// * @private -// * @param {ICreditNoteCreatedPayload|ICreditNoteOpenedPayload} payload - -// */ -// private writeGlEntriesOnceCreditNoteCreated = async ({ -// tenantId, -// creditNote, -// creditNoteId, -// trx, -// }: ICreditNoteCreatedPayload | ICreditNoteOpenedPayload) => { -// // Can't continue if the credit note is not published yet. -// if (!creditNote.isPublished) return; + await this.creditNoteGLEntries.createVendorCreditGLEntries( + creditNote.id, + trx, + ); + } -// await this.creditNoteGLEntries.createVendorCreditGLEntries( -// tenantId, -// creditNoteId, -// trx -// ); -// }; + /** + * Writes the GL entries once the vendor credit transaction opened. + * @param {ICreditNoteOpenedPayload} payload + */ + @OnEvent(events.creditNote.onOpened) + public async writeGLEntriesOnceCreditNoteOpened({ + creditNote, + trx, + }: ICreditNoteOpenedPayload) { + await this.creditNoteGLEntries.createVendorCreditGLEntries( + creditNote.id, + trx, + ); + } -// /** -// * Writes the GL entries once the vendor credit transaction opened. -// * @param {ICreditNoteOpenedPayload} payload -// */ -// private writeGLEntriesOnceCreditNoteOpened = async ({ -// tenantId, -// creditNoteId, -// trx, -// }: ICreditNoteOpenedPayload) => { -// await this.creditNoteGLEntries.createVendorCreditGLEntries( -// tenantId, -// creditNoteId, -// trx -// ); -// }; + /** + * Reverts GL entries once credit note deleted. + */ + @OnEvent(events.creditNote.onDeleted) + public async revertGLEntriesOnceCreditNoteDeleted({ + oldCreditNote, + creditNoteId, + trx, + }: ICreditNoteDeletedPayload) { + // Can't continue if the credit note is not published yet. + if (!oldCreditNote.isPublished) return; -// /** -// * Reverts GL entries once credit note deleted. -// */ -// private revertGLEntriesOnceCreditNoteDeleted = async ({ -// tenantId, -// oldCreditNote, -// creditNoteId, -// trx, -// }: ICreditNoteDeletedPayload) => { -// // Can't continue if the credit note is not published yet. -// if (!oldCreditNote.isPublished) return; + await this.creditNoteGLEntries.revertVendorCreditGLEntries(creditNoteId); + } -// await this.creditNoteGLEntries.revertVendorCreditGLEntries( -// tenantId, -// creditNoteId -// ); -// }; + /** + * Edits vendor credit associated GL entries once the transaction edited. + * @param {ICreditNoteEditedPayload} payload - + */ + @OnEvent(events.creditNote.onEdited) + public async editVendorCreditGLEntriesOnceEdited({ + creditNote, + trx, + }: ICreditNoteEditedPayload) { + // Can't continue if the credit note is not published yet. + if (!creditNote.isPublished) return; -// /** -// * Edits vendor credit associated GL entries once the transaction edited. -// * @param {ICreditNoteEditedPayload} payload - -// */ -// private editVendorCreditGLEntriesOnceEdited = async ({ -// tenantId, -// creditNote, -// creditNoteId, -// trx, -// }: ICreditNoteEditedPayload) => { -// // Can't continue if the credit note is not published yet. -// if (!creditNote.isPublished) return; - -// await this.creditNoteGLEntries.editVendorCreditGLEntries( -// tenantId, -// creditNoteId, -// trx -// ); -// }; -// } + await this.creditNoteGLEntries.editVendorCreditGLEntries(creditNote.id, trx); + } +} diff --git a/packages/server-nest/src/modules/Expenses/Expenses.controller.ts b/packages/server-nest/src/modules/Expenses/Expenses.controller.ts index d9b1eac8e..c1c36b540 100644 --- a/packages/server-nest/src/modules/Expenses/Expenses.controller.ts +++ b/packages/server-nest/src/modules/Expenses/Expenses.controller.ts @@ -12,8 +12,10 @@ import { IExpenseCreateDTO, IExpenseEditDTO, } from './interfaces/Expenses.interface'; +import { PublicRoute } from '../Auth/Jwt.guard'; @Controller('expenses') +@PublicRoute() export class ExpensesController { constructor(private readonly expensesApplication: ExpensesApplication) {} diff --git a/packages/server-nest/src/modules/Expenses/Expenses.module.ts b/packages/server-nest/src/modules/Expenses/Expenses.module.ts index 4c0d766de..596d3e345 100644 --- a/packages/server-nest/src/modules/Expenses/Expenses.module.ts +++ b/packages/server-nest/src/modules/Expenses/Expenses.module.ts @@ -12,9 +12,12 @@ import { TenancyContext } from '../Tenancy/TenancyContext.service'; import { TransformerInjectable } from '../Transformer/TransformerInjectable.service'; import { ExpensesWriteGLSubscriber } from './subscribers/ExpenseGLEntries.subscriber'; import { ExpenseGLEntriesStorageService } from './subscribers/ExpenseGLEntriesStorage.sevice'; +import { ExpenseGLEntriesService } from './subscribers/ExpenseGLEntries.service'; +import { LedgerModule } from '../Ledger/Ledger.module'; +import { BranchesModule } from '../Branches/Branches.module'; @Module({ - imports: [], + imports: [LedgerModule, BranchesModule], controllers: [ExpensesController], providers: [ CreateExpense, @@ -29,6 +32,7 @@ import { ExpenseGLEntriesStorageService } from './subscribers/ExpenseGLEntriesSt TransformerInjectable, ExpensesWriteGLSubscriber, ExpenseGLEntriesStorageService, + ExpenseGLEntriesService ], }) export class ExpensesModule {} diff --git a/packages/server-nest/src/modules/Expenses/Expenses.types.ts b/packages/server-nest/src/modules/Expenses/Expenses.types.ts index 2b05e3f72..9ccd46379 100644 --- a/packages/server-nest/src/modules/Expenses/Expenses.types.ts +++ b/packages/server-nest/src/modules/Expenses/Expenses.types.ts @@ -2,10 +2,6 @@ import { Knex } from 'knex'; import { Expense } from './models/Expense.model'; import { SystemUser } from '../System/models/SystemUser'; import { IFilterRole } from '../DynamicListing/DynamicFilter/DynamicFilter.types'; -// import { ISystemUser } from './User'; -// import { IFilterRole } from './DynamicFilter'; -// import { IAccount } from './Account'; -// import { AttachmentLinkDTO } from './Attachments'; export interface IPaginationMeta { total: number; @@ -23,55 +19,6 @@ export interface IExpensesFilter { filterQuery?: (query: any) => void; } -// export interface IExpense { -// id: number; -// totalAmount: number; -// localAmount?: number; -// currencyCode: string; -// exchangeRate: number; -// description?: string; -// paymentAccountId: number; -// peyeeId?: number; -// referenceNo?: string; -// publishedAt: Date | null; -// userId: number; -// paymentDate: Date; -// payeeId: number; -// landedCostAmount: number; -// allocatedCostAmount: number; -// unallocatedCostAmount: number; -// categories?: IExpenseCategory[]; -// isPublished: boolean; - -// localLandedCostAmount?: number; -// localAllocatedCostAmount?: number; -// localUnallocatedCostAmount?: number; - -// billableAmount: number; -// invoicedAmount: number; - -// branchId?: number; - -// createdAt?: Date; -// } - -// export interface IExpenseCategory { -// id?: number; -// expenseAccountId: number; -// index: number; -// description: string; -// expenseId: number; -// amount: number; - -// projectId?: number; - -// allocatedCostAmount: number; -// unallocatedCostAmount: number; -// landedCost: boolean; - -// expenseAccount?: IAccount; -// } - export interface IExpenseCommonDTO { currencyCode: string; exchangeRate?: number; @@ -103,68 +50,25 @@ export interface IExpenseCategoryDTO { projectId?: number; } -// export interface IExpensesService { -// newExpense( -// tenantid: number, -// expenseDTO: IExpenseDTO, -// authorizedUser: ISystemUser -// ): Promise; - -// editExpense( -// tenantid: number, -// expenseId: number, -// expenseDTO: IExpenseDTO, -// authorizedUser: ISystemUser -// ): void; - -// publishExpense( -// tenantId: number, -// expenseId: number, -// authorizedUser: ISystemUser -// ): Promise; - -// deleteExpense( -// tenantId: number, -// expenseId: number, -// authorizedUser: ISystemUser -// ): Promise; - -// getExpensesList( -// tenantId: number, -// expensesFilter: IExpensesFilter -// ): Promise<{ -// expenses: IExpense[]; -// pagination: IPaginationMeta; -// filterMeta: IFilterMeta; -// }>; - -// getExpense(tenantId: number, expenseId: number): Promise; -// } - export interface IExpenseCreatingPayload { trx: Knex.Transaction; - // tenantId: number; expenseDTO: IExpenseCreateDTO; } export interface IExpenseEventEditingPayload { - // tenantId: number; oldExpense: Expense; expenseDTO: IExpenseEditDTO; trx: Knex.Transaction; } export interface IExpenseCreatedPayload { - // tenantId: number; expenseId: number; - // authorizedUser: ISystemUser; expense: Expense; expenseDTO: IExpenseCreateDTO; trx?: Knex.Transaction; } export interface IExpenseEventEditPayload { - // tenantId: number; expenseId: number; expense: Expense; expenseDTO: IExpenseEditDTO; @@ -174,7 +78,6 @@ export interface IExpenseEventEditPayload { } export interface IExpenseEventDeletePayload { - // tenantId: number; expenseId: number; authorizedUser: SystemUser; oldExpense: Expense; @@ -183,11 +86,9 @@ export interface IExpenseEventDeletePayload { export interface IExpenseDeletingPayload { trx: Knex.Transaction; - // tenantId: number; oldExpense: Expense; } export interface IExpenseEventPublishedPayload { - // tenantId: number; expenseId: number; oldExpense: Expense; expense: Expense; @@ -198,7 +99,6 @@ export interface IExpenseEventPublishedPayload { export interface IExpensePublishingPayload { trx: Knex.Transaction; oldExpense: Expense; - // tenantId: number; } export enum ExpenseAction { Create = 'Create', diff --git a/packages/server-nest/src/modules/Expenses/commands/CommandExpenseDTO.transformer.ts b/packages/server-nest/src/modules/Expenses/commands/CommandExpenseDTO.transformer.ts index 2d5785b7b..5532178f1 100644 --- a/packages/server-nest/src/modules/Expenses/commands/CommandExpenseDTO.transformer.ts +++ b/packages/server-nest/src/modules/Expenses/commands/CommandExpenseDTO.transformer.ts @@ -1,21 +1,24 @@ +import { Injectable } from '@nestjs/common'; import { omit, sumBy } from 'lodash'; -import moment from 'moment'; +import * as moment from 'moment'; import * as R from 'ramda'; import { IExpenseCreateDTO, - IExpenseCommonDTO, IExpenseEditDTO, } from '../interfaces/Expenses.interface'; -// import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; -import { Injectable } from '@nestjs/common'; +import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform'; import { Expense } from '../models/Expense.model'; import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index'; import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; @Injectable() export class ExpenseDTOTransformer { + /** + * @param {BranchTransactionDTOTransformer} branchDTOTransform - Branch transaction DTO transformer. + * @param {TenancyContext} tenancyContext - Tenancy context. + */ constructor( - // private readonly branchDTOTransform: BranchTransactionDTOTransform; + private readonly branchDTOTransform: BranchTransactionDTOTransformer, private readonly tenancyContext: TenancyContext, ) {} @@ -50,7 +53,6 @@ export class ExpenseDTOTransformer { */ private expenseDTOToModel( expenseDTO: IExpenseCreateDTO | IExpenseEditDTO, - // user?: ISystemUser ): Expense { const landedCostAmount = this.getExpenseLandedCostAmount(expenseDTO); const totalAmount = this.getExpenseCategoriesTotal(expenseDTO.categories); @@ -72,10 +74,9 @@ export class ExpenseDTOTransformer { } : {}), }; - return initialDTO; - // return R.compose(this.branchDTOTransform.transformDTO(tenantId))( - // initialDTO - // ); + return R.compose(this.branchDTOTransform.transformDTO)( + initialDTO, + ) as Expense; } /** diff --git a/packages/server-nest/src/modules/Expenses/models/Expense.model.ts b/packages/server-nest/src/modules/Expenses/models/Expense.model.ts index 09cfc4ba1..1ca0433d5 100644 --- a/packages/server-nest/src/modules/Expenses/models/Expense.model.ts +++ b/packages/server-nest/src/modules/Expenses/models/Expense.model.ts @@ -203,21 +203,28 @@ export class Expense extends BaseModel { * Relationship mapping. */ static get relationMappings() { - // const Account = require('models/Account'); + const { Account } = require('../../Accounts/models/Account.model'); const { ExpenseCategory } = require('./ExpenseCategory.model'); - // const Document = require('models/Document'); - // const Branch = require('models/Branch'); + const { Document } = require('../../ChromiumlyTenancy/models/Document'); + const { Branch } = require('../../Branches/models/Branch.model'); // const { MatchedBankTransaction } = require('models/MatchedBankTransaction'); return { - // paymentAccount: { - // relation: Model.BelongsToOneRelation, - // modelClass: Account.default, - // join: { - // from: 'expenses_transactions.paymentAccountId', - // to: 'accounts.id', - // }, - // }, + /** + * Expense transaction may belongs to a payment account. + */ + paymentAccount: { + relation: Model.BelongsToOneRelation, + modelClass: Account, + join: { + from: 'expenses_transactions.paymentAccountId', + to: 'accounts.id', + }, + }, + + /** + * Expense transaction may has many expense categories. + */ categories: { relation: Model.HasManyRelation, modelClass: ExpenseCategory, @@ -233,33 +240,33 @@ export class Expense extends BaseModel { /** * Expense transction may belongs to a branch. */ - // branch: { - // relation: Model.BelongsToOneRelation, - // modelClass: Branch.default, - // join: { - // from: 'expenses_transactions.branchId', - // to: 'branches.id', - // }, - // }, + branch: { + relation: Model.BelongsToOneRelation, + modelClass: Branch, + join: { + from: 'expenses_transactions.branchId', + to: 'branches.id', + }, + }, - // /** - // * Expense transaction may has many attached attachments. - // */ - // attachments: { - // relation: Model.ManyToManyRelation, - // modelClass: Document.default, - // join: { - // from: 'expenses_transactions.id', - // through: { - // from: 'document_links.modelId', - // to: 'document_links.documentId', - // }, - // to: 'documents.id', - // }, - // filter(query) { - // query.where('model_ref', 'Expense'); - // }, - // }, + /** + * Expense transaction may has many attached attachments. + */ + attachments: { + relation: Model.ManyToManyRelation, + modelClass: Document, + join: { + from: 'expenses_transactions.id', + through: { + from: 'document_links.modelId', + to: 'document_links.documentId', + }, + to: 'documents.id', + }, + filter(query) { + query.where('model_ref', 'Expense'); + }, + }, // /** // * Expense may belongs to matched bank transaction. diff --git a/packages/server-nest/src/modules/Expenses/subscribers/ExpenseGL.ts b/packages/server-nest/src/modules/Expenses/subscribers/ExpenseGL.ts index fbc7c4197..08a265a10 100644 --- a/packages/server-nest/src/modules/Expenses/subscribers/ExpenseGL.ts +++ b/packages/server-nest/src/modules/Expenses/subscribers/ExpenseGL.ts @@ -77,7 +77,7 @@ export class ExpenseGL { index: index + 2, projectId: category.projectId, }; - } + }, ); /** @@ -88,8 +88,9 @@ export class ExpenseGL { const getCategoryEntry = this.getExpenseGLCategoryEntry(); const paymentEntry = this.getExpenseGLPaymentEntry(); - const categoryEntries = this.expense.categories.map(getCategoryEntry); - + const categoryEntries = this.expense.categories.map((category, index) => + getCategoryEntry(category, index), + ); return [paymentEntry, ...categoryEntries]; }; diff --git a/packages/server-nest/src/modules/Items/models/ItemEntry.ts b/packages/server-nest/src/modules/Items/models/ItemEntry.ts index e69388a81..042c1e74d 100644 --- a/packages/server-nest/src/modules/Items/models/ItemEntry.ts +++ b/packages/server-nest/src/modules/Items/models/ItemEntry.ts @@ -2,6 +2,7 @@ import { Model } from 'objection'; // import TenantModel from 'models/TenantModel'; // import { getExlusiveTaxAmount, getInclusiveTaxAmount } from '@/utils/taxRate'; import { BaseModel } from '@/models/Model'; +import { Item } from './Item'; export class ItemEntry extends BaseModel { public taxRate: number; @@ -12,10 +13,14 @@ export class ItemEntry extends BaseModel { public itemId: number; public costAccountId: number; public taxRateId: number; + public sellAccountId: number; + public description: string; public landedCost!: boolean; public allocatedCostAmount!: number; + public item!: Item; + /** * Table name. * @returns {string} diff --git a/packages/server-nest/src/modules/Ledger/Ledger.module.ts b/packages/server-nest/src/modules/Ledger/Ledger.module.ts index d186db0e3..1f8bd166b 100644 --- a/packages/server-nest/src/modules/Ledger/Ledger.module.ts +++ b/packages/server-nest/src/modules/Ledger/Ledger.module.ts @@ -3,18 +3,25 @@ import { LedgerStorageService } from './LedgerStorage.service'; import { LedgerEntriesStorageService } from './LedgerEntriesStorage.service'; import { LedgerRevertService } from './LedgerStorageRevert.service'; import { LedgerContactsBalanceStorage } from './LedgerContactStorage.service'; +import { TenancyContext } from '../Tenancy/TenancyContext.service'; +import { LedegrAccountsStorage } from './LedgetAccountStorage.service'; +import { AccountsModule } from '../Accounts/Accounts.module'; @Module({ + imports: [AccountsModule], providers: [ LedgerStorageService, LedgerEntriesStorageService, LedgerRevertService, LedgerContactsBalanceStorage, + LedegrAccountsStorage, + TenancyContext ], exports: [ LedgerStorageService, LedgerEntriesStorageService, LedgerRevertService, + LedegrAccountsStorage ], }) export class LedgerModule {} diff --git a/packages/server-nest/src/modules/Ledger/Ledger.ts b/packages/server-nest/src/modules/Ledger/Ledger.ts index 0a06e8cb6..8824ac143 100644 --- a/packages/server-nest/src/modules/Ledger/Ledger.ts +++ b/packages/server-nest/src/modules/Ledger/Ledger.ts @@ -1,6 +1,9 @@ import moment from 'moment'; import { defaultTo, sumBy, uniqBy } from 'lodash'; -import { IAccountTransaction, ILedger, ILedgerEntry } from '@/interfaces'; +import { ILedger } from './types/Ledger.types'; +import { ILedgerEntry } from './types/Ledger.types'; +import { AccountTransaction } from '../Accounts/models/AccountTransaction.model'; +import { IAccountTransaction } from '@/interfaces/Account'; export class Ledger implements ILedger { readonly entries: ILedgerEntry[]; @@ -225,7 +228,7 @@ export class Ledger implements ILedger { * @param {IAccountTransaction[]} entries * @returns {ILedgerEntry[]} */ - static mappingTransactions(entries: IAccountTransaction[]): ILedgerEntry[] { + static mappingTransactions(entries: AccountTransaction[]): ILedgerEntry[] { return entries.map(this.mapTransaction); } @@ -234,7 +237,7 @@ export class Ledger implements ILedger { * @param {IAccountTransaction} entry * @returns {ILedgerEntry} */ - static mapTransaction(entry: IAccountTransaction): ILedgerEntry { + static mapTransaction(entry: AccountTransaction): ILedgerEntry { return { credit: defaultTo(entry.credit, 0), debit: defaultTo(entry.debit, 0), @@ -274,7 +277,7 @@ export class Ledger implements ILedger { * @param {IAccountTransaction[]} transactions * @returns {ILedger} */ - static fromTransactions(transactions: IAccountTransaction[]): Ledger { + static fromTransactions(transactions: AccountTransaction[]): Ledger { const entries = Ledger.mappingTransactions(transactions); return new Ledger(entries); } diff --git a/packages/server-nest/src/modules/Ledger/LedgerContactStorage.service.ts b/packages/server-nest/src/modules/Ledger/LedgerContactStorage.service.ts index cde2ca314..89429ccd2 100644 --- a/packages/server-nest/src/modules/Ledger/LedgerContactStorage.service.ts +++ b/packages/server-nest/src/modules/Ledger/LedgerContactStorage.service.ts @@ -24,7 +24,7 @@ export class LedgerContactsBalanceStorage { ) {} /** - * + * Saves the contacts balance. * @param {ILedger} ledger * @param {Knex.Transaction} trx * @returns {Promise} @@ -48,7 +48,7 @@ export class LedgerContactsBalanceStorage { }; /** - * + * Saves the contact balance. * @param {ISaleContactsBalanceQueuePayload} task * @returns {Promise} */ @@ -67,7 +67,6 @@ export class LedgerContactsBalanceStorage { * @returns {Promise<(entry: ILedgerEntry) => boolean>} */ private filterARAPLedgerEntris = async ( - tenantId: number, trx?: Knex.Transaction, ): Promise<(entry: ILedgerEntry) => boolean> => { const ARAPAccounts = await this.accountModel @@ -91,14 +90,11 @@ export class LedgerContactsBalanceStorage { * @returns {Promise} */ private saveContactBalance = async ( - tenantId: number, ledger: ILedger, contactId: number, trx?: Knex.Transaction, ): Promise => { const contact = await this.contactModel.query(trx).findById(contactId); - - // Retrieves the given tenant metadata. const tenant = await this.tenancyContext.getTenant(true); // Detarmines whether the contact has foreign currency. @@ -106,10 +102,7 @@ export class LedgerContactsBalanceStorage { contact.currencyCode !== tenant?.metadata.baseCurrency; // Filters the ledger base on the given contact id. - const filterARAPLedgerEntris = await this.filterARAPLedgerEntris( - tenantId, - trx, - ); + const filterARAPLedgerEntris = await this.filterARAPLedgerEntris(trx); const contactLedger = ledger // Filter entries only that have contact id. .whereContactId(contactId) @@ -122,27 +115,25 @@ export class LedgerContactsBalanceStorage { .getForeignClosingBalance() : contactLedger.getClosingBalance(); - await this.changeContactBalance(tenantId, contactId, closingBalance, trx); + await this.changeContactBalance(contactId, closingBalance, trx); }; /** - * - * @param {number} tenantId - * @param {number} contactId - * @param {number} change - * @returns + * Changes the contact receiable/payable balance. + * @param {number} contactId - The contact ID. + * @param {number} change - The change amount. + * @returns {Promise} */ private changeContactBalance = ( - tenantId: number, contactId: number, change: number, trx?: Knex.Transaction, ) => { - return this.contactModel.changeAmount( - { id: contactId }, - 'balance', - change, - trx, - ); + // return this.contactModel.changeAmount( + // { id: contactId }, + // 'balance', + // change, + // trx, + // ); }; } diff --git a/packages/server-nest/src/modules/Ledger/LedgerStorage.service.ts b/packages/server-nest/src/modules/Ledger/LedgerStorage.service.ts index d6e0097f7..3e76020db 100644 --- a/packages/server-nest/src/modules/Ledger/LedgerStorage.service.ts +++ b/packages/server-nest/src/modules/Ledger/LedgerStorage.service.ts @@ -1,17 +1,26 @@ import { Knex } from 'knex'; +import { Inject, Injectable } from '@nestjs/common'; import { ILedger } from './types/Ledger.types'; import { LedgerContactsBalanceStorage } from './LedgerContactStorage.service'; import { LedegrAccountsStorage } from './LedgetAccountStorage.service'; import { LedgerEntriesStorageService } from './LedgerEntriesStorage.service'; +import { AccountTransaction } from '../Accounts/models/AccountTransaction.model'; import { Ledger } from './Ledger'; -import { Injectable } from '@nestjs/common'; @Injectable() export class LedgerStorageService { + /** + * @param {LedgerContactsBalanceStorage} ledgerContactsBalance - Ledger contacts balance storage. + * @param {LedegrAccountsStorage} ledgerAccountsBalance - Ledger accounts balance storage. + * @param {LedgerEntriesStorageService} ledgerEntriesService - Ledger entries storage service. + */ constructor( private ledgerContactsBalance: LedgerContactsBalanceStorage, private ledgerAccountsBalance: LedegrAccountsStorage, private ledgerEntriesService: LedgerEntriesStorageService, + + @Inject(AccountTransaction.name) + private accountTransactionModel: typeof AccountTransaction, ) {} /** @@ -43,10 +52,7 @@ export class LedgerStorageService { * @param {Knex.Transaction} trx * @returns {Promise} */ - public delete = async ( - ledger: ILedger, - trx?: Knex.Transaction, - ) => { + public delete = async (ledger: ILedger, trx?: Knex.Transaction) => { const tasks = [ // Deletes the ledger entries. this.ledgerEntriesService.deleteEntries(ledger, trx), @@ -61,9 +67,10 @@ export class LedgerStorageService { }; /** - * @param {number | number[]} referenceId - * @param {string | string[]} referenceType - * @param {Knex.Transaction} trx + * Deletes the ledger entries by the given reference. + * @param {number | number[]} referenceId - The reference ID. + * @param {string | string[]} referenceType - The reference type. + * @param {Knex.Transaction} trx - The knex transaction. */ public deleteByReference = async ( referenceId: number | number[], @@ -71,15 +78,15 @@ export class LedgerStorageService { trx?: Knex.Transaction, ) => { // Retrieves the transactions of the given reference. - const transactions = - await transactionsRepository.getTransactionsByReference( - referenceId, - referenceType, - ); + const transactions = await this.accountTransactionModel + .query(trx) + .modify('filterByReference', referenceId, referenceType) + .withGraphFetched('account'); + // Creates a new ledger from transaction and reverse the entries. const reversedLedger = Ledger.fromTransactions(transactions).reverse(); // Deletes and reverts the balances. - await this.delete(tenantId, reversedLedger, trx); + await this.delete(reversedLedger, trx); }; } diff --git a/packages/server-nest/src/modules/Ledger/LedgetAccountStorage.service.ts b/packages/server-nest/src/modules/Ledger/LedgetAccountStorage.service.ts index 7eab97944..bdcdc3e4a 100644 --- a/packages/server-nest/src/modules/Ledger/LedgetAccountStorage.service.ts +++ b/packages/server-nest/src/modules/Ledger/LedgetAccountStorage.service.ts @@ -8,6 +8,7 @@ import { import { Inject, Injectable } from '@nestjs/common'; import { Account } from '../Accounts/models/Account.model'; import { AccountRepository } from '../Accounts/repositories/Account.repository'; +import { TenancyContext } from '../Tenancy/TenancyContext.service'; @Injectable() export class LedegrAccountsStorage { @@ -16,10 +17,12 @@ export class LedegrAccountsStorage { * @param {AccountRepository} accountRepository - */ constructor( + private tenancyContext: TenancyContext, + @Inject(Account.name) private accountModel: typeof Account, - @Inject(AccountRepository.name) + @Inject(AccountRepository) private accountRepository: AccountRepository, ) {} @@ -43,7 +46,7 @@ export class LedegrAccountsStorage { }; /** - * + * Finds the dependant accounts ids. * @param {number[]} accountsIds * @returns {number[]} */ @@ -60,9 +63,9 @@ export class LedegrAccountsStorage { /** * Atomic mutation for accounts balances. - * @param {number} tenantId - * @param {ILedger} ledger - * @param {Knex.Transaction} trx - + * @param {number} tenantId + * @param {ILedger} ledger + * @param {Knex.Transaction} trx - * @returns {Promise} */ public saveAccountsBalance = async ( @@ -95,9 +98,9 @@ export class LedegrAccountsStorage { private saveAccountBalanceTask = async ( task: ISaveAccountsBalanceQueuePayload, ): Promise => { - const { tenantId, ledger, accountId, trx } = task; + const { ledger, accountId, trx } = task; - await this.saveAccountBalanceFromLedger(tenantId, ledger, accountId, trx); + await this.saveAccountBalanceFromLedger(ledger, accountId, trx); }; /** @@ -109,7 +112,6 @@ export class LedegrAccountsStorage { * @returns {Promise} */ private saveAccountBalanceFromLedger = async ( - tenantId: number, ledger: ILedger, accountId: number, trx?: Knex.Transaction, @@ -120,10 +122,11 @@ export class LedegrAccountsStorage { const accountLedger = ledger.whereAccountId(accountId); // Retrieves the given tenant metadata. - const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); + const tenant = await this.tenancyContext.getTenant(true); // Detarmines whether the account has foreign currency. - const isAccountForeign = account.currencyCode !== tenantMeta.baseCurrency; + const isAccountForeign = + account.currencyCode !== tenant.metadata?.baseCurrency; // Calculates the closing foreign balance by the given currency if account was has // foreign currency otherwise get closing balance. @@ -133,7 +136,7 @@ export class LedegrAccountsStorage { .getForeignClosingBalance() : accountLedger.getClosingBalance(); - await this.saveAccountBalance(tenantId, accountId, closingBalance, trx); + await this.saveAccountBalance(accountId, closingBalance, trx); }; /** @@ -156,11 +159,11 @@ export class LedegrAccountsStorage { .whereNull('amount') .patch({ amount: 0 }); - await this.accountModel.changeAmount( - { id: accountId }, - 'amount', - change, - trx, - ); + // await this.accountModel.changeAmount( + // { id: accountId }, + // 'amount', + // change, + // trx, + // ); }; } diff --git a/packages/server-nest/src/modules/Ledger/utils.ts b/packages/server-nest/src/modules/Ledger/utils.ts index 2edc25b97..756d90145 100644 --- a/packages/server-nest/src/modules/Ledger/utils.ts +++ b/packages/server-nest/src/modules/Ledger/utils.ts @@ -1,8 +1,9 @@ -import { IAccountTransaction, ILedgerEntry } from '@/interfaces'; +import { AccountTransaction } from "../Accounts/models/AccountTransaction.model"; +import { ILedgerEntry } from "./types/Ledger.types"; export const transformLedgerEntryToTransaction = ( entry: ILedgerEntry -): IAccountTransaction => { +): Partial => { return { date: entry.date, @@ -33,7 +34,7 @@ export const transformLedgerEntryToTransaction = ( itemId: entry.itemId, projectId: entry.projectId, - costable: entry.costable, + // costable: entry.costable, taxRateId: entry.taxRateId, taxRate: entry.taxRate, diff --git a/packages/server-nest/src/modules/ManualJournals/ManualJournals.module.ts b/packages/server-nest/src/modules/ManualJournals/ManualJournals.module.ts index b094fdfe2..112e513cf 100644 --- a/packages/server-nest/src/modules/ManualJournals/ManualJournals.module.ts +++ b/packages/server-nest/src/modules/ManualJournals/ManualJournals.module.ts @@ -12,9 +12,12 @@ import { BranchesModule } from '../Branches/Branches.module'; import { ManualJournalsController } from './ManualJournals.controller'; import { ManualJournalsApplication } from './ManualJournalsApplication.service'; import { GetManualJournal } from './queries/GetManualJournal.service'; +import { ManualJournalWriteGLSubscriber } from './commands/ManualJournalGLEntriesSubscriber'; +import { ManualJournalGLEntries } from './commands/ManualJournalGLEntries'; +import { LedgerModule } from '../Ledger/Ledger.module'; @Module({ - imports: [BranchesModule], + imports: [BranchesModule, LedgerModule], controllers: [ManualJournalsController], providers: [ TenancyContext, @@ -28,7 +31,9 @@ import { GetManualJournal } from './queries/GetManualJournal.service'; ManualJournalBranchesDTOTransformer, AutoIncrementOrdersService, ManualJournalsApplication, - GetManualJournal + GetManualJournal, + ManualJournalGLEntries, + ManualJournalWriteGLSubscriber ], }) export class ManualJournalsModule {} diff --git a/packages/server-nest/src/modules/ManualJournals/commands/ManualJournalGL.ts b/packages/server-nest/src/modules/ManualJournals/commands/ManualJournalGL.ts new file mode 100644 index 000000000..f88ed828c --- /dev/null +++ b/packages/server-nest/src/modules/ManualJournals/commands/ManualJournalGL.ts @@ -0,0 +1,80 @@ +import { Ledger } from '@/modules/Ledger/Ledger'; +import { ManualJournal } from '../models/ManualJournal'; +import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types'; +import { ManualJournalEntry } from '../models/ManualJournalEntry'; + +export class ManualJournalGL { + manualJournal: ManualJournal; + + constructor(manualJournal: ManualJournal) { + this.manualJournal = manualJournal; + } + + /** + * Retrieves the ledger of the given manual journal. + * @param {ManualJournal} manualJournal - The manual journal. + * @returns {Ledger} + */ + public getManualJournalGLedger = () => { + const entries = this.getManualJournalGLEntries(); + + return new Ledger(entries); + }; + + /** + * Retrieves the common entry details of the manual journal + * @param {IManualJournal} manualJournal - The manual journal. + * @returns {Partial} + */ + public get manualJournalCommonEntry() { + return { + transactionNumber: this.manualJournal.journalNumber, + referenceNumber: this.manualJournal.reference, + createdAt: this.manualJournal.createdAt, + date: this.manualJournal.date, + currencyCode: this.manualJournal.currencyCode, + exchangeRate: this.manualJournal.exchangeRate, + + transactionType: 'Journal', + transactionId: this.manualJournal.id, + + userId: this.manualJournal.userId, + }; + } + + /** + * Retrieves the ledger entry of the given manual journal and + * its associated entry. + * @param {IManualJournal} manualJournal - The manual journal. + * @param {IManualJournalEntry} entry - The manual journal entry. + * @returns {ILedgerEntry} + */ + public getManualJournalEntry(entry: ManualJournalEntry): ILedgerEntry { + const commonEntry = this.manualJournalCommonEntry; + + return { + ...commonEntry, + debit: entry.debit, + credit: entry.credit, + accountId: entry.accountId, + + contactId: entry.contactId, + note: entry.note, + + index: entry.index, + accountNormal: entry.account.accountNormal, + + branchId: entry.branchId, + projectId: entry.projectId, + }; + } + + /** + * Retrieves the ledger entries of the given manual journal. + * @param {IManualJournal} manualJournal - The manual journal. + * @returns {ILedgerEntry[]} + */ + public getManualJournalGLEntries = (): ILedgerEntry[] => { + return this.manualJournal.entries.map(this.getManualJournalEntry).flat(); + }; +} diff --git a/packages/server-nest/src/modules/ManualJournals/commands/ManualJournalGLEntries.ts b/packages/server-nest/src/modules/ManualJournals/commands/ManualJournalGLEntries.ts index 38f33e1cf..917051158 100644 --- a/packages/server-nest/src/modules/ManualJournals/commands/ManualJournalGLEntries.ts +++ b/packages/server-nest/src/modules/ManualJournals/commands/ManualJournalGLEntries.ts @@ -1,161 +1,72 @@ -// import { Service, Inject } from 'typedi'; -// import * as R from 'ramda'; -// import { -// IManualJournal, -// IManualJournalEntry, -// ILedgerEntry, -// } from '@/interfaces'; -// import { Knex } from 'knex'; -// import Ledger from '@/services/Accounting/Ledger'; -// import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; -// import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { Knex } from 'knex'; +import { Inject, Injectable } from '@nestjs/common'; +import { LedgerStorageService } from '@/modules/Ledger/LedgerStorage.service'; +import { ManualJournal } from '../models/ManualJournal'; +import { ManualJournalGL } from './ManualJournalGL'; -// @Service() -// export class ManualJournalGLEntries { -// @Inject() -// private ledgerStorage: LedgerStorageService; +@Injectable() +export class ManualJournalGLEntries { + /** + * @param {typeof ManualJournal} manualJournalModel - The manual journal model. + * @param {LedgerStorageService} ledgerStorage - The ledger storage service. + */ + constructor( + @Inject(ManualJournal.name) + private readonly manualJournalModel: typeof ManualJournal, + private readonly ledgerStorage: LedgerStorageService, + ) {} -// @Inject() -// private tenancy: HasTenancyService; + /** + * Create manual journal GL entries. + * @param {number} manualJournalId - The manual journal ID. + * @param {Knex.Transaction} trx - The knex transaction. + */ + public createManualJournalGLEntries = async ( + manualJournalId: number, + trx?: Knex.Transaction, + ) => { + // Retrieves the given manual journal with associated entries. + const manualJournal = await this.manualJournalModel + .query(trx) + .findById(manualJournalId) + .withGraphFetched('entries.account'); -// /** -// * Create manual journal GL entries. -// * @param {number} tenantId -// * @param {number} manualJournalId -// * @param {Knex.Transaction} trx -// */ -// public createManualJournalGLEntries = async ( -// tenantId: number, -// manualJournalId: number, -// trx?: Knex.Transaction -// ) => { -// const { ManualJournal } = this.tenancy.models(tenantId); + // Retrieves the ledger entries of the given manual journal. + const ledger = new ManualJournalGL(manualJournal).getManualJournalGLedger(); -// // Retrieves the given manual journal with associated entries. -// const manualJournal = await ManualJournal.query(trx) -// .findById(manualJournalId) -// .withGraphFetched('entries.account'); + // Commits the given ledger on the storage. + await this.ledgerStorage.commit(ledger, trx); + }; -// // Retrieves the ledger entries of the given manual journal. -// const ledger = this.getManualJournalGLedger(manualJournal); + /** + * Edits manual journal GL entries. + * @param {number} manualJournalId - The manual journal ID. + * @param {Knex.Transaction} trx - The knex transaction. + */ + public editManualJournalGLEntries = async ( + manualJournalId: number, + trx?: Knex.Transaction, + ) => { + // Reverts the manual journal GL entries. + await this.revertManualJournalGLEntries(manualJournalId, trx); -// // Commits the given ledger on the storage. -// await this.ledgerStorage.commit(tenantId, ledger, trx); -// }; + // Write the manual journal GL entries. + await this.createManualJournalGLEntries(manualJournalId, trx); + }; -// /** -// * Edits manual journal GL entries. -// * @param {number} tenantId -// * @param {number} manualJournalId -// * @param {Knex.Transaction} trx -// */ -// public editManualJournalGLEntries = async ( -// tenantId: number, -// manualJournalId: number, -// trx?: Knex.Transaction -// ) => { -// // Reverts the manual journal GL entries. -// await this.revertManualJournalGLEntries(tenantId, manualJournalId, trx); - -// // Write the manual journal GL entries. -// await this.createManualJournalGLEntries(tenantId, manualJournalId, trx); -// }; - -// /** -// * Deletes the manual journal GL entries. -// * @param {number} tenantId -// * @param {number} manualJournalId -// * @param {Knex.Transaction} trx -// */ -// public revertManualJournalGLEntries = async ( -// tenantId: number, -// manualJournalId: number, -// trx?: Knex.Transaction -// ): Promise => { -// return this.ledgerStorage.deleteByReference( -// tenantId, -// manualJournalId, -// 'Journal', -// trx -// ); -// }; - -// /** -// * Retrieves the ledger of the given manual journal. -// * @param {IManualJournal} manualJournal -// * @returns {Ledger} -// */ -// private getManualJournalGLedger = (manualJournal: IManualJournal) => { -// const entries = this.getManualJournalGLEntries(manualJournal); - -// return new Ledger(entries); -// }; - -// /** -// * Retrieves the common entry details of the manual journal -// * @param {IManualJournal} manualJournal -// * @returns {Partial} -// */ -// private getManualJournalCommonEntry = ( -// manualJournal: IManualJournal -// ): Partial => { -// return { -// transactionNumber: manualJournal.journalNumber, -// referenceNumber: manualJournal.reference, -// createdAt: manualJournal.createdAt, -// date: manualJournal.date, -// currencyCode: manualJournal.currencyCode, -// exchangeRate: manualJournal.exchangeRate, - -// transactionType: 'Journal', -// transactionId: manualJournal.id, - -// userId: manualJournal.userId, -// }; -// }; - -// /** -// * Retrieves the ledger entry of the given manual journal and -// * its associated entry. -// * @param {IManualJournal} manualJournal - -// * @param {IManualJournalEntry} entry - -// * @returns {ILedgerEntry} -// */ -// private getManualJournalEntry = R.curry( -// ( -// manualJournal: IManualJournal, -// entry: IManualJournalEntry -// ): ILedgerEntry => { -// const commonEntry = this.getManualJournalCommonEntry(manualJournal); - -// return { -// ...commonEntry, -// debit: entry.debit, -// credit: entry.credit, -// accountId: entry.accountId, - -// contactId: entry.contactId, -// note: entry.note, - -// index: entry.index, -// accountNormal: entry.account.accountNormal, - -// branchId: entry.branchId, -// projectId: entry.projectId, -// }; -// } -// ); - -// /** -// * Retrieves the ledger of the given manual journal. -// * @param {IManualJournal} manualJournal -// * @returns {ILedgerEntry[]} -// */ -// private getManualJournalGLEntries = ( -// manualJournal: IManualJournal -// ): ILedgerEntry[] => { -// const transformEntry = this.getManualJournalEntry(manualJournal); - -// return manualJournal.entries.map(transformEntry).flat(); -// }; -// } + /** + * Deletes the manual journal GL entries. + * @param {number} manualJournalId - The manual journal ID. + * @param {Knex.Transaction} trx - The knex transaction. + */ + public revertManualJournalGLEntries = async ( + manualJournalId: number, + trx?: Knex.Transaction, + ): Promise => { + return this.ledgerStorage.deleteByReference( + manualJournalId, + 'Journal', + trx, + ); + }; +} diff --git a/packages/server-nest/src/modules/ManualJournals/commands/ManualJournalGLEntriesSubscriber.ts b/packages/server-nest/src/modules/ManualJournals/commands/ManualJournalGLEntriesSubscriber.ts index c3acb306b..06d7a8643 100644 --- a/packages/server-nest/src/modules/ManualJournals/commands/ManualJournalGLEntriesSubscriber.ts +++ b/packages/server-nest/src/modules/ManualJournals/commands/ManualJournalGLEntriesSubscriber.ts @@ -1,131 +1,102 @@ -// import { Inject } from 'typedi'; -// import { EventSubscriber } from 'event-dispatch'; -// import { -// IManualJournalEventCreatedPayload, -// IManualJournalEventEditedPayload, -// IManualJournalEventPublishedPayload, -// IManualJournalEventDeletedPayload, -// } from '@/interfaces'; -// import events from '@/subscribers/events'; -// import { ManualJournalGLEntries } from './ManualJournalGLEntries'; -// import { AutoIncrementManualJournal } from './AutoIncrementManualJournal.service'; +import { OnEvent } from '@nestjs/event-emitter'; +import { Injectable } from '@nestjs/common'; +import { + IManualJournalEventCreatedPayload, + IManualJournalEventEditedPayload, + IManualJournalEventPublishedPayload, + IManualJournalEventDeletedPayload, +} from '../types/ManualJournals.types'; +import { ManualJournalGLEntries } from './ManualJournalGLEntries'; +import { AutoIncrementManualJournal } from './AutoIncrementManualJournal.service'; +import { events } from '@/common/events/events'; -// @EventSubscriber() -// export class ManualJournalWriteGLSubscriber { -// @Inject() -// private manualJournalGLEntries: ManualJournalGLEntries; +@Injectable() +export class ManualJournalWriteGLSubscriber { + /** + * @param {ManualJournalGLEntries} manualJournalGLEntries - The manual journal GL entries service. + * @param {AutoIncrementManualJournal} manualJournalAutoIncrement - The manual journal auto increment service. + */ + constructor( + private manualJournalGLEntries: ManualJournalGLEntries, + private manualJournalAutoIncrement: AutoIncrementManualJournal, + ) {} -// @Inject() -// private manualJournalAutoIncrement: AutoIncrementManualJournal; + /** + * Handle manual journal created event. + * @param {IManualJournalEventCreatedPayload} payload - + * @returns {Promise} + */ + @OnEvent(events.manualJournals.onCreated) + public async handleWriteJournalEntriesOnCreated({ + manualJournal, + trx, + }: IManualJournalEventCreatedPayload) { + // Ingore writing manual journal journal entries in case was not published. + if (!manualJournal.publishedAt) return; -// /** -// * Attaches events with handlers. -// * @param bus -// */ -// public attach(bus) { -// bus.subscribe( -// events.manualJournals.onCreated, -// this.handleWriteJournalEntriesOnCreated -// ); -// bus.subscribe( -// events.manualJournals.onCreated, -// this.handleJournalNumberIncrement -// ); -// bus.subscribe( -// events.manualJournals.onEdited, -// this.handleRewriteJournalEntriesOnEdited -// ); -// bus.subscribe( -// events.manualJournals.onPublished, -// this.handleWriteJournalEntriesOnPublished -// ); -// bus.subscribe( -// events.manualJournals.onDeleted, -// this.handleRevertJournalEntries -// ); -// } + await this.manualJournalGLEntries.createManualJournalGLEntries( + manualJournal.id, + trx, + ); + } -// /** -// * Handle manual journal created event. -// * @param {IManualJournalEventCreatedPayload} payload - -// * @returns {Promise} -// */ -// private handleWriteJournalEntriesOnCreated = async ({ -// tenantId, -// manualJournal, -// trx, -// }: IManualJournalEventCreatedPayload) => { -// // Ingore writing manual journal journal entries in case was not published. -// if (!manualJournal.publishedAt) return; + /** + * Handles the manual journal next number increment once the journal be created. + * @param {IManualJournalEventCreatedPayload} payload - + * @return {Promise} + */ + @OnEvent(events.manualJournals.onCreated) + public async handleJournalNumberIncrement({}: IManualJournalEventCreatedPayload) { + await this.manualJournalAutoIncrement.incrementNextJournalNumber(); + } -// await this.manualJournalGLEntries.createManualJournalGLEntries( -// tenantId, -// manualJournal.id, -// trx -// ); -// }; + /** + * Handle manual journal edited event. + * @param {IManualJournalEventEditedPayload} + * @return {Promise} + */ + @OnEvent(events.manualJournals.onEdited) + public async handleRewriteJournalEntriesOnEdited({ + manualJournal, + oldManualJournal, + trx, + }: IManualJournalEventEditedPayload) { + if (manualJournal.publishedAt) { + await this.manualJournalGLEntries.editManualJournalGLEntries( + manualJournal.id, + trx, + ); + } + } -// /** -// * Handles the manual journal next number increment once the journal be created. -// * @param {IManualJournalEventCreatedPayload} payload - -// * @return {Promise} -// */ -// private handleJournalNumberIncrement = async ({ -// tenantId, -// }: IManualJournalEventCreatedPayload) => { -// await this.manualJournalAutoIncrement.incrementNextJournalNumber(tenantId); -// }; + /** + * Handles writing journal entries once the manula journal publish. + * @param {IManualJournalEventPublishedPayload} payload - + * @return {Promise} + */ + @OnEvent(events.manualJournals.onPublished) + public async handleWriteJournalEntriesOnPublished({ + manualJournal, + trx, + }: IManualJournalEventPublishedPayload) { + await this.manualJournalGLEntries.createManualJournalGLEntries( + manualJournal.id, + trx, + ); + } -// /** -// * Handle manual journal edited event. -// * @param {IManualJournalEventEditedPayload} -// * @return {Promise} -// */ -// private handleRewriteJournalEntriesOnEdited = async ({ -// tenantId, -// manualJournal, -// oldManualJournal, -// trx, -// }: IManualJournalEventEditedPayload) => { -// if (manualJournal.publishedAt) { -// await this.manualJournalGLEntries.editManualJournalGLEntries( -// tenantId, -// manualJournal.id, -// trx -// ); -// } -// }; - -// /** -// * Handles writing journal entries once the manula journal publish. -// * @param {IManualJournalEventPublishedPayload} payload - -// * @return {Promise} -// */ -// private handleWriteJournalEntriesOnPublished = async ({ -// tenantId, -// manualJournal, -// trx, -// }: IManualJournalEventPublishedPayload) => { -// await this.manualJournalGLEntries.createManualJournalGLEntries( -// tenantId, -// manualJournal.id, -// trx -// ); -// }; - -// /** -// * Handle manual journal deleted event. -// * @param {IManualJournalEventDeletedPayload} payload - -// */ -// private handleRevertJournalEntries = async ({ -// tenantId, -// manualJournalId, -// trx, -// }: IManualJournalEventDeletedPayload) => { -// await this.manualJournalGLEntries.revertManualJournalGLEntries( -// tenantId, -// manualJournalId, -// trx -// ); -// }; -// } + /** + * Handle manual journal deleted event. + * @param {IManualJournalEventDeletedPayload} payload - + */ + @OnEvent(events.manualJournals.onDeleted) + public async handleRevertJournalEntries({ + manualJournalId, + trx, + }: IManualJournalEventDeletedPayload) { + await this.manualJournalGLEntries.revertManualJournalGLEntries( + manualJournalId, + trx, + ); + } +} diff --git a/packages/server-nest/src/modules/ManualJournals/models/ManualJournalEntry.ts b/packages/server-nest/src/modules/ManualJournals/models/ManualJournalEntry.ts index 89161c405..5898dd4b4 100644 --- a/packages/server-nest/src/modules/ManualJournals/models/ManualJournalEntry.ts +++ b/packages/server-nest/src/modules/ManualJournals/models/ManualJournalEntry.ts @@ -1,5 +1,8 @@ import { Model } from 'objection'; import { BaseModel } from '@/models/Model'; +import { Account } from '@/modules/Accounts/models/Account.model'; +import { Contact } from '@/modules/Contacts/models/Contact'; +import { Branch } from '@/modules/Branches/models/Branch.model'; export class ManualJournalEntry extends BaseModel { index: number; @@ -12,6 +15,10 @@ export class ManualJournalEntry extends BaseModel { branchId!: number; projectId?: number; + contact?: Contact; + account?: Account; + branch?: Branch; + /** * Table name. */ diff --git a/packages/server-nest/src/modules/PaymentReceived/commands/PaymentReceivedGL.ts b/packages/server-nest/src/modules/PaymentReceived/commands/PaymentReceivedGL.ts new file mode 100644 index 000000000..cb605b768 --- /dev/null +++ b/packages/server-nest/src/modules/PaymentReceived/commands/PaymentReceivedGL.ts @@ -0,0 +1,181 @@ +import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types'; +import { PaymentReceived } from '../models/PaymentReceived'; +import { sumBy } from 'lodash'; +import { AccountNormal } from '@/interfaces/Account'; +import { Ledger } from '@/modules/Ledger/Ledger'; + +export class PaymentReceivedGL { + private readonly paymentReceived: PaymentReceived; + private ARAccountId: number; + private exchangeGainOrLossAccountId: number; + private baseCurrencyCode: string; + + /** + * Constructor method. + * @param {PaymentReceived} paymentReceived - Payment received. + */ + constructor(paymentReceived: PaymentReceived) { + this.paymentReceived = paymentReceived; + } + + setARAccountId(ARAccountId: number) { + this.ARAccountId = ARAccountId; + return this; + } + + setExchangeGainOrLossAccountId(exchangeGainOrLossAccountId: number) { + this.exchangeGainOrLossAccountId = exchangeGainOrLossAccountId; + return this; + } + + setBaseCurrencyCode(baseCurrencyCode: string) { + this.baseCurrencyCode = baseCurrencyCode; + return this; + } + + /** + * Calculates the payment total exchange gain/loss. + * @param {IBillPayment} paymentReceive - Payment receive with entries. + * @returns {number} + */ + private paymentExGainOrLoss = (): number => { + return sumBy(this.paymentReceived.entries, (entry) => { + const paymentLocalAmount = + entry.paymentAmount * this.paymentReceived.exchangeRate; + const invoicePayment = entry.paymentAmount * entry.invoice.exchangeRate; + + return paymentLocalAmount - invoicePayment; + }); + }; + + /** + * Retrieves the common entry of payment receive. + */ + private get paymentReceiveCommonEntry() { + return { + debit: 0, + credit: 0, + + currencyCode: this.paymentReceived.currencyCode, + exchangeRate: this.paymentReceived.exchangeRate, + + transactionId: this.paymentReceived.id, + transactionType: 'PaymentReceive', + + transactionNumber: this.paymentReceived.paymentReceiveNo, + referenceNumber: this.paymentReceived.referenceNo, + + date: this.paymentReceived.paymentDate, + userId: this.paymentReceived.userId, + createdAt: this.paymentReceived.createdAt, + + branchId: this.paymentReceived.branchId, + }; + } + + /** + * Retrieves the payment exchange gain/loss entry. + * @param {IPaymentReceived} paymentReceive - + * @param {number} ARAccountId - + * @param {number} exchangeGainOrLossAccountId - + * @param {string} baseCurrencyCode - + * @returns {ILedgerEntry[]} + */ + private get paymentExchangeGainLossEntry(): ILedgerEntry[] { + const commonJournal = this.paymentReceiveCommonEntry; + const gainOrLoss = this.paymentExGainOrLoss(); + const absGainOrLoss = Math.abs(gainOrLoss); + + return gainOrLoss + ? [ + { + ...commonJournal, + currencyCode: this.baseCurrencyCode, + exchangeRate: 1, + debit: gainOrLoss > 0 ? absGainOrLoss : 0, + credit: gainOrLoss < 0 ? absGainOrLoss : 0, + accountId: this.ARAccountId, + contactId: this.paymentReceived.customerId, + index: 3, + accountNormal: AccountNormal.CREDIT, + }, + { + ...commonJournal, + currencyCode: this.baseCurrencyCode, + exchangeRate: 1, + credit: gainOrLoss > 0 ? absGainOrLoss : 0, + debit: gainOrLoss < 0 ? absGainOrLoss : 0, + accountId: this.exchangeGainOrLossAccountId, + index: 3, + accountNormal: AccountNormal.DEBIT, + }, + ] + : []; + } + + /** + * Retrieves the payment deposit GL entry. + * @param {IPaymentReceived} paymentReceive + * @returns {ILedgerEntry} + */ + private get paymentDepositGLEntry(): ILedgerEntry { + const commonJournal = this.paymentReceiveCommonEntry; + + return { + ...commonJournal, + debit: this.paymentReceived.localAmount, + accountId: this.paymentReceived.depositAccountId, + index: 2, + accountNormal: AccountNormal.DEBIT, + }; + } + + /** + * Retrieves the payment receivable entry. + * @param {IPaymentReceived} paymentReceive + * @param {number} ARAccountId + * @returns {ILedgerEntry} + */ + private get paymentReceivableEntry(): ILedgerEntry { + const commonJournal = this.paymentReceiveCommonEntry; + + return { + ...commonJournal, + credit: this.paymentReceived.localAmount, + contactId: this.paymentReceived.customerId, + accountId: this.ARAccountId, + index: 1, + accountNormal: AccountNormal.DEBIT, + }; + } + + /** + * Records payment receive journal transactions. + * + * Invoice payment journals. + * -------- + * - Account receivable -> Debit + * - Payment account [current asset] -> Credit + * @returns {Promise} + */ + public GLEntries(): ILedgerEntry[] { + // Retrieve the payment deposit entry. + const paymentDepositEntry = this.paymentDepositGLEntry; + + // Retrieves the A/R entry. + const receivableEntry = this.paymentReceivableEntry; + + // Exchange gain/loss entries. + const gainLossEntries = this.paymentExchangeGainLossEntry; + + return [paymentDepositEntry, receivableEntry, ...gainLossEntries]; + } + + /** + * Retrieves the payment receive ledger. + * @returns {Ledger} + */ + public getLedger = (): Ledger => { + return new Ledger(this.GLEntries()); + }; +} diff --git a/packages/server-nest/src/modules/PaymentReceived/commands/PaymentReceivedGLEntries.ts b/packages/server-nest/src/modules/PaymentReceived/commands/PaymentReceivedGLEntries.ts index 1679d5059..c567ea3f1 100644 --- a/packages/server-nest/src/modules/PaymentReceived/commands/PaymentReceivedGLEntries.ts +++ b/packages/server-nest/src/modules/PaymentReceived/commands/PaymentReceivedGLEntries.ts @@ -1,299 +1,110 @@ -// import { Service, Inject } from 'typedi'; -// import { sumBy } from 'lodash'; -// import { Knex } from 'knex'; -// import Ledger from '@/services/Accounting/Ledger'; -// import TenancyService from '@/services/Tenancy/TenancyService'; -// import { -// IPaymentReceived, -// ILedgerEntry, -// AccountNormal, -// IPaymentReceiveGLCommonEntry, -// } from '@/interfaces'; -// import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; -// import { TenantMetadata } from '@/system/models'; +import { Knex } from 'knex'; +import { PaymentReceivedGL } from './PaymentReceivedGL'; +import { PaymentReceived } from '../models/PaymentReceived'; +import { LedgerStorageService } from '@/modules/Ledger/LedgerStorage.service'; +import { Ledger } from '@/modules/Ledger/Ledger'; +import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository'; +import { Injectable } from '@nestjs/common'; +import { Inject } from '@nestjs/common'; +import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; -// @Service() -// export class PaymentReceivedGLEntries { -// @Inject() -// private tenancy: TenancyService; +@Injectable() +export class PaymentReceivedGLEntries { + constructor( + private readonly ledgerStorage: LedgerStorageService, + private readonly accountRepository: AccountRepository, + private readonly tenancyContext: TenancyContext, -// @Inject() -// private ledgerStorage: LedgerStorageService; + @Inject(PaymentReceived.name) + private readonly paymentReceivedModel: typeof PaymentReceived, + ) {} -// /** -// * Writes payment GL entries to the storage. -// * @param {number} tenantId -// * @param {number} paymentReceiveId -// * @param {Knex.Transaction} trx -// * @returns {Promise} -// */ -// public writePaymentGLEntries = async ( -// tenantId: number, -// paymentReceiveId: number, -// trx?: Knex.Transaction -// ): Promise => { -// const { PaymentReceive } = this.tenancy.models(tenantId); + /** + * Writes payment GL entries to the storage. + * @param {number} paymentReceiveId - Payment received id. + * @param {Knex.Transaction} trx - Knex transaction. + * @returns {Promise} + */ + public writePaymentGLEntries = async ( + paymentReceiveId: number, + trx?: Knex.Transaction + ): Promise => { + // Retrieves the given tenant metadata. + const tenantMeta = await this.tenancyContext.getTenantMetadata(); -// // Retrieves the given tenant metadata. -// const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); + // Retrieves the payment receive with associated entries. + const paymentReceive = await this.paymentReceivedModel + .query(trx) + .findById(paymentReceiveId) + .withGraphFetched('entries.invoice'); -// // Retrieves the payment receive with associated entries. -// const paymentReceive = await PaymentReceive.query(trx) -// .findById(paymentReceiveId) -// .withGraphFetched('entries.invoice'); + // Retrives the payment receive ledger. + const ledger = await this.getPaymentReceiveGLedger( + paymentReceive, + tenantMeta.baseCurrency, + ); + // Commit the ledger entries to the storage. + await this.ledgerStorage.commit(ledger, trx); + }; -// // Retrives the payment receive ledger. -// const ledger = await this.getPaymentReceiveGLedger( -// tenantId, -// paymentReceive, -// tenantMeta.baseCurrency, -// trx -// ); -// // Commit the ledger entries to the storage. -// await this.ledgerStorage.commit(tenantId, ledger, trx); -// }; + /** + * Reverts the given payment receive GL entries. + * @param {number} paymentReceiveId - Payment received id. + * @param {Knex.Transaction} trx - Knex transaction. + */ + public revertPaymentGLEntries = async ( + paymentReceiveId: number, + trx?: Knex.Transaction + ) => { + await this.ledgerStorage.deleteByReference( + paymentReceiveId, + 'PaymentReceive', + trx + ); + }; -// /** -// * Reverts the given payment receive GL entries. -// * @param {number} tenantId -// * @param {number} paymentReceiveId -// * @param {Knex.Transaction} trx -// */ -// public revertPaymentGLEntries = async ( -// tenantId: number, -// paymentReceiveId: number, -// trx?: Knex.Transaction -// ) => { -// await this.ledgerStorage.deleteByReference( -// tenantId, -// paymentReceiveId, -// 'PaymentReceive', -// trx -// ); -// }; + /** + * Rewrites the given payment receive GL entries. + * @param {number} paymentReceiveId - Payment received id. + * @param {Knex.Transaction} trx - Knex transaction. + */ + public rewritePaymentGLEntries = async ( + paymentReceiveId: number, + trx?: Knex.Transaction + ) => { + // Reverts the payment GL entries. + await this.revertPaymentGLEntries(paymentReceiveId, trx); -// /** -// * Rewrites the given payment receive GL entries. -// * @param {number} tenantId -// * @param {number} paymentReceiveId -// * @param {Knex.Transaction} trx -// */ -// public rewritePaymentGLEntries = async ( -// tenantId: number, -// paymentReceiveId: number, -// trx?: Knex.Transaction -// ) => { -// // Reverts the payment GL entries. -// await this.revertPaymentGLEntries(tenantId, paymentReceiveId, trx); + // Writes the payment GL entries. + await this.writePaymentGLEntries(paymentReceiveId, trx); + }; -// // Writes the payment GL entries. -// await this.writePaymentGLEntries(tenantId, paymentReceiveId, trx); -// }; + /** + * Retrieves the payment receive general ledger. + * @param {IPaymentReceived} paymentReceive - Payment received. + * @param {string} baseCurrencyCode - Base currency code. + * @param {Knex.Transaction} trx - Knex transaction. + * @returns {Ledger} + */ + public getPaymentReceiveGLedger = async ( + paymentReceive: PaymentReceived, + baseCurrencyCode: string, + ): Promise => { + // Retrieve the A/R account of the given currency. + const receivableAccount = + await this.accountRepository.findOrCreateAccountReceivable( + paymentReceive.currencyCode + ); + // Exchange gain/loss account. + const exGainLossAccount = await this.accountRepository.findBySlug( + 'exchange-grain-loss' + ); + const paymentReceivedGL = new PaymentReceivedGL(paymentReceive) + .setARAccountId(receivableAccount.id) + .setExchangeGainOrLossAccountId(exGainLossAccount.id) + .setBaseCurrencyCode(baseCurrencyCode); -// /** -// * Retrieves the payment receive general ledger. -// * @param {number} tenantId - -// * @param {IPaymentReceived} paymentReceive - -// * @param {string} baseCurrencyCode - -// * @param {Knex.Transaction} trx - -// * @returns {Ledger} -// */ -// public getPaymentReceiveGLedger = async ( -// tenantId: number, -// paymentReceive: IPaymentReceived, -// baseCurrencyCode: string, -// trx?: Knex.Transaction -// ): Promise => { -// const { Account } = this.tenancy.models(tenantId); -// const { accountRepository } = this.tenancy.repositories(tenantId); + return paymentReceivedGL.getLedger(); + }; -// // Retrieve the A/R account of the given currency. -// const receivableAccount = -// await accountRepository.findOrCreateAccountReceivable( -// paymentReceive.currencyCode -// ); -// // Exchange gain/loss account. -// const exGainLossAccount = await Account.query(trx).modify( -// 'findBySlug', -// 'exchange-grain-loss' -// ); -// const ledgerEntries = this.getPaymentReceiveGLEntries( -// paymentReceive, -// receivableAccount.id, -// exGainLossAccount.id, -// baseCurrencyCode -// ); -// return new Ledger(ledgerEntries); -// }; - -// /** -// * Calculates the payment total exchange gain/loss. -// * @param {IBillPayment} paymentReceive - Payment receive with entries. -// * @returns {number} -// */ -// private getPaymentExGainOrLoss = ( -// paymentReceive: IPaymentReceived -// ): number => { -// return sumBy(paymentReceive.entries, (entry) => { -// const paymentLocalAmount = -// entry.paymentAmount * paymentReceive.exchangeRate; -// const invoicePayment = entry.paymentAmount * entry.invoice.exchangeRate; - -// return paymentLocalAmount - invoicePayment; -// }); -// }; - -// /** -// * Retrieves the common entry of payment receive. -// * @param {IPaymentReceived} paymentReceive -// * @returns {} -// */ -// private getPaymentReceiveCommonEntry = ( -// paymentReceive: IPaymentReceived -// ): IPaymentReceiveGLCommonEntry => { -// return { -// debit: 0, -// credit: 0, - -// currencyCode: paymentReceive.currencyCode, -// exchangeRate: paymentReceive.exchangeRate, - -// transactionId: paymentReceive.id, -// transactionType: 'PaymentReceive', - -// transactionNumber: paymentReceive.paymentReceiveNo, -// referenceNumber: paymentReceive.referenceNo, - -// date: paymentReceive.paymentDate, -// userId: paymentReceive.userId, -// createdAt: paymentReceive.createdAt, - -// branchId: paymentReceive.branchId, -// }; -// }; - -// /** -// * Retrieves the payment exchange gain/loss entry. -// * @param {IPaymentReceived} paymentReceive - -// * @param {number} ARAccountId - -// * @param {number} exchangeGainOrLossAccountId - -// * @param {string} baseCurrencyCode - -// * @returns {ILedgerEntry[]} -// */ -// private getPaymentExchangeGainLossEntry = ( -// paymentReceive: IPaymentReceived, -// ARAccountId: number, -// exchangeGainOrLossAccountId: number, -// baseCurrencyCode: string -// ): ILedgerEntry[] => { -// const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceive); -// const gainOrLoss = this.getPaymentExGainOrLoss(paymentReceive); -// const absGainOrLoss = Math.abs(gainOrLoss); - -// return gainOrLoss -// ? [ -// { -// ...commonJournal, -// currencyCode: baseCurrencyCode, -// exchangeRate: 1, -// debit: gainOrLoss > 0 ? absGainOrLoss : 0, -// credit: gainOrLoss < 0 ? absGainOrLoss : 0, -// accountId: ARAccountId, -// contactId: paymentReceive.customerId, -// index: 3, -// accountNormal: AccountNormal.CREDIT, -// }, -// { -// ...commonJournal, -// currencyCode: baseCurrencyCode, -// exchangeRate: 1, -// credit: gainOrLoss > 0 ? absGainOrLoss : 0, -// debit: gainOrLoss < 0 ? absGainOrLoss : 0, -// accountId: exchangeGainOrLossAccountId, -// index: 3, -// accountNormal: AccountNormal.DEBIT, -// }, -// ] -// : []; -// }; - -// /** -// * Retrieves the payment deposit GL entry. -// * @param {IPaymentReceived} paymentReceive -// * @returns {ILedgerEntry} -// */ -// private getPaymentDepositGLEntry = ( -// paymentReceive: IPaymentReceived -// ): ILedgerEntry => { -// const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceive); - -// return { -// ...commonJournal, -// debit: paymentReceive.localAmount, -// accountId: paymentReceive.depositAccountId, -// index: 2, -// accountNormal: AccountNormal.DEBIT, -// }; -// }; - -// /** -// * Retrieves the payment receivable entry. -// * @param {IPaymentReceived} paymentReceive -// * @param {number} ARAccountId -// * @returns {ILedgerEntry} -// */ -// private getPaymentReceivableEntry = ( -// paymentReceive: IPaymentReceived, -// ARAccountId: number -// ): ILedgerEntry => { -// const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceive); - -// return { -// ...commonJournal, -// credit: paymentReceive.localAmount, -// contactId: paymentReceive.customerId, -// accountId: ARAccountId, -// index: 1, -// accountNormal: AccountNormal.DEBIT, -// }; -// }; - -// /** -// * Records payment receive journal transactions. -// * -// * Invoice payment journals. -// * -------- -// * - Account receivable -> Debit -// * - Payment account [current asset] -> Credit -// * -// * @param {number} tenantId -// * @param {IPaymentReceived} paymentRecieve - Payment receive model. -// * @param {number} ARAccountId - A/R account id. -// * @param {number} exGainOrLossAccountId - Exchange gain/loss account id. -// * @param {string} baseCurrency - Base currency code. -// * @returns {Promise} -// */ -// public getPaymentReceiveGLEntries = ( -// paymentReceive: IPaymentReceived, -// ARAccountId: number, -// exGainOrLossAccountId: number, -// baseCurrency: string -// ): ILedgerEntry[] => { -// // Retrieve the payment deposit entry. -// const paymentDepositEntry = this.getPaymentDepositGLEntry(paymentReceive); - -// // Retrieves the A/R entry. -// const receivableEntry = this.getPaymentReceivableEntry( -// paymentReceive, -// ARAccountId -// ); -// // Exchange gain/loss entries. -// const gainLossEntries = this.getPaymentExchangeGainLossEntry( -// paymentReceive, -// ARAccountId, -// exGainOrLossAccountId, -// baseCurrency -// ); -// return [paymentDepositEntry, receivableEntry, ...gainLossEntries]; -// }; -// } +} diff --git a/packages/server-nest/src/modules/PaymentReceived/subscribers/PaymentReceivedGLEntriesSubscriber.ts b/packages/server-nest/src/modules/PaymentReceived/subscribers/PaymentReceivedGLEntriesSubscriber.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/server-nest/src/modules/SaleInvoices/InvoiceGLEntries.ts b/packages/server-nest/src/modules/SaleInvoices/InvoiceGLEntries.ts deleted file mode 100644 index 0896639f5..000000000 --- a/packages/server-nest/src/modules/SaleInvoices/InvoiceGLEntries.ts +++ /dev/null @@ -1,276 +0,0 @@ -// import * as R from 'ramda'; -// import { Knex } from 'knex'; -// import { -// ISaleInvoice, -// IItemEntry, -// ILedgerEntry, -// AccountNormal, -// ILedger, -// } from '@/interfaces'; -// import { Service, Inject } from 'typedi'; -// import Ledger from '@/services/Accounting/Ledger'; -// import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; -// import HasTenancyService from '@/services/Tenancy/TenancyService'; -// import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; - -// @Service() -// export class SaleInvoiceGLEntries { -// @Inject() -// private tenancy: HasTenancyService; - -// @Inject() -// private ledegrRepository: LedgerStorageService; - -// @Inject() -// private itemsEntriesService: ItemsEntriesService; - -// /** -// * Writes a sale invoice GL entries. -// * @param {number} tenantId - Tenant id. -// * @param {number} saleInvoiceId - Sale invoice id. -// * @param {Knex.Transaction} trx -// */ -// public writeInvoiceGLEntries = async ( -// tenantId: number, -// saleInvoiceId: number, -// trx?: Knex.Transaction -// ) => { -// const { SaleInvoice } = this.tenancy.models(tenantId); -// const { accountRepository } = this.tenancy.repositories(tenantId); - -// const saleInvoice = await SaleInvoice.query(trx) -// .findById(saleInvoiceId) -// .withGraphFetched('entries.item'); - -// // Find or create the A/R account. -// const ARAccount = await accountRepository.findOrCreateAccountReceivable( -// saleInvoice.currencyCode, {}, trx -// ); -// // Find or create tax payable account. -// const taxPayableAccount = await accountRepository.findOrCreateTaxPayable( -// {}, -// trx -// ); -// // Retrieves the ledger of the invoice. -// const ledger = this.getInvoiceGLedger( -// saleInvoice, -// ARAccount.id, -// taxPayableAccount.id -// ); -// // Commits the ledger entries to the storage as UOW. -// await this.ledegrRepository.commit(tenantId, ledger, trx); -// }; - -// /** -// * Rewrites the given invoice GL entries. -// * @param {number} tenantId -// * @param {number} saleInvoiceId -// * @param {Knex.Transaction} trx -// */ -// public rewritesInvoiceGLEntries = async ( -// tenantId: number, -// saleInvoiceId: number, -// trx?: Knex.Transaction -// ) => { -// // Reverts the invoice GL entries. -// await this.revertInvoiceGLEntries(tenantId, saleInvoiceId, trx); - -// // Writes the invoice GL entries. -// await this.writeInvoiceGLEntries(tenantId, saleInvoiceId, trx); -// }; - -// /** -// * Reverts the given invoice GL entries. -// * @param {number} tenantId -// * @param {number} saleInvoiceId -// * @param {Knex.Transaction} trx -// */ -// public revertInvoiceGLEntries = async ( -// tenantId: number, -// saleInvoiceId: number, -// trx?: Knex.Transaction -// ) => { -// await this.ledegrRepository.deleteByReference( -// tenantId, -// saleInvoiceId, -// 'SaleInvoice', -// trx -// ); -// }; - -// /** -// * Retrieves the given invoice ledger. -// * @param {ISaleInvoice} saleInvoice -// * @param {number} ARAccountId -// * @returns {ILedger} -// */ -// public getInvoiceGLedger = ( -// saleInvoice: ISaleInvoice, -// ARAccountId: number, -// taxPayableAccountId: number -// ): ILedger => { -// const entries = this.getInvoiceGLEntries( -// saleInvoice, -// ARAccountId, -// taxPayableAccountId -// ); -// return new Ledger(entries); -// }; - -// /** -// * Retrieves the invoice GL common entry. -// * @param {ISaleInvoice} saleInvoice -// * @returns {Partial} -// */ -// private getInvoiceGLCommonEntry = ( -// saleInvoice: ISaleInvoice -// ): Partial => ({ -// credit: 0, -// debit: 0, -// currencyCode: saleInvoice.currencyCode, -// exchangeRate: saleInvoice.exchangeRate, - -// transactionType: 'SaleInvoice', -// transactionId: saleInvoice.id, - -// date: saleInvoice.invoiceDate, -// userId: saleInvoice.userId, - -// transactionNumber: saleInvoice.invoiceNo, -// referenceNumber: saleInvoice.referenceNo, - -// createdAt: saleInvoice.createdAt, -// indexGroup: 10, - -// branchId: saleInvoice.branchId, -// }); - -// /** -// * Retrieve receivable entry of the given invoice. -// * @param {ISaleInvoice} saleInvoice -// * @param {number} ARAccountId -// * @returns {ILedgerEntry} -// */ -// private getInvoiceReceivableEntry = ( -// saleInvoice: ISaleInvoice, -// ARAccountId: number -// ): ILedgerEntry => { -// const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice); - -// return { -// ...commonEntry, -// debit: saleInvoice.totalLocal, -// accountId: ARAccountId, -// contactId: saleInvoice.customerId, -// accountNormal: AccountNormal.DEBIT, -// index: 1, -// } as ILedgerEntry; -// }; - -// /** -// * Retrieve item income entry of the given invoice. -// * @param {ISaleInvoice} saleInvoice - -// * @param {IItemEntry} entry - -// * @param {number} index - -// * @returns {ILedgerEntry} -// */ -// private getInvoiceItemEntry = R.curry( -// ( -// saleInvoice: ISaleInvoice, -// entry: IItemEntry, -// index: number -// ): ILedgerEntry => { -// const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice); -// const localAmount = entry.amountExludingTax * saleInvoice.exchangeRate; - -// return { -// ...commonEntry, -// credit: localAmount, -// accountId: entry.sellAccountId, -// note: entry.description, -// index: index + 2, -// itemId: entry.itemId, -// itemQuantity: entry.quantity, -// accountNormal: AccountNormal.CREDIT, -// projectId: entry.projectId || saleInvoice.projectId, -// taxRateId: entry.taxRateId, -// taxRate: entry.taxRate, -// }; -// } -// ); - -// /** -// * Retreives the GL entry of tax payable. -// * @param {ISaleInvoice} saleInvoice - -// * @param {number} taxPayableAccountId - -// * @returns {ILedgerEntry} -// */ -// private getInvoiceTaxEntry = R.curry( -// ( -// saleInvoice: ISaleInvoice, -// taxPayableAccountId: number, -// entry: IItemEntry, -// index: number -// ): ILedgerEntry => { -// const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice); - -// return { -// ...commonEntry, -// credit: entry.taxAmount, -// accountId: taxPayableAccountId, -// index: index + 1, -// indexGroup: 30, -// accountNormal: AccountNormal.CREDIT, -// taxRateId: entry.taxRateId, -// taxRate: entry.taxRate, -// }; -// } -// ); - -// /** -// * Retrieves the invoice tax GL entries. -// * @param {ISaleInvoice} saleInvoice -// * @param {number} taxPayableAccountId -// * @returns {ILedgerEntry[]} -// */ -// private getInvoiceTaxEntries = ( -// saleInvoice: ISaleInvoice, -// taxPayableAccountId: number -// ): ILedgerEntry[] => { -// // Retrieves the non-zero tax entries. -// const nonZeroTaxEntries = this.itemsEntriesService.getNonZeroEntries( -// saleInvoice.entries -// ); -// const transformTaxEntry = this.getInvoiceTaxEntry( -// saleInvoice, -// taxPayableAccountId -// ); -// // Transforms the non-zero tax entries to GL entries. -// return nonZeroTaxEntries.map(transformTaxEntry); -// }; - -// /** -// * Retrieves the invoice GL entries. -// * @param {ISaleInvoice} saleInvoice -// * @param {number} ARAccountId -// * @returns {ILedgerEntry[]} -// */ -// public getInvoiceGLEntries = ( -// saleInvoice: ISaleInvoice, -// ARAccountId: number, -// taxPayableAccountId: number -// ): ILedgerEntry[] => { -// const receivableEntry = this.getInvoiceReceivableEntry( -// saleInvoice, -// ARAccountId -// ); -// const transformItemEntry = this.getInvoiceItemEntry(saleInvoice); -// const creditEntries = saleInvoice.entries.map(transformItemEntry); - -// const taxEntries = this.getInvoiceTaxEntries( -// saleInvoice, -// taxPayableAccountId -// ); -// return [receivableEntry, ...creditEntries, ...taxEntries]; -// }; -// } diff --git a/packages/server-nest/src/modules/SaleInvoices/SaleInvoice.types.ts b/packages/server-nest/src/modules/SaleInvoices/SaleInvoice.types.ts index 1dc5bdbb0..792ae8f5c 100644 --- a/packages/server-nest/src/modules/SaleInvoices/SaleInvoice.types.ts +++ b/packages/server-nest/src/modules/SaleInvoices/SaleInvoice.types.ts @@ -117,6 +117,7 @@ export interface ISaleInvoiceDeletePayload { // tenantId: number; oldSaleInvoice: SaleInvoice; saleInvoiceId: number; + trx: Knex.Transaction; } export interface ISaleInvoiceDeletingPayload { diff --git a/packages/server-nest/src/modules/SaleInvoices/SaleInvoices.module.ts b/packages/server-nest/src/modules/SaleInvoices/SaleInvoices.module.ts index 1d495762e..979655f58 100644 --- a/packages/server-nest/src/modules/SaleInvoices/SaleInvoices.module.ts +++ b/packages/server-nest/src/modules/SaleInvoices/SaleInvoices.module.ts @@ -31,6 +31,8 @@ import { BranchesModule } from '../Branches/Branches.module'; import { WarehousesModule } from '../Warehouses/Warehouses.module'; import { TaxRatesModule } from '../TaxRates/TaxRate.module'; import { SaleInvoicesController } from './SaleInvoices.controller'; +import { InvoiceGLEntriesSubscriber } from './subscribers/InvoiceGLEntriesSubscriber'; +import { SaleInvoiceGLEntries } from './ledger/InvoiceGLEntries'; @Module({ imports: [ @@ -68,6 +70,8 @@ import { SaleInvoicesController } from './SaleInvoices.controller'; SaleInvoicePdfTemplate, WriteoffSaleInvoice, GetInvoicePaymentsService, + SaleInvoiceGLEntries, + InvoiceGLEntriesSubscriber, ], }) export class SaleInvoicesModule {} diff --git a/packages/server-nest/src/modules/SaleInvoices/commands/CreateSaleInvoice.service.ts b/packages/server-nest/src/modules/SaleInvoices/commands/CreateSaleInvoice.service.ts index 1647ff5a6..c4a825ee6 100644 --- a/packages/server-nest/src/modules/SaleInvoices/commands/CreateSaleInvoice.service.ts +++ b/packages/server-nest/src/modules/SaleInvoices/commands/CreateSaleInvoice.service.ts @@ -23,8 +23,8 @@ export class CreateSaleInvoice { private readonly validators: CommandSaleInvoiceValidators, private readonly transformerDTO: CommandSaleInvoiceDTOTransformer, private readonly eventPublisher: EventEmitter2, - private readonly uow: UnitOfWork, private readonly commandEstimateValidators: SaleEstimateValidators, + private readonly uow: UnitOfWork, @Inject(SaleInvoice.name) private readonly saleInvoiceModel: typeof SaleInvoice, diff --git a/packages/server-nest/src/modules/SaleInvoices/ledger/InvoiceGL.ts b/packages/server-nest/src/modules/SaleInvoices/ledger/InvoiceGL.ts new file mode 100644 index 000000000..bc9b87d68 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/ledger/InvoiceGL.ts @@ -0,0 +1,217 @@ +import * as R from 'ramda'; +import { ILedger } from '@/modules/Ledger/types/Ledger.types'; +import { AccountNormal } from '@/modules/Accounts/Accounts.types'; +import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types'; +import { ItemEntry } from '@/modules/Items/models/ItemEntry'; +import { Ledger } from '@/modules/Ledger/Ledger'; +import { SaleInvoice } from '../models/SaleInvoice'; + +export class InvoiceGL { + private saleInvoice: SaleInvoice; + private ARAccountId: number; + private taxPayableAccountId: number; + private discountAccountId: number; + private otherChargesAccountId: number; + + /** + * Constructor method. + * @param {SaleInvoice} saleInvoice - Sale invoice. + */ + constructor(saleInvoice: SaleInvoice) { + this.saleInvoice = saleInvoice; + } + + /** + * Set the receivable account id. + * @param {number} ARAccountId - Receivable account id. + */ + setARAccountId(ARAccountId: number) { + this.ARAccountId = ARAccountId; + } + + /** + * Set the tax payable account id. + * @param {number} taxPayableAccountId - Tax payable account id. + */ + setTaxPayableAccountId(taxPayableAccountId: number) { + this.taxPayableAccountId = taxPayableAccountId; + } + + /** + * Set the discount account id. + * @param {number} discountAccountId - Discount account id. + */ + setDiscountAccountId(discountAccountId: number) { + this.discountAccountId = discountAccountId; + } + + /** + * Set the other charges account id. + * @param {number} otherChargesAccountId - Other charges account id. + */ + setOtherChargesAccountId(otherChargesAccountId: number) { + this.otherChargesAccountId = otherChargesAccountId; + } + + /** + * Retrieves the invoice GL common entry. + */ + private get invoiceGLCommonEntry() { + return { + credit: 0, + debit: 0, + + currencyCode: this.saleInvoice.currencyCode, + exchangeRate: this.saleInvoice.exchangeRate, + + transactionType: 'SaleInvoice', + transactionId: this.saleInvoice.id, + + date: this.saleInvoice.invoiceDate, + userId: this.saleInvoice.userId, + + transactionNumber: this.saleInvoice.invoiceNo, + referenceNumber: this.saleInvoice.referenceNo, + + createdAt: this.saleInvoice.createdAt, + indexGroup: 10, + + branchId: this.saleInvoice.branchId, + }; + } + + /** + * Retrieve receivable entry of the invoice. + * @returns {ILedgerEntry} + */ + public get invoiceReceivableEntry(): ILedgerEntry { + const commonEntry = this.invoiceGLCommonEntry; + + return { + ...commonEntry, + debit: this.saleInvoice.totalLocal, + accountId: this.ARAccountId, + contactId: this.saleInvoice.customerId, + accountNormal: AccountNormal.DEBIT, + index: 1, + }; + } + + /** + * Retrieve item income entry of the invoice. + * @param {ItemEntry} entry - Item entry. + * @param {number} index - Index. + * @returns {ILedgerEntry} + */ + private getInvoiceItemEntry = R.curry( + (entry: ItemEntry, index: number): ILedgerEntry => { + const commonEntry = this.invoiceGLCommonEntry; + const localAmount = + entry.totalExcludingTax * this.saleInvoice.exchangeRate; + + return { + ...commonEntry, + credit: localAmount, + accountId: entry.sellAccountId, + note: entry.description, + index: index + 2, + itemId: entry.itemId, + itemQuantity: entry.quantity, + accountNormal: AccountNormal.CREDIT, + taxRateId: entry.taxRateId, + taxRate: entry.taxRate, + }; + }, + ); + + /** + * Retreives the GL entry of tax payable. + * @param {ItemEntry} entry - Item entry. + * @param {number} index - Index. + * @returns {ILedgerEntry} + */ + private getInvoiceTaxEntry(entry: ItemEntry, index: number): ILedgerEntry { + const commonEntry = this.invoiceGLCommonEntry; + + return { + ...commonEntry, + credit: entry.taxAmount, + accountId: this.taxPayableAccountId, + index: index + 1, + indexGroup: 30, + accountNormal: AccountNormal.CREDIT, + taxRateId: entry.taxRateId, + taxRate: entry.taxRate, + }; + } + + /** + * Retrieves the invoice discount GL entry. + * @returns {ILedgerEntry} + */ + private getInvoiceDiscountEntry = (): ILedgerEntry => { + const commonEntry = this.invoiceGLCommonEntry; + + return { + ...commonEntry, + debit: this.saleInvoice.discountAmountLocal, + accountId: this.discountAccountId, + accountNormal: AccountNormal.CREDIT, + index: 1, + } as ILedgerEntry; + }; + + /** + * Retrieves the invoice adjustment GL entry. + * @returns {ILedgerEntry} + */ + private getAdjustmentEntry = (): ILedgerEntry => { + const commonEntry = this.invoiceGLCommonEntry; + const adjustmentAmount = Math.abs(this.saleInvoice.adjustmentLocal); + + return { + ...commonEntry, + debit: this.saleInvoice.adjustmentLocal < 0 ? adjustmentAmount : 0, + credit: this.saleInvoice.adjustmentLocal > 0 ? adjustmentAmount : 0, + accountId: this.otherChargesAccountId, + accountNormal: AccountNormal.CREDIT, + index: 1, + }; + }; + + /** + * Retrieves the invoice GL entries. + * @returns {ILedgerEntry[]} + */ + public getInvoiceGLEntries = (): ILedgerEntry[] => { + const receivableEntry = this.invoiceReceivableEntry; + const creditEntries = this.saleInvoice.entries.map( + this.getInvoiceItemEntry, + ); + + const taxEntries = this.saleInvoice.entries + .filter((entry) => entry.taxAmount > 0) + .map(this.getInvoiceTaxEntry); + + const discountEntry = this.getInvoiceDiscountEntry(); + const adjustmentEntry = this.getAdjustmentEntry(); + + return [ + this.invoiceReceivableEntry, + ...creditEntries, + ...taxEntries, + discountEntry, + adjustmentEntry, + ]; + }; + + /** + * Retrieves the invoice ledger. + * @returns {ILedger} + */ + public getInvoiceLedger = (): ILedger => { + const entries = this.getInvoiceGLEntries(); + + return new Ledger(entries); + }; +} diff --git a/packages/server-nest/src/modules/SaleInvoices/ledger/InvoiceGLEntries.ts b/packages/server-nest/src/modules/SaleInvoices/ledger/InvoiceGLEntries.ts new file mode 100644 index 000000000..e29d11aef --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/ledger/InvoiceGLEntries.ts @@ -0,0 +1,95 @@ +import { Knex } from 'knex'; +import { Inject, Injectable } from '@nestjs/common'; +import { LedgerStorageService } from '../../Ledger/LedgerStorage.service'; +import { SaleInvoice } from '../models/SaleInvoice'; +import { AccountRepository } from '../../Accounts/repositories/Account.repository'; +import { InvoiceGL } from './InvoiceGL'; + +@Injectable() +export class SaleInvoiceGLEntries { + constructor( + private readonly ledegrRepository: LedgerStorageService, + private readonly accountRepository: AccountRepository, + + @Inject(SaleInvoice.name) + private readonly saleInvoiceModel: typeof SaleInvoice, + ) {} + + /** + * Writes a sale invoice GL entries. + * @param {number} saleInvoiceId - Sale invoice id. + * @param {Knex.Transaction} trx + */ + public writeInvoiceGLEntries = async ( + saleInvoiceId: number, + trx?: Knex.Transaction, + ) => { + const saleInvoice = await this.saleInvoiceModel + .query(trx) + .findById(saleInvoiceId) + .withGraphFetched('entries.item'); + + // Find or create the A/R account. + const ARAccount = + await this.accountRepository.findOrCreateAccountReceivable( + saleInvoice.currencyCode, + {}, + trx, + ); + // Find or create tax payable account. + const taxPayableAccount = + await this.accountRepository.findOrCreateTaxPayable({}, trx); + // Find or create the discount expense account. + const discountAccount = + await this.accountRepository.findOrCreateDiscountAccount({}, trx); + // Find or create the other charges account. + const otherChargesAccount = + await this.accountRepository.findOrCreateOtherChargesAccount({}, trx); + + // Retrieves the ledger of the invoice. + const invoiceGL = new InvoiceGL(saleInvoice); + + invoiceGL.setARAccountId(ARAccount.id); + invoiceGL.setTaxPayableAccountId(taxPayableAccount.id); + invoiceGL.setDiscountAccountId(discountAccount.id); + invoiceGL.setOtherChargesAccountId(otherChargesAccount.id); + + const ledger = invoiceGL.getInvoiceLedger(); + + // Commits the ledger entries to the storage as UOW. + await this.ledegrRepository.commit(ledger, trx); + }; + + /** + * Rewrites the given invoice GL entries. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @param {Knex.Transaction} trx + */ + public rewritesInvoiceGLEntries = async ( + saleInvoiceId: number, + trx?: Knex.Transaction, + ) => { + // Reverts the invoice GL entries. + await this.revertInvoiceGLEntries(saleInvoiceId, trx); + + // Writes the invoice GL entries. + await this.writeInvoiceGLEntries(saleInvoiceId, trx); + }; + + /** + * Reverts the given invoice GL entries. + * @param {number} saleInvoiceId - Sale invoice id. + * @param {Knex.Transaction} trx + */ + public revertInvoiceGLEntries = async ( + saleInvoiceId: number, + trx?: Knex.Transaction, + ) => { + await this.ledegrRepository.deleteByReference( + saleInvoiceId, + 'SaleInvoice', + trx, + ); + }; +} diff --git a/packages/server-nest/src/modules/SaleInvoices/models/SaleInvoice.ts b/packages/server-nest/src/modules/SaleInvoices/models/SaleInvoice.ts index d89a3ed58..37b0ed9f7 100644 --- a/packages/server-nest/src/modules/SaleInvoices/models/SaleInvoice.ts +++ b/packages/server-nest/src/modules/SaleInvoices/models/SaleInvoice.ts @@ -39,6 +39,7 @@ export class SaleInvoice extends BaseModel { public referenceNo: string; public pdfTemplateId: number; + public userId: number; public branchId: number; public warehouseId: number; diff --git a/packages/server-nest/src/modules/SaleInvoices/subscribers/InvoiceGLEntriesSubscriber.ts b/packages/server-nest/src/modules/SaleInvoices/subscribers/InvoiceGLEntriesSubscriber.ts new file mode 100644 index 000000000..1917d1477 --- /dev/null +++ b/packages/server-nest/src/modules/SaleInvoices/subscribers/InvoiceGLEntriesSubscriber.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@nestjs/common'; +import { + ISaleInvoiceCreatedPayload, + ISaleInvoiceDeletePayload, + ISaleInvoiceEditedPayload, +} from '../SaleInvoice.types'; +import { OnEvent } from '@nestjs/event-emitter'; +import { SaleInvoiceGLEntries } from '../ledger/InvoiceGLEntries'; +import { events } from '@/common/events/events'; + +@Injectable() +export class InvoiceGLEntriesSubscriber { + constructor(public readonly saleInvoiceGLEntries: SaleInvoiceGLEntries) {} + + /** + * Records journal entries of the non-inventory invoice. + * @param {ISaleInvoiceCreatedPayload} payload - + * @returns {Promise} + */ + @OnEvent(events.saleInvoice.onCreated) + @OnEvent(events.saleInvoice.onDelivered) + public async handleWriteJournalEntriesOnInvoiceCreated({ + saleInvoiceId, + saleInvoice, + trx, + }: ISaleInvoiceCreatedPayload) { + // Can't continue if the sale invoice is not delivered yet. + if (!saleInvoice.deliveredAt) return null; + + await this.saleInvoiceGLEntries.writeInvoiceGLEntries(saleInvoiceId, trx); + } + + /** + * Records journal entries of the non-inventory invoice. + * @param {ISaleInvoiceEditedPayload} payload - + * @returns {Promise} + */ + @OnEvent(events.saleInvoice.onEdited) + public async handleRewriteJournalEntriesOnceInvoiceEdit({ + saleInvoice, + trx, + }: ISaleInvoiceEditedPayload) { + // Can't continue if the sale invoice is not delivered yet. + if (!saleInvoice.deliveredAt) return null; + + await this.saleInvoiceGLEntries.rewritesInvoiceGLEntries( + saleInvoice.id, + trx, + ); + } + + /** + * Handle reverting journal entries once sale invoice delete. + * @param {ISaleInvoiceDeletePayload} payload - + * @returns {Promise} + */ + @OnEvent(events.saleInvoice.onDeleted) + public async handleRevertingInvoiceJournalEntriesOnDelete({ + saleInvoiceId, + trx, + }: ISaleInvoiceDeletePayload) { + await this.saleInvoiceGLEntries.revertInvoiceGLEntries(saleInvoiceId, trx); + } +} diff --git a/packages/server-nest/src/modules/SaleReceipts/SaleReceipts.module.ts b/packages/server-nest/src/modules/SaleReceipts/SaleReceipts.module.ts index f38ffd261..262f60299 100644 --- a/packages/server-nest/src/modules/SaleReceipts/SaleReceipts.module.ts +++ b/packages/server-nest/src/modules/SaleReceipts/SaleReceipts.module.ts @@ -20,6 +20,8 @@ import { SaleReceiptIncrement } from './commands/SaleReceiptIncrement.service'; import { PdfTemplatesModule } from '../PdfTemplate/PdfTemplates.module'; import { AutoIncrementOrdersModule } from '../AutoIncrementOrders/AutoIncrementOrders.module'; import { SaleReceiptsController } from './SaleReceipts.controller'; +import { SaleReceiptGLEntriesSubscriber } from './subscribers/SaleReceiptGLEntriesSubscriber'; +import { SaleReceiptGLEntries } from './ledger/SaleReceiptGLEntries'; @Module({ controllers: [SaleReceiptsController], @@ -46,6 +48,8 @@ import { SaleReceiptsController } from './SaleReceipts.controller'; SaleReceiptDTOTransformer, SaleReceiptBrandingTemplate, SaleReceiptIncrement, + SaleReceiptGLEntries, + SaleReceiptGLEntriesSubscriber ], }) export class SaleReceiptsModule {} diff --git a/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptGLEntries.ts b/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptGLEntries.ts deleted file mode 100644 index 64580a02b..000000000 --- a/packages/server-nest/src/modules/SaleReceipts/commands/SaleReceiptGLEntries.ts +++ /dev/null @@ -1,184 +0,0 @@ -// import { Knex } from 'knex'; -// import { Service, Inject } from 'typedi'; -// import * as R from 'ramda'; -// import HasTenancyService from '@/services/Tenancy/TenancyService'; -// import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; -// import { -// AccountNormal, -// ILedgerEntry, -// ISaleReceipt, -// IItemEntry, -// } from '@/interfaces'; -// import Ledger from '@/services/Accounting/Ledger'; - -// @Service() -// export class SaleReceiptGLEntries { -// @Inject() -// private tenancy: HasTenancyService; - -// @Inject() -// private ledgerStorage: LedgerStorageService; - -// /** -// * Creates income GL entries. -// * @param {number} tenantId -// * @param {number} saleReceiptId -// * @param {Knex.Transaction} trx -// */ -// public writeIncomeGLEntries = async ( -// tenantId: number, -// saleReceiptId: number, -// trx?: Knex.Transaction -// ): Promise => { -// const { SaleReceipt } = this.tenancy.models(tenantId); - -// const saleReceipt = await SaleReceipt.query(trx) -// .findById(saleReceiptId) -// .withGraphFetched('entries.item'); - -// // Retrieve the income entries ledger. -// const incomeLedger = this.getIncomeEntriesLedger(saleReceipt); - -// // Commits the ledger entries to the storage. -// await this.ledgerStorage.commit(tenantId, incomeLedger, trx); -// }; - -// /** -// * Reverts the receipt GL entries. -// * @param {number} tenantId -// * @param {number} saleReceiptId -// * @param {Knex.Transaction} trx -// * @returns {Promise} -// */ -// public revertReceiptGLEntries = async ( -// tenantId: number, -// saleReceiptId: number, -// trx?: Knex.Transaction -// ): Promise => { -// await this.ledgerStorage.deleteByReference( -// tenantId, -// saleReceiptId, -// 'SaleReceipt', -// trx -// ); -// }; - -// /** -// * Rewrites the receipt GL entries. -// * @param {number} tenantId -// * @param {number} saleReceiptId -// * @param {Knex.Transaction} trx -// * @returns {Promise} -// */ -// public rewriteReceiptGLEntries = async ( -// tenantId: number, -// saleReceiptId: number, -// trx?: Knex.Transaction -// ): Promise => { -// // Reverts the receipt GL entries. -// await this.revertReceiptGLEntries(tenantId, saleReceiptId, trx); - -// // Writes the income GL entries. -// await this.writeIncomeGLEntries(tenantId, saleReceiptId, trx); -// }; - -// /** -// * Retrieves the income GL ledger. -// * @param {ISaleReceipt} saleReceipt -// * @returns {Ledger} -// */ -// private getIncomeEntriesLedger = (saleReceipt: ISaleReceipt): Ledger => { -// const entries = this.getIncomeGLEntries(saleReceipt); - -// return new Ledger(entries); -// }; - -// /** -// * Retireves the income GL common entry. -// * @param {ISaleReceipt} saleReceipt - -// */ -// private getIncomeGLCommonEntry = (saleReceipt: ISaleReceipt) => { -// return { -// currencyCode: saleReceipt.currencyCode, -// exchangeRate: saleReceipt.exchangeRate, - -// transactionType: 'SaleReceipt', -// transactionId: saleReceipt.id, - -// date: saleReceipt.receiptDate, - -// transactionNumber: saleReceipt.receiptNumber, -// referenceNumber: saleReceipt.referenceNo, - -// createdAt: saleReceipt.createdAt, - -// credit: 0, -// debit: 0, - -// userId: saleReceipt.userId, -// branchId: saleReceipt.branchId, -// }; -// }; - -// /** -// * Retrieve receipt income item GL entry. -// * @param {ISaleReceipt} saleReceipt - -// * @param {IItemEntry} entry - -// * @param {number} index - -// * @returns {ILedgerEntry} -// */ -// private getReceiptIncomeItemEntry = R.curry( -// ( -// saleReceipt: ISaleReceipt, -// entry: IItemEntry, -// index: number -// ): ILedgerEntry => { -// const commonEntry = this.getIncomeGLCommonEntry(saleReceipt); -// const itemIncome = entry.amount * saleReceipt.exchangeRate; - -// return { -// ...commonEntry, -// credit: itemIncome, -// accountId: entry.item.sellAccountId, -// note: entry.description, -// index: index + 2, -// itemId: entry.itemId, -// itemQuantity: entry.quantity, -// accountNormal: AccountNormal.CREDIT, -// }; -// } -// ); - -// /** -// * Retrieves the receipt deposit GL deposit entry. -// * @param {ISaleReceipt} saleReceipt -// * @returns {ILedgerEntry} -// */ -// private getReceiptDepositEntry = ( -// saleReceipt: ISaleReceipt -// ): ILedgerEntry => { -// const commonEntry = this.getIncomeGLCommonEntry(saleReceipt); - -// return { -// ...commonEntry, -// debit: saleReceipt.localAmount, -// accountId: saleReceipt.depositAccountId, -// index: 1, -// accountNormal: AccountNormal.DEBIT, -// }; -// }; - -// /** -// * Retrieves the income GL entries. -// * @param {ISaleReceipt} saleReceipt - -// * @returns {ILedgerEntry[]} -// */ -// private getIncomeGLEntries = (saleReceipt: ISaleReceipt): ILedgerEntry[] => { -// const getItemEntry = this.getReceiptIncomeItemEntry(saleReceipt); - -// const creditEntries = saleReceipt.entries.map(getItemEntry); -// const depositEntry = this.getReceiptDepositEntry(saleReceipt); - -// return [depositEntry, ...creditEntries]; -// }; -// } diff --git a/packages/server-nest/src/modules/SaleReceipts/ledger/SaleReceiptGL.ts b/packages/server-nest/src/modules/SaleReceipts/ledger/SaleReceiptGL.ts new file mode 100644 index 000000000..a3198402c --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/ledger/SaleReceiptGL.ts @@ -0,0 +1,167 @@ +import * as R from 'ramda'; +import { ILedger } from '@/modules/Ledger/types/Ledger.types'; +import { AccountNormal } from '@/modules/Accounts/Accounts.types'; +import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types'; +import { Ledger } from '@/modules/Ledger/Ledger'; +import { SaleReceipt } from '../models/SaleReceipt'; +import { ItemEntry } from '@/modules/Items/models/ItemEntry'; + +export class SaleReceiptGL { + private saleReceipt: SaleReceipt; + private discountAccountId: number; + private otherChargesAccountId: number; + + /** + * Constructor method. + * @param {SaleReceipt} saleReceipt - Sale receipt. + */ + constructor(saleReceipt: SaleReceipt) { + this.saleReceipt = saleReceipt; + } + + /** + * Sets the discount account id. + * @param {number} discountAccountId - Discount account id. + */ + setDiscountAccountId(discountAccountId: number) { + this.discountAccountId = discountAccountId; + return this; + } + + /** + * Sets the other charges account id. + * @param {number} otherChargesAccountId - Other charges account id. + */ + setOtherChargesAccountId(otherChargesAccountId: number) { + this.otherChargesAccountId = otherChargesAccountId; + return this; + } + + /** + * Retrieves the income GL common entry. + */ + private getIncomeGLCommonEntry = () => { + return { + currencyCode: this.saleReceipt.currencyCode, + exchangeRate: this.saleReceipt.exchangeRate, + + transactionType: 'SaleReceipt', + transactionId: this.saleReceipt.id, + + date: this.saleReceipt.receiptDate, + + transactionNumber: this.saleReceipt.receiptNumber, + referenceNumber: this.saleReceipt.referenceNo, + + createdAt: this.saleReceipt.createdAt, + + credit: 0, + debit: 0, + + userId: this.saleReceipt.userId, + branchId: this.saleReceipt.branchId, + }; + }; + + /** + * Retrieve receipt income item G/L entry. + * @param {ItemEntry} entry - Item entry. + * @param {number} index - Index. + * @returns {ILedgerEntry} + */ + private getReceiptIncomeItemEntry = R.curry( + (entry: ItemEntry, index: number): ILedgerEntry => { + const commonEntry = this.getIncomeGLCommonEntry(); + const totalLocal = + entry.totalExcludingTax * this.saleReceipt.exchangeRate; + + return { + ...commonEntry, + credit: totalLocal, + accountId: entry.item.sellAccountId, + note: entry.description, + index: index + 2, + itemId: entry.itemId, + itemQuantity: entry.quantity, + accountNormal: AccountNormal.CREDIT, + }; + }, + ); + + /** + * Retrieves the receipt deposit GL deposit entry. + * @returns {ILedgerEntry} + */ + private getReceiptDepositEntry = (): ILedgerEntry => { + const commonEntry = this.getIncomeGLCommonEntry(); + + return { + ...commonEntry, + debit: this.saleReceipt.totalLocal, + accountId: this.saleReceipt.depositAccountId, + index: 1, + accountNormal: AccountNormal.DEBIT, + }; + }; + + /** + * Retrieves the discount GL entry. + * @returns {ILedgerEntry} + */ + private getDiscountEntry = (): ILedgerEntry => { + const commonEntry = this.getIncomeGLCommonEntry(); + + return { + ...commonEntry, + debit: this.saleReceipt.discountAmountLocal, + accountId: this.discountAccountId, + index: 1, + accountNormal: AccountNormal.CREDIT, + }; + }; + + /** + * Retrieves the adjustment GL entry. + * @returns {ILedgerEntry} + */ + private getAdjustmentEntry = (): ILedgerEntry => { + const commonEntry = this.getIncomeGLCommonEntry(); + const adjustmentAmount = Math.abs(this.saleReceipt.adjustmentLocal); + + return { + ...commonEntry, + debit: this.saleReceipt.adjustmentLocal < 0 ? adjustmentAmount : 0, + credit: this.saleReceipt.adjustmentLocal > 0 ? adjustmentAmount : 0, + accountId: this.otherChargesAccountId, + accountNormal: AccountNormal.CREDIT, + index: 1, + }; + }; + + /** + * Retrieves the income GL entries. + * @returns {ILedgerEntry[]} + */ + public getIncomeGLEntries = (): ILedgerEntry[] => { + const getItemEntry = this.getReceiptIncomeItemEntry; + + const creditEntries = this.saleReceipt.entries.map((e, index) => + getItemEntry(e, index), + ); + const depositEntry = this.getReceiptDepositEntry(); + const discountEntry = this.getDiscountEntry(); + const adjustmentEntry = this.getAdjustmentEntry(); + + return [depositEntry, ...creditEntries, discountEntry, adjustmentEntry]; + }; + + /** + * Retrieves the income GL ledger. + * @returns {ILedger} + */ + public getIncomeLedger = (): ILedger => { + const entries = this.getIncomeGLEntries(); + + return new Ledger(entries); + }; +} diff --git a/packages/server-nest/src/modules/SaleReceipts/ledger/SaleReceiptGLEntries.ts b/packages/server-nest/src/modules/SaleReceipts/ledger/SaleReceiptGLEntries.ts new file mode 100644 index 000000000..5626a5841 --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/ledger/SaleReceiptGLEntries.ts @@ -0,0 +1,83 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { LedgerStorageService } from '@/modules/Ledger/LedgerStorage.service'; +import { SaleReceipt } from '../models/SaleReceipt'; +import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository'; +import { SaleReceiptGL } from './SaleReceiptGL'; + +@Injectable() +export class SaleReceiptGLEntries { + constructor( + private readonly ledgerStorage: LedgerStorageService, + private readonly accountRepository: AccountRepository, + + @Inject(SaleReceipt.name) + private readonly saleReceiptModel: typeof SaleReceipt, + ) {} + + /** + * Creates income GL entries. + * @param {number} saleReceiptId + * @param {Knex.Transaction} trx + */ + public writeIncomeGLEntries = async ( + saleReceiptId: number, + trx?: Knex.Transaction + ): Promise => { + const saleReceipt = await this.saleReceiptModel.query(trx) + .findById(saleReceiptId) + .withGraphFetched('entries.item'); + + // Find or create the discount expense account. + const discountAccount = await this.accountRepository.findOrCreateDiscountAccount( + {}, + trx + ); + // Find or create the other charges account. + const otherChargesAccount = + await this.accountRepository.findOrCreateOtherChargesAccount({}, trx); + + // Retrieves the income ledger. + const incomeLedger = new SaleReceiptGL(saleReceipt) + .setDiscountAccountId(discountAccount.id) + .setOtherChargesAccountId(otherChargesAccount.id) + .getIncomeLedger(); + + // Commits the ledger entries to the storage. + await this.ledgerStorage.commit(incomeLedger, trx); + }; + + /** + * Reverts the receipt GL entries. + * @param {number} saleReceiptId - Sale receipt id. + * @param {Knex.Transaction} trx + * @returns {Promise} + */ + public revertReceiptGLEntries = async ( + saleReceiptId: number, + trx?: Knex.Transaction + ): Promise => { + await this.ledgerStorage.deleteByReference( + saleReceiptId, + 'SaleReceipt', + trx + ); + }; + + /** + * Rewrites the receipt GL entries. + * @param {number} saleReceiptId + * @param {Knex.Transaction} trx + * @returns {Promise} + */ + public rewriteReceiptGLEntries = async ( + saleReceiptId: number, + trx?: Knex.Transaction + ): Promise => { + // Reverts the receipt GL entries. + await this.revertReceiptGLEntries(saleReceiptId, trx); + + // Writes the income GL entries. + await this.writeIncomeGLEntries(saleReceiptId, trx); + }; +} diff --git a/packages/server-nest/src/modules/SaleReceipts/models/SaleReceipt.ts b/packages/server-nest/src/modules/SaleReceipts/models/SaleReceipt.ts index 8b572fd4e..a78f988d8 100644 --- a/packages/server-nest/src/modules/SaleReceipts/models/SaleReceipt.ts +++ b/packages/server-nest/src/modules/SaleReceipts/models/SaleReceipt.ts @@ -6,6 +6,11 @@ import { Model, mixin } from 'objection'; // import { DEFAULT_VIEWS } from '@/services/Sales/Receipts/constants'; // import ModelSearchable from './ModelSearchable'; import { BaseModel } from '@/models/Model'; +import { ItemEntry } from '@/modules/Items/models/ItemEntry'; +import { Customer } from '@/modules/Customers/models/Customer'; +import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model'; +import { Branch } from '@/modules/Branches/models/Branch.model'; +import { Warehouse } from '@/modules/Warehouses/models/Warehouse.model'; export class SaleReceipt extends BaseModel { amount: number; @@ -27,6 +32,12 @@ export class SaleReceipt extends BaseModel { createdAt: Date; updatedAt: Date | null; + customer!: Customer; + entries!: ItemEntry[]; + transactions!: AccountTransaction[]; + branch!: Branch; + warehouse!: Warehouse; + /** * Table name */ diff --git a/packages/server-nest/src/modules/SaleReceipts/subscribers/SaleReceiptGLEntriesSubscriber.ts b/packages/server-nest/src/modules/SaleReceipts/subscribers/SaleReceiptGLEntriesSubscriber.ts new file mode 100644 index 000000000..c9bb428fe --- /dev/null +++ b/packages/server-nest/src/modules/SaleReceipts/subscribers/SaleReceiptGLEntriesSubscriber.ts @@ -0,0 +1,63 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { + ISaleReceiptCreatedPayload, + ISaleReceiptEditedPayload, + ISaleReceiptEventDeletedPayload, +} from '../types/SaleReceipts.types'; +import { SaleReceiptGLEntries } from '../ledger/SaleReceiptGLEntries'; +import { OnEvent } from '@nestjs/event-emitter'; +import { events } from '@/common/events/events'; + +@Injectable() +export class SaleReceiptGLEntriesSubscriber { + constructor(private readonly saleReceiptGLEntries: SaleReceiptGLEntries) {} + + /** + * Handles writing sale receipt income journal entries once created. + * @param {ISaleReceiptCreatedPayload} payload - + */ + @OnEvent(events.saleReceipt.onCreated) + @OnEvent(events.saleReceipt.onClosed) + public async handleWriteReceiptIncomeJournalEntrieOnCreate({ + saleReceiptId, + saleReceipt, + trx, + }: ISaleReceiptCreatedPayload) { + // Can't continue if the sale receipt is not closed yet. + if (!saleReceipt.closedAt) return null; + + // Writes the sale receipt income journal entries. + await this.saleReceiptGLEntries.writeIncomeGLEntries(saleReceiptId, trx); + } + + /** + * Handles sale receipt revert jouranl entries once be deleted. + * @param {ISaleReceiptEventDeletedPayload} payload - + */ + @OnEvent(events.saleReceipt.onDeleted) + public async handleRevertReceiptJournalEntriesOnDeleted({ + saleReceiptId, + trx, + }: ISaleReceiptEventDeletedPayload) { + await this.saleReceiptGLEntries.revertReceiptGLEntries(saleReceiptId, trx); + } + + /** + * Handles writing sale receipt income journal entries once be edited. + * @param {ISaleReceiptEditedPayload} payload - + */ + @OnEvent(events.saleReceipt.onEdited) + public async handleWriteReceiptIncomeJournalEntrieOnEdited({ + saleReceipt, + trx, + }: ISaleReceiptEditedPayload) { + // Can't continue if the sale receipt is not closed yet. + if (!saleReceipt.closedAt) return null; + + // Writes the sale receipt income journal entries. + await this.saleReceiptGLEntries.rewriteReceiptGLEntries( + saleReceipt.id, + trx, + ); + } +} diff --git a/packages/server-nest/src/modules/Tenancy/TenancyContext.service.ts b/packages/server-nest/src/modules/Tenancy/TenancyContext.service.ts index f80ca7503..e294e6695 100644 --- a/packages/server-nest/src/modules/Tenancy/TenancyContext.service.ts +++ b/packages/server-nest/src/modules/Tenancy/TenancyContext.service.ts @@ -35,6 +35,12 @@ export class TenancyContext { return query; } + async getTenantMetadata() { + const tenant = await this.getTenant(true); + + return tenant?.metadata; + } + /** * Retrieves the current system user. * @returns {Promise} diff --git a/packages/server-nest/test/expenses.e2e-spec.ts b/packages/server-nest/test/expenses.e2e-spec.ts new file mode 100644 index 000000000..74078ebc8 --- /dev/null +++ b/packages/server-nest/test/expenses.e2e-spec.ts @@ -0,0 +1,61 @@ +import * as request from 'supertest'; +import { faker } from '@faker-js/faker'; +import { app } from './init-app-test'; + +const makeExpenseRequest = () => ({ + exchangeRate: 1, + description: faker.lorem.sentence(), + paymentAccountId: 1000, + referenceNo: faker.string.alphanumeric(10), + publish: true, + paymentDate: faker.date.recent(), + categories: [ + { + expenseAccountId: 1045, + amount: faker.number.float({ min: 10, max: 1000, precision: 0.01 }), + description: faker.lorem.sentence(), + }, + ], + // currencyCode: faker.finance.currencyCode(), + // userId: faker.number.int({ min: 1, max: 100 }), + // payeeId: faker.number.int({ min: 1, max: 100 }), + // branchId: faker.number.int({ min: 1, max: 100 }), +}); + +describe('Expenses (e2e)', () => { + it('/expenses (POST)', () => { + return request(app.getHttpServer()) + .post('/expenses') + .set('organization-id', '4064541lv40nhca') + .send(makeExpenseRequest()) + .expect(201); + }); + + it('/expenses/:id (GET)', async () => { + const response = await request(app.getHttpServer()) + .post('/expenses') + .set('organization-id', '4064541lv40nhca') + .send(makeExpenseRequest()); + + const expenseId = response.body.id; + + return request(app.getHttpServer()) + .get(`/expenses/${expenseId}`) + .set('organization-id', '4064541lv40nhca') + .expect(200); + }); + + it('/expenses/:id (DELETE)', async () => { + const response = await request(app.getHttpServer()) + .post('/expenses') + .set('organization-id', '4064541lv40nhca') + .send(makeExpenseRequest()); + + const expenseId = response.body.id; + + return request(app.getHttpServer()) + .delete(`/expenses/${expenseId}`) + .set('organization-id', '4064541lv40nhca') + .expect(200); + }); +}); diff --git a/packages/server-nest/test/init-app-test.ts b/packages/server-nest/test/init-app-test.ts index 0096b2a97..45935b92a 100644 --- a/packages/server-nest/test/init-app-test.ts +++ b/packages/server-nest/test/init-app-test.ts @@ -1,4 +1,4 @@ -import { INestApplication } from '@nestjs/common'; +import { INestApplication, Logger } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { AppModule } from '../src/modules/App/App.module'; @@ -10,6 +10,8 @@ beforeAll(async () => { }).compile(); app = moduleFixture.createNestApplication(); + app.useLogger(new Logger()); + await app.init(); });