diff --git a/packages/server-nest/src/modules/App/App.module.ts b/packages/server-nest/src/modules/App/App.module.ts index a04389476..31c9dea24 100644 --- a/packages/server-nest/src/modules/App/App.module.ts +++ b/packages/server-nest/src/modules/App/App.module.ts @@ -62,6 +62,7 @@ import { BankingMatchingModule } from '../BankingMatching/BankingMatching.module import { BankingTransactionsModule } from '../BankingTransactions/BankingTransactions.module'; import { TransactionsLockingModule } from '../TransactionsLocking/TransactionsLocking.module'; import { SettingsModule } from '../Settings/Settings.module'; +import { InventoryAdjustmentsModule } from '../InventoryAdjutments/InventoryAdjustments.module'; @Module({ imports: [ @@ -147,7 +148,8 @@ import { SettingsModule } from '../Settings/Settings.module'; BankingTransactionsRegonizeModule, BankingMatchingModule, TransactionsLockingModule, - SettingsModule + SettingsModule, + InventoryAdjustmentsModule ], controllers: [AppController], providers: [ diff --git a/packages/server-nest/src/modules/InventoryAdjutments/InventoryAdjustmentGL.ts b/packages/server-nest/src/modules/InventoryAdjutments/InventoryAdjustmentGL.ts new file mode 100644 index 000000000..b2f1e0f1b --- /dev/null +++ b/packages/server-nest/src/modules/InventoryAdjutments/InventoryAdjustmentGL.ts @@ -0,0 +1,226 @@ +// import { Service, Inject } from 'typedi'; +// import { Knex } from 'knex'; +// import * as R from 'ramda'; +// import TenancyService from '@/services/Tenancy/TenancyService'; +// import { +// AccountNormal, +// IInventoryAdjustment, +// IInventoryAdjustmentEntry, +// ILedgerEntry, +// } from '@/interfaces'; +// import Ledger from '@/services/Accounting/Ledger'; +// import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; +// import { TenantMetadata } from '@/system/models'; + +// @Service() +// export default class InventoryAdjustmentsGL { +// @Inject() +// private tenancy: TenancyService; + +// @Inject() +// private ledgerStorage: LedgerStorageService; + +// /** +// * Retrieves the inventory adjustment common GL entry. +// * @param {InventoryAdjustment} inventoryAdjustment - +// * @param {string} baseCurrency - +// * @returns {ILedgerEntry} +// */ +// private getAdjustmentGLCommonEntry = ( +// inventoryAdjustment: IInventoryAdjustment, +// baseCurrency: string +// ) => { +// return { +// currencyCode: baseCurrency, +// exchangeRate: 1, + +// transactionId: inventoryAdjustment.id, +// transactionType: 'InventoryAdjustment', +// referenceNumber: inventoryAdjustment.referenceNo, + +// date: inventoryAdjustment.date, + +// userId: inventoryAdjustment.userId, +// branchId: inventoryAdjustment.branchId, + +// createdAt: inventoryAdjustment.createdAt, + +// credit: 0, +// debit: 0, +// }; +// }; + +// /** +// * Retrieve the inventory adjustment inventory GL entry. +// * @param {IInventoryAdjustment} inventoryAdjustment -Inventory adjustment model. +// * @param {string} baseCurrency - Base currency of the organization. +// * @param {IInventoryAdjustmentEntry} entry - +// * @param {number} index - +// * @returns {ILedgerEntry} +// */ +// private getAdjustmentGLInventoryEntry = R.curry( +// ( +// inventoryAdjustment: IInventoryAdjustment, +// baseCurrency: string, +// entry: IInventoryAdjustmentEntry, +// index: number +// ): ILedgerEntry => { +// const commonEntry = this.getAdjustmentGLCommonEntry( +// inventoryAdjustment, +// baseCurrency +// ); +// const amount = entry.cost * entry.quantity; + +// return { +// ...commonEntry, +// debit: amount, +// accountId: entry.item.inventoryAccountId, +// accountNormal: AccountNormal.DEBIT, +// index, +// }; +// } +// ); + +// /** +// * Retrieves the inventory adjustment +// * @param {IInventoryAdjustment} inventoryAdjustment +// * @param {IInventoryAdjustmentEntry} entry +// * @returns {ILedgerEntry} +// */ +// private getAdjustmentGLCostEntry = R.curry( +// ( +// inventoryAdjustment: IInventoryAdjustment, +// baseCurrency: string, +// entry: IInventoryAdjustmentEntry, +// index: number +// ): ILedgerEntry => { +// const commonEntry = this.getAdjustmentGLCommonEntry( +// inventoryAdjustment, +// baseCurrency +// ); +// const amount = entry.cost * entry.quantity; + +// return { +// ...commonEntry, +// accountId: inventoryAdjustment.adjustmentAccountId, +// accountNormal: AccountNormal.DEBIT, +// credit: amount, +// index: index + 2, +// }; +// } +// ); + +// /** +// * Retrieve the inventory adjustment GL item entry. +// * @param {InventoryAdjustment} adjustment +// * @param {string} baseCurrency +// * @param {InventoryAdjustmentEntry} entry +// * @param {number} index +// * @returns {} +// */ +// private getAdjustmentGLItemEntry = R.curry( +// ( +// adjustment: IInventoryAdjustment, +// baseCurrency: string, +// entry: IInventoryAdjustmentEntry, +// index: number +// ): ILedgerEntry[] => { +// const getInventoryEntry = this.getAdjustmentGLInventoryEntry( +// adjustment, +// baseCurrency +// ); +// const inventoryEntry = getInventoryEntry(entry, index); +// const costEntry = this.getAdjustmentGLCostEntry( +// adjustment, +// baseCurrency, +// entry, +// index +// ); +// return [inventoryEntry, costEntry]; +// } +// ); + +// /** +// * Writes increment inventroy adjustment GL entries. +// * @param {InventoryAdjustment} inventoryAdjustment - +// * @param {JournalPoster} jorunal - +// * @returns {ILedgerEntry[]} +// */ +// public getIncrementAdjustmentGLEntries( +// inventoryAdjustment: IInventoryAdjustment, +// baseCurrency: string +// ): ILedgerEntry[] { +// const getItemEntry = this.getAdjustmentGLItemEntry( +// inventoryAdjustment, +// baseCurrency +// ); +// return inventoryAdjustment.entries.map(getItemEntry).flat(); +// } + +// /** +// * Writes inventory increment adjustment GL entries. +// * @param {number} tenantId +// * @param {number} inventoryAdjustmentId +// */ +// public writeAdjustmentGLEntries = async ( +// tenantId: number, +// inventoryAdjustmentId: number, +// trx?: Knex.Transaction +// ): Promise => { +// const { InventoryAdjustment } = this.tenancy.models(tenantId); + +// // Retrieves the inventory adjustment with associated entries. +// const adjustment = await InventoryAdjustment.query(trx) +// .findById(inventoryAdjustmentId) +// .withGraphFetched('entries.item'); + +// const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); + +// // Retrieves the inventory adjustment GL entries. +// const entries = this.getIncrementAdjustmentGLEntries( +// adjustment, +// tenantMeta.baseCurrency +// ); +// const ledger = new Ledger(entries); + +// // Commits the ledger entries to the storage. +// await this.ledgerStorage.commit(tenantId, ledger, trx); +// }; + +// /** +// * Reverts the adjustment transactions GL entries. +// * @param {number} tenantId +// * @param {number} inventoryAdjustmentId +// * @returns {Promise} +// */ +// public revertAdjustmentGLEntries = ( +// tenantId: number, +// inventoryAdjustmentId: number, +// trx?: Knex.Transaction +// ): Promise => { +// return this.ledgerStorage.deleteByReference( +// tenantId, +// inventoryAdjustmentId, +// 'InventoryAdjustment', +// trx +// ); +// }; + +// /** +// * Rewrite inventory adjustment GL entries. +// * @param {number} tenantId +// * @param {number} inventoryAdjustmentId +// * @param {Knex.Transaction} trx +// */ +// public rewriteAdjustmentGLEntries = async ( +// tenantId: number, +// inventoryAdjustmentId: number, +// trx?: Knex.Transaction +// ) => { +// // Reverts GL entries of the given inventory adjustment. +// await this.revertAdjustmentGLEntries(tenantId, inventoryAdjustmentId, trx); + +// // Writes GL entries of th egiven inventory adjustment. +// await this.writeAdjustmentGLEntries(tenantId, inventoryAdjustmentId, trx); +// }; +// } diff --git a/packages/server-nest/src/modules/InventoryAdjutments/InventoryAdjustmentService.ts b/packages/server-nest/src/modules/InventoryAdjutments/InventoryAdjustmentService.ts new file mode 100644 index 000000000..98342bf3a --- /dev/null +++ b/packages/server-nest/src/modules/InventoryAdjutments/InventoryAdjustmentService.ts @@ -0,0 +1,154 @@ +// import { Inject, Service } from 'typedi'; +// import { omit } from 'lodash'; +// import moment from 'moment'; +// import * as R from 'ramda'; +// import { Knex } from 'knex'; +// import { ServiceError } from '@/exceptions'; +// import { +// IInventoryAdjustment, +// IPaginationMeta, +// IInventoryAdjustmentsFilter, +// IInventoryTransaction, +// IInventoryAdjustmentEventPublishedPayload, +// IInventoryAdjustmentEventDeletedPayload, +// IInventoryAdjustmentDeletingPayload, +// IInventoryAdjustmentPublishingPayload, +// } from '@/interfaces'; +// import events from '@/subscribers/events'; +// import DynamicListingService from '@/services/DynamicListing/DynamicListService'; +// import HasTenancyService from '@/services/Tenancy/TenancyService'; +// import InventoryService from './Inventory'; +// import UnitOfWork from '@/services/UnitOfWork'; +// import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +// import InventoryAdjustmentTransformer from './InventoryAdjustmentTransformer'; +// import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; +// import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform'; +// import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; + +// const ERRORS = { +// INVENTORY_ADJUSTMENT_NOT_FOUND: 'INVENTORY_ADJUSTMENT_NOT_FOUND', +// ITEM_SHOULD_BE_INVENTORY_TYPE: 'ITEM_SHOULD_BE_INVENTORY_TYPE', +// INVENTORY_ADJUSTMENT_ALREADY_PUBLISHED: +// 'INVENTORY_ADJUSTMENT_ALREADY_PUBLISHED', +// }; + +// @Service() +// export default class InventoryAdjustmentService { +// @Inject() +// private tenancy: HasTenancyService; + +// @Inject() +// private eventPublisher: EventPublisher; + +// @Inject() +// private inventoryService: InventoryService; + +// @Inject() +// private dynamicListService: DynamicListingService; + +// @Inject() +// private uow: UnitOfWork; + +// @Inject() +// private branchDTOTransform: BranchTransactionDTOTransform; + +// @Inject() +// private warehouseDTOTransform: WarehouseTransactionDTOTransform; + +// @Inject() +// private transfromer: TransformerInjectable; + +// /** +// * Retrieve the inventory adjustment or throw not found service error. +// * @param {number} tenantId - +// * @param {number} adjustmentId - +// */ +// async getInventoryAdjustmentOrThrowError( +// tenantId: number, +// adjustmentId: number +// ) { +// const { InventoryAdjustment } = this.tenancy.models(tenantId); + +// const inventoryAdjustment = await InventoryAdjustment.query() +// .findById(adjustmentId) +// .withGraphFetched('entries'); + +// if (!inventoryAdjustment) { +// throw new ServiceError(ERRORS.INVENTORY_ADJUSTMENT_NOT_FOUND); +// } +// return inventoryAdjustment; +// } + + +// /** +// * Parses inventory adjustments list filter DTO. +// * @param filterDTO - +// */ +// private parseListFilterDTO(filterDTO) { +// return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); +// } + +// /** +// * Writes the inventory transactions from the inventory adjustment transaction. +// * @param {number} tenantId - +// * @param {IInventoryAdjustment} inventoryAdjustment - +// * @param {boolean} override - +// * @param {Knex.Transaction} trx - +// * @return {Promise} +// */ +// public async writeInventoryTransactions( +// tenantId: number, +// inventoryAdjustment: IInventoryAdjustment, +// override: boolean = false, +// trx?: Knex.Transaction +// ): Promise { +// const commonTransaction = { +// direction: inventoryAdjustment.inventoryDirection, +// date: inventoryAdjustment.date, +// transactionType: 'InventoryAdjustment', +// transactionId: inventoryAdjustment.id, +// createdAt: inventoryAdjustment.createdAt, +// costAccountId: inventoryAdjustment.adjustmentAccountId, + +// branchId: inventoryAdjustment.branchId, +// warehouseId: inventoryAdjustment.warehouseId, +// }; +// const inventoryTransactions = []; + +// inventoryAdjustment.entries.forEach((entry) => { +// inventoryTransactions.push({ +// ...commonTransaction, +// itemId: entry.itemId, +// quantity: entry.quantity, +// rate: entry.cost, +// }); +// }); +// // Saves the given inventory transactions to the storage. +// await this.inventoryService.recordInventoryTransactions( +// tenantId, +// inventoryTransactions, +// override, +// trx +// ); +// } + +// /** +// * Reverts the inventory transactions from the inventory adjustment transaction. +// * @param {number} tenantId +// * @param {number} inventoryAdjustmentId +// */ +// async revertInventoryTransactions( +// tenantId: number, +// inventoryAdjustmentId: number, +// trx?: Knex.Transaction +// ): Promise<{ oldInventoryTransactions: IInventoryTransaction[] }> { +// return this.inventoryService.deleteInventoryTransactions( +// tenantId, +// inventoryAdjustmentId, +// 'InventoryAdjustment', +// trx +// ); +// } + + +// } diff --git a/packages/server-nest/src/modules/InventoryAdjutments/InventoryAdjustmentTransformer.ts b/packages/server-nest/src/modules/InventoryAdjutments/InventoryAdjustmentTransformer.ts new file mode 100644 index 000000000..9380f86e6 --- /dev/null +++ b/packages/server-nest/src/modules/InventoryAdjutments/InventoryAdjustmentTransformer.ts @@ -0,0 +1,25 @@ +import { Transformer } from "../Transformer/Transformer"; +import { InventoryAdjustment } from "./models/InventoryAdjustment"; + +export class InventoryAdjustmentTransformer extends Transformer { + /** + * Include these attributes to sale invoice object. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return ['formattedType']; + }; + + /** + * Retrieves the formatted and localized adjustment type. + * @param {IInventoryAdjustment} inventoryAdjustment + * @returns {string} + */ + formattedType(inventoryAdjustment: InventoryAdjustment) { + const types = { + increment: 'inventory_adjustment.type.increment', + decrement: 'inventory_adjustment.type.decrement', + }; + return this.context.i18n.t(types[inventoryAdjustment.type] || ''); + } +} diff --git a/packages/server-nest/src/modules/InventoryAdjutments/InventoryAdjustments.controller.ts b/packages/server-nest/src/modules/InventoryAdjutments/InventoryAdjustments.controller.ts new file mode 100644 index 000000000..617b25ad0 --- /dev/null +++ b/packages/server-nest/src/modules/InventoryAdjutments/InventoryAdjustments.controller.ts @@ -0,0 +1,38 @@ +import { Body, Controller, Delete, Param, Post } from '@nestjs/common'; +import { InventoryAdjustmentsApplicationService } from './InventoryAdjustmentsApplication.service'; +import { IQuickInventoryAdjustmentDTO } from './types/InventoryAdjustments.types'; +import { InventoryAdjustment } from './models/InventoryAdjustment'; + +@Controller('inventory-adjustments') +export class InventoryAdjustmentsController { + constructor( + private readonly inventoryAdjustmentsApplicationService: InventoryAdjustmentsApplicationService, + ) {} + + @Post('quick') + public async createQuickInventoryAdjustment( + @Body() quickAdjustmentDTO: IQuickInventoryAdjustmentDTO, + ): Promise { + return this.inventoryAdjustmentsApplicationService.createQuickInventoryAdjustment( + quickAdjustmentDTO, + ); + } + + @Delete(':id') + public async deleteInventoryAdjustment( + @Param('id') inventoryAdjustmentId: number, + ): Promise { + return this.inventoryAdjustmentsApplicationService.deleteInventoryAdjustment( + inventoryAdjustmentId, + ); + } + + @Post(':id/publish') + public async publishInventoryAdjustment( + @Param('id') inventoryAdjustmentId: number, + ): Promise { + return this.inventoryAdjustmentsApplicationService.publishInventoryAdjustment( + inventoryAdjustmentId, + ); + } +} diff --git a/packages/server-nest/src/modules/InventoryAdjutments/InventoryAdjustments.module.ts b/packages/server-nest/src/modules/InventoryAdjutments/InventoryAdjustments.module.ts new file mode 100644 index 000000000..77238773c --- /dev/null +++ b/packages/server-nest/src/modules/InventoryAdjutments/InventoryAdjustments.module.ts @@ -0,0 +1,30 @@ +import { Module } from '@nestjs/common'; +import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module'; +import { InventoryAdjustment } from './models/InventoryAdjustment'; +import { InventoryAdjustmentEntry } from './models/InventoryAdjustmentEntry'; +import { CreateQuickInventoryAdjustmentService } from './commands/CreateQuickInventoryAdjustment.service'; +import { PublishInventoryAdjustmentService } from './commands/PublishInventoryAdjustment.service'; +import { GetInventoryAdjustmentService } from './queries/GetInventoryAdjustment.service'; +import { GetInventoryAdjustmentsService } from './queries/GetInventoryAdjustments.service'; +import { DeleteInventoryAdjustmentService } from './commands/DeleteInventoryAdjustment.service'; +import { InventoryAdjustmentsApplicationService } from './InventoryAdjustmentsApplication.service'; +import { InventoryAdjustmentsController } from './InventoryAdjustments.controller'; + +const models = [ + RegisterTenancyModel(InventoryAdjustment), + RegisterTenancyModel(InventoryAdjustmentEntry), +]; +@Module({ + controllers: [InventoryAdjustmentsController], + providers: [ + ...models, + CreateQuickInventoryAdjustmentService, + PublishInventoryAdjustmentService, + GetInventoryAdjustmentsService, + GetInventoryAdjustmentService, + DeleteInventoryAdjustmentService, + InventoryAdjustmentsApplicationService, + ], + exports: [...models], +}) +export class InventoryAdjustmentsModule {} diff --git a/packages/server-nest/src/modules/InventoryAdjutments/InventoryAdjustmentsApplication.service.ts b/packages/server-nest/src/modules/InventoryAdjutments/InventoryAdjustmentsApplication.service.ts new file mode 100644 index 000000000..16ea35dab --- /dev/null +++ b/packages/server-nest/src/modules/InventoryAdjutments/InventoryAdjustmentsApplication.service.ts @@ -0,0 +1,51 @@ +import { Injectable } from '@nestjs/common'; +import { DeleteInventoryAdjustmentService } from './commands/DeleteInventoryAdjustment.service'; +import { PublishInventoryAdjustmentService } from './commands/PublishInventoryAdjustment.service'; +import { CreateQuickInventoryAdjustmentService } from './commands/CreateQuickInventoryAdjustment.service'; +import { IQuickInventoryAdjustmentDTO } from './types/InventoryAdjustments.types'; +import { InventoryAdjustment } from './models/InventoryAdjustment'; + +@Injectable() +export class InventoryAdjustmentsApplicationService { + constructor( + private readonly createQuickInventoryAdjustmentService: CreateQuickInventoryAdjustmentService, + private readonly deleteInventoryAdjustmentService: DeleteInventoryAdjustmentService, + private readonly publishInventoryAdjustmentService: PublishInventoryAdjustmentService, + ) {} + + /** + * Creates a quick inventory adjustment transaction. + * @param {IQuickInventoryAdjustmentDTO} quickAdjustmentDTO - Quick inventory adjustment DTO. + */ + public async createQuickInventoryAdjustment( + quickAdjustmentDTO: IQuickInventoryAdjustmentDTO, + ): Promise { + return this.createQuickInventoryAdjustmentService.createQuickAdjustment( + quickAdjustmentDTO, + ); + } + + /** + * Deletes the inventory adjustment transaction. + * @param {number} inventoryAdjustmentId - Inventory adjustment id. + */ + public async deleteInventoryAdjustment( + inventoryAdjustmentId: number, + ): Promise { + return this.deleteInventoryAdjustmentService.deleteInventoryAdjustment( + inventoryAdjustmentId, + ); + } + + /** + * Publishes the inventory adjustment transaction. + * @param {number} inventoryAdjustmentId - Inventory adjustment id. + */ + public async publishInventoryAdjustment( + inventoryAdjustmentId: number, + ): Promise { + return this.publishInventoryAdjustmentService.publishInventoryAdjustment( + inventoryAdjustmentId, + ); + } +} diff --git a/packages/server-nest/src/modules/InventoryAdjutments/commands/CreateQuickInventoryAdjustment.service.ts b/packages/server-nest/src/modules/InventoryAdjutments/commands/CreateQuickInventoryAdjustment.service.ts new file mode 100644 index 000000000..7562efe2d --- /dev/null +++ b/packages/server-nest/src/modules/InventoryAdjutments/commands/CreateQuickInventoryAdjustment.service.ts @@ -0,0 +1,147 @@ +import { Knex } from 'knex'; +import { Inject } from '@nestjs/common'; +import * as R from 'ramda'; +import { omit } from 'lodash'; +import { events } from '@/common/events/events'; +import { InventoryAdjustment } from '../models/InventoryAdjustment'; +import { InventoryAdjustmentEntry } from '../models/InventoryAdjustmentEntry'; +import { + IInventoryAdjustmentCreatingPayload, + IInventoryAdjustmentEventCreatedPayload, + IQuickInventoryAdjustmentDTO, +} from '../types/InventoryAdjustments.types'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { ServiceError } from '@/modules/Items/ServiceError'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { Item } from '@/modules/Items/models/Item'; +import { Account } from '@/modules/Accounts/models/Account.model'; +import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform'; +import { WarehouseTransactionDTOTransform } from '@/modules/Warehouses/Integrations/WarehouseTransactionDTOTransform'; + +export class CreateQuickInventoryAdjustmentService { + constructor( + @Inject(InventoryAdjustment.name) + private readonly inventoryAdjustmentModel: typeof InventoryAdjustment, + + @Inject(InventoryAdjustmentEntry.name) + private readonly inventoryAdjustmentEntryModel: typeof InventoryAdjustmentEntry, + + @Inject(Item.name) + private readonly itemModel: typeof Item, + + @Inject(Account.name) + private readonly accountModel: typeof Account, + + private readonly eventEmitter: EventEmitter2, + private readonly uow: UnitOfWork, + private readonly warehouseDTOTransform: WarehouseTransactionDTOTransform, + private readonly branchDTOTransform: BranchTransactionDTOTransformer, + ) {} + + /** + * Transformes the quick inventory adjustment DTO to model object. + * @param {IQuickInventoryAdjustmentDTO} adjustmentDTO - + * @return {IInventoryAdjustment} + */ + private transformQuickAdjToModel( + adjustmentDTO: IQuickInventoryAdjustmentDTO, + ): InventoryAdjustment { + const entries = [ + { + index: 1, + itemId: adjustmentDTO.itemId, + ...('increment' === adjustmentDTO.type + ? { + quantity: adjustmentDTO.quantity, + cost: adjustmentDTO.cost, + } + : {}), + ...('decrement' === adjustmentDTO.type + ? { + quantity: adjustmentDTO.quantity, + } + : {}), + }, + ]; + const initialDTO = { + ...omit(adjustmentDTO, ['quantity', 'cost', 'itemId', 'publish']), + userId: authorizedUser.id, + ...(adjustmentDTO.publish + ? { + publishedAt: moment().toMySqlDateTime(), + } + : {}), + entries, + }; + return R.compose( + this.warehouseDTOTransform.transformDTO, + this.branchDTOTransform.transformDTO, + )(initialDTO) as InventoryAdjustment; + } + + /** + * Creates a quick inventory adjustment for specific item. + * @param {number} tenantId - Tenant id. + * @param {IQuickInventoryAdjustmentDTO} quickAdjustmentDTO - qucik adjustment DTO. + */ + public async createQuickAdjustment( + quickAdjustmentDTO: IQuickInventoryAdjustmentDTO, + ): Promise { + // Retrieve the adjustment account or throw not found error. + const adjustmentAccount = await this.accountModel + .query() + .findById(quickAdjustmentDTO.adjustmentAccountId) + .throwIfNotFound(); + + // Retrieve the item model or throw not found service error. + const item = await this.itemModel + .query() + .findById(quickAdjustmentDTO.itemId) + .throwIfNotFound(); + + // Validate item inventory type. + this.validateItemInventoryType(item); + + // Transform the DTO to inventory adjustment model. + const invAdjustmentObject = + this.transformQuickAdjToModel(quickAdjustmentDTO); + // Writes inventory adjustment transaction with associated transactions + // under unit-of-work envirment. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onInventoryAdjustmentCreating` event. + await this.eventEmitter.emitAsync( + events.inventoryAdjustment.onQuickCreating, + { + quickAdjustmentDTO, + trx, + } as IInventoryAdjustmentCreatingPayload, + ); + // Saves the inventory adjustment with associated entries to the storage. + const inventoryAdjustment = await this.inventoryAdjustmentModel + .query(trx) + .upsertGraph({ + ...invAdjustmentObject, + }); + // Triggers `onInventoryAdjustmentQuickCreated` event. + await this.eventEmitter.emitAsync( + events.inventoryAdjustment.onQuickCreated, + { + inventoryAdjustment, + inventoryAdjustmentId: inventoryAdjustment.id, + trx, + } as IInventoryAdjustmentEventCreatedPayload, + ); + return inventoryAdjustment; + }); + } + + /** + * Validate the item inventory type. + * @param {IItem} item + */ + validateItemInventoryType(item) { + if (item.type !== 'inventory') { + throw new ServiceError(ERRORS.ITEM_SHOULD_BE_INVENTORY_TYPE); + } + } +} diff --git a/packages/server-nest/src/modules/InventoryAdjutments/commands/DeleteInventoryAdjustment.service.ts b/packages/server-nest/src/modules/InventoryAdjutments/commands/DeleteInventoryAdjustment.service.ts new file mode 100644 index 000000000..e62321907 --- /dev/null +++ b/packages/server-nest/src/modules/InventoryAdjutments/commands/DeleteInventoryAdjustment.service.ts @@ -0,0 +1,67 @@ +import { Inject } from '@nestjs/common'; +import { Knex } from 'knex'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { events } from '@/common/events/events'; +import { + IInventoryAdjustmentDeletingPayload, + IInventoryAdjustmentEventDeletedPayload, +} from '../types/InventoryAdjustments.types'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { InventoryAdjustmentEntry } from '../models/InventoryAdjustmentEntry'; +import { InventoryAdjustment } from '../models/InventoryAdjustment'; + +export class DeleteInventoryAdjustmentService { + constructor( + private readonly eventEmitter: EventEmitter2, + private readonly uow: UnitOfWork, + + @Inject(InventoryAdjustment.name) + private readonly inventoryAdjustmentModel: typeof InventoryAdjustment, + + @Inject(InventoryAdjustmentEntry.name) + private readonly inventoryAdjustmentEntryModel: typeof InventoryAdjustmentEntry, + ) {} + + /** + * Deletes the inventory adjustment transaction. + * @param {number} inventoryAdjustmentId - Inventory adjustment id. + */ + public async deleteInventoryAdjustment( + inventoryAdjustmentId: number, + ): Promise { + // Retrieve the inventory adjustment or throw not found service error. + const oldInventoryAdjustment = await this.inventoryAdjustmentModel + .query() + .findById(inventoryAdjustmentId) + .throwIfNotFound(); + + // Deletes the inventory adjustment transaction and associated transactions + // under unit-of-work env. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onInventoryAdjustmentDeleting` event. + await this.eventEmitter.emitAsync(events.inventoryAdjustment.onDeleting, { + oldInventoryAdjustment, + trx, + } as IInventoryAdjustmentDeletingPayload); + + // Deletes the inventory adjustment entries. + await this.inventoryAdjustmentEntryModel + .query(trx) + .where('adjustment_id', inventoryAdjustmentId) + .delete(); + + // Deletes the inventory adjustment transaction. + await this.inventoryAdjustmentModel + .query(trx) + .findById(inventoryAdjustmentId) + .delete(); + + // Triggers `onInventoryAdjustmentDeleted` event. + await this.eventEmitter.emitAsync(events.inventoryAdjustment.onDeleted, { + inventoryAdjustmentId, + oldInventoryAdjustment, + trx, + } as IInventoryAdjustmentEventDeletedPayload); + }); + } +} diff --git a/packages/server-nest/src/modules/InventoryAdjutments/commands/PublishInventoryAdjustment.service.ts b/packages/server-nest/src/modules/InventoryAdjutments/commands/PublishInventoryAdjustment.service.ts new file mode 100644 index 000000000..61cc06955 --- /dev/null +++ b/packages/server-nest/src/modules/InventoryAdjutments/commands/PublishInventoryAdjustment.service.ts @@ -0,0 +1,97 @@ +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { Inject } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { InventoryAdjustment } from '../models/InventoryAdjustment'; +import { InventoryAdjustmentEntry } from '../models/InventoryAdjustmentEntry'; +import { + IInventoryAdjustmentEventPublishedPayload, + IInventoryAdjustmentPublishingPayload, +} from '../types/InventoryAdjustments.types'; +import { events } from '@/common/events/events'; +import { Knex } from 'knex'; +import { ServiceError } from '@/modules/Items/ServiceError'; + +export class PublishInventoryAdjustmentService { + constructor( + private readonly eventEmitter: EventEmitter2, + private readonly uow: UnitOfWork, + + @Inject(InventoryAdjustment.name) + private readonly inventoryAdjustmentModel: typeof InventoryAdjustment, + + @Inject(InventoryAdjustmentEntry.name) + private readonly inventoryAdjustmentEntryModel: typeof InventoryAdjustmentEntry, + ) {} + + /** + * Publish the inventory adjustment transaction. + * @param {number} tenantId + * @param {number} inventoryAdjustmentId + */ + public async publishInventoryAdjustment( + inventoryAdjustmentId: number, + ): Promise { + // Retrieve the inventory adjustment or throw not found service error. + const oldInventoryAdjustment = await this.inventoryAdjustmentModel + .query() + .findById(inventoryAdjustmentId) + .throwIfNotFound(); + + // Validate adjustment not already published. + this.validateAdjustmentTransactionsNotPublished(oldInventoryAdjustment); + + // Publishes inventory adjustment with associated inventory transactions + // under unit-of-work envirement. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + await this.eventEmitter.emitAsync( + events.inventoryAdjustment.onPublishing, + { + trx, + oldInventoryAdjustment, + } as IInventoryAdjustmentPublishingPayload, + ); + + // Publish the inventory adjustment transaction. + await InventoryAdjustment.query().findById(inventoryAdjustmentId).patch({ + publishedAt: moment().toMySqlDateTime(), + }); + // Retrieve the inventory adjustment after the modification. + const inventoryAdjustment = await InventoryAdjustment.query() + .findById(inventoryAdjustmentId) + .withGraphFetched('entries'); + + // Triggers `onInventoryAdjustmentDeleted` event. + await this.eventEmitter.emitAsync( + events.inventoryAdjustment.onPublished, + { + inventoryAdjustmentId, + inventoryAdjustment, + oldInventoryAdjustment, + trx, + } as IInventoryAdjustmentEventPublishedPayload, + ); + }); + } + + /** + * Validate the adjustment transaction is exists. + * @param {IInventoryAdjustment} inventoryAdjustment + */ + private throwIfAdjustmentNotFound(inventoryAdjustment: InventoryAdjustment) { + if (!inventoryAdjustment) { + throw new ServiceError(ERRORS.INVENTORY_ADJUSTMENT_NOT_FOUND); + } + } + + /** + * Validates the adjustment transaction is not already published. + * @param {IInventoryAdjustment} oldInventoryAdjustment + */ + private validateAdjustmentTransactionsNotPublished( + oldInventoryAdjustment: InventoryAdjustment, + ) { + if (oldInventoryAdjustment.isPublished) { + throw new ServiceError(ERRORS.INVENTORY_ADJUSTMENT_ALREADY_PUBLISHED); + } + } +} diff --git a/packages/server-nest/src/modules/InventoryAdjutments/models/InventoryAdjustment.ts b/packages/server-nest/src/modules/InventoryAdjutments/models/InventoryAdjustment.ts new file mode 100644 index 000000000..9383019d5 --- /dev/null +++ b/packages/server-nest/src/modules/InventoryAdjutments/models/InventoryAdjustment.ts @@ -0,0 +1,124 @@ +import { Model, mixin } from 'objection'; +// import TenantModel from 'models/TenantModel'; +// import InventoryAdjustmentSettings from './InventoryAdjustment.Settings'; +// import ModelSetting from './ModelSetting'; +import { BaseModel } from '@/models/Model'; + +export class InventoryAdjustment extends BaseModel { + date!: string; + type!: string; + adjustmentAccountId!: number; + reason?: string; + referenceNo!: string; + description?: string; + userId!: number; + publishedAt?: string; + + branchId!: number; + warehouseId!: number; + + /** + * Table name + */ + static get tableName() { + return 'inventory_adjustments'; + } + + /** + * Timestamps columns. + */ + get timestamps() { + return ['created_at']; + } + + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return ['formattedType', 'inventoryDirection', 'isPublished']; + } + + /** + * Retrieve formatted adjustment type. + */ + get formattedType() { + return InventoryAdjustment.getFormattedType(this.type); + } + + /** + * Retrieve formatted reference type. + */ + get inventoryDirection() { + return InventoryAdjustment.getInventoryDirection(this.type); + } + + /** + * Detarmines whether the adjustment is published. + * @return {boolean} + */ + get isPublished() { + return !!this.publishedAt; + } + + static getInventoryDirection(type) { + const directions = { + increment: 'IN', + decrement: 'OUT', + }; + return directions[type] || ''; + } + + /** + * Retrieve the formatted adjustment type of the given type. + * @param {string} type + * @returns {string} + */ + static getFormattedType(type) { + const types = { + increment: 'inventory_adjustment.type.increment', + decrement: 'inventory_adjustment.type.decrement', + }; + return types[type]; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const { InventoryAdjustmentEntry } = require('./InventoryAdjustmentEntry'); + const { Account } = require('../../Accounts/models/Account.model'); + + return { + /** + * Adjustment entries. + */ + entries: { + relation: Model.HasManyRelation, + modelClass: InventoryAdjustmentEntry, + join: { + from: 'inventory_adjustments.id', + to: 'inventory_adjustments_entries.adjustmentId', + }, + }, + + /** + * Inventory adjustment account. + */ + adjustmentAccount: { + relation: Model.BelongsToOneRelation, + modelClass: Account, + join: { + from: 'inventory_adjustments.adjustmentAccountId', + to: 'accounts.id', + }, + }, + }; + } + + /** + * Model settings. + */ + // static get meta() { + // return InventoryAdjustmentSettings; + // } +} diff --git a/packages/server-nest/src/modules/InventoryAdjutments/models/InventoryAdjustmentEntry.ts b/packages/server-nest/src/modules/InventoryAdjutments/models/InventoryAdjustmentEntry.ts new file mode 100644 index 000000000..46faf5656 --- /dev/null +++ b/packages/server-nest/src/modules/InventoryAdjutments/models/InventoryAdjustmentEntry.ts @@ -0,0 +1,43 @@ +import { Model } from 'objection'; +import { BaseModel } from '@/models/Model'; +// import TenantModel from 'models/TenantModel'; + +export class InventoryAdjustmentEntry extends BaseModel { + /** + * Table name. + */ + static get tableName() { + return 'inventory_adjustments_entries'; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const { InventoryAdjustment } = require('./InventoryAdjustment'); + const { Item } = require('../../Items/models/Item'); + + return { + inventoryAdjustment: { + relation: Model.BelongsToOneRelation, + modelClass: InventoryAdjustment, + join: { + from: 'inventory_adjustments_entries.adjustmentId', + to: 'inventory_adjustments.id', + }, + }, + + /** + * Entry item. + */ + item: { + relation: Model.BelongsToOneRelation, + modelClass: Item, + join: { + from: 'inventory_adjustments_entries.itemId', + to: 'items.id', + }, + }, + }; + } +} diff --git a/packages/server-nest/src/modules/InventoryAdjutments/queries/GetInventoryAdjustment.service.ts b/packages/server-nest/src/modules/InventoryAdjutments/queries/GetInventoryAdjustment.service.ts new file mode 100644 index 000000000..c44b45607 --- /dev/null +++ b/packages/server-nest/src/modules/InventoryAdjutments/queries/GetInventoryAdjustment.service.ts @@ -0,0 +1,31 @@ +import { TransformerInjectable } from "@/modules/Transformer/TransformerInjectable.service"; +import { InventoryAdjustment } from "../models/InventoryAdjustment"; +import { InventoryAdjustmentTransformer } from "../InventoryAdjustmentTransformer"; + +export class GetInventoryAdjustmentService { + constructor( + private readonly transformer: TransformerInjectable, + ) {} + + /** + * Retrieve specific inventory adjustment transaction details. + * @param {number} inventoryAdjustmentId - Inventory adjustment id. + */ + async getInventoryAdjustment( + inventoryAdjustmentId: number, + ) { + // Retrieve inventory adjustment transation with associated models. + const inventoryAdjustment = await InventoryAdjustment.query() + .findById(inventoryAdjustmentId) + .withGraphFetched('entries.item') + .withGraphFetched('adjustmentAccount'); + + // Throw not found if the given adjustment transaction not exists. + this.throwIfAdjustmentNotFound(inventoryAdjustment); + + return this.transformer.transform( + inventoryAdjustment, + new InventoryAdjustmentTransformer(), + ); + } +} diff --git a/packages/server-nest/src/modules/InventoryAdjutments/queries/GetInventoryAdjustments.service.ts b/packages/server-nest/src/modules/InventoryAdjutments/queries/GetInventoryAdjustments.service.ts new file mode 100644 index 000000000..1647819d5 --- /dev/null +++ b/packages/server-nest/src/modules/InventoryAdjutments/queries/GetInventoryAdjustments.service.ts @@ -0,0 +1,56 @@ +import { InventoryAdjustmentTransformer } from "../InventoryAdjustmentTransformer"; +import { InventoryAdjustment } from "../models/InventoryAdjustment"; +import { IInventoryAdjustmentsFilter } from "../types/InventoryAdjustments.types"; + +import { TransformerInjectable } from "@/modules/Transformer/TransformerInjectable.service"; + +export class GetInventoryAdjustmentsService { + + constructor( + public readonly transformer: TransformerInjectable, + private readonly inventoryAdjustmentModel: typeof InventoryAdjustment, + ) { + + } + /** + * Retrieve the inventory adjustments paginated list. + * @param {number} tenantId + * @param {IInventoryAdjustmentsFilter} adjustmentsFilter + */ + public async getInventoryAdjustments( + tenantId: number, + filterDTO: IInventoryAdjustmentsFilter, + ): Promise<{ + inventoryAdjustments: IInventoryAdjustment[]; + pagination: IPaginationMeta; + }> { + + // Parses inventory adjustments list filter DTO. + const filter = this.parseListFilterDTO(filterDTO); + + // Dynamic list service. + const dynamicFilter = await this.dynamicListService.dynamicList( + tenantId, + InventoryAdjustment, + filter, + ); + const { results, pagination } = await this.inventoryAdjustmentModel.query() + .onBuild((query) => { + query.withGraphFetched('entries.item'); + query.withGraphFetched('adjustmentAccount'); + + dynamicFilter.buildQuery()(query); + }) + .pagination(filter.page - 1, filter.pageSize); + + // Retrieves the transformed inventory adjustments. + const inventoryAdjustments = await this.transformer.transform( + results, + new InventoryAdjustmentTransformer(), + ); + return { + inventoryAdjustments, + pagination, + }; + } +} diff --git a/packages/server-nest/src/modules/InventoryAdjutments/types/InventoryAdjustments.types.ts b/packages/server-nest/src/modules/InventoryAdjutments/types/InventoryAdjustments.types.ts new file mode 100644 index 000000000..4e988a234 --- /dev/null +++ b/packages/server-nest/src/modules/InventoryAdjutments/types/InventoryAdjustments.types.ts @@ -0,0 +1,62 @@ +import { Knex } from 'knex'; +import { InventoryAdjustment } from '../models/InventoryAdjustment'; + +type IAdjustmentTypes = 'increment' | 'decrement'; + +export interface IQuickInventoryAdjustmentDTO { + date: Date; + type: IAdjustmentTypes; + adjustmentAccountId: number; + reason: string; + description: string; + referenceNo: string; + itemId: number; + quantity: number; + cost: number; + publish: boolean; + + warehouseId?: number; + branchId?: number; +} +export interface IInventoryAdjustmentsFilter { + page: number; + pageSize: number; +} + +export interface IInventoryAdjustmentEventCreatedPayload { + inventoryAdjustment: InventoryAdjustment; + inventoryAdjustmentId: number; + trx: Knex.Transaction; +} +export interface IInventoryAdjustmentCreatingPayload { + quickAdjustmentDTO: IQuickInventoryAdjustmentDTO; + trx: Knex.Transaction; +} + +export interface IInventoryAdjustmentEventPublishedPayload { + inventoryAdjustmentId: number; + inventoryAdjustment: InventoryAdjustment; + trx: Knex.Transaction; +} + +export interface IInventoryAdjustmentPublishingPayload { + trx: Knex.Transaction; + oldInventoryAdjustment: InventoryAdjustment; +} +export interface IInventoryAdjustmentEventDeletedPayload { + inventoryAdjustmentId: number; + oldInventoryAdjustment: InventoryAdjustment; + trx: Knex.Transaction; +} + +export interface IInventoryAdjustmentDeletingPayload { + oldInventoryAdjustment: InventoryAdjustment; + trx: Knex.Transaction; +} + +export enum InventoryAdjustmentAction { + CREATE = 'Create', + EDIT = 'Edit', + DELETE = 'Delete', + VIEW = 'View', +}