refactor: inventory transfers to nestjs

This commit is contained in:
Ahmed Bouhuolia
2025-03-13 00:44:11 +02:00
parent 67ae7ad037
commit cf496909a5
48 changed files with 2334 additions and 135 deletions

View File

@@ -230,6 +230,7 @@ export class Account extends TenantBaseModel {
to: 'accounts_transactions.accountId',
},
},
/**
* Account may has many items as cost account.
*/

View File

@@ -69,6 +69,8 @@ import { MailModule } from '../Mail/Mail.module';
import { FinancialStatementsModule } from '../FinancialStatements/FinancialStatements.module';
import { StripePaymentModule } from '../StripePayment/StripePayment.module';
import { FeaturesModule } from '../Features/Features.module';
import { InventoryCostModule } from '../InventoryCost/InventoryCost.module';
import { WarehousesTransfersModule } from '../WarehousesTransfers/WarehouseTransfers.module';
@Module({
imports: [
@@ -135,6 +137,7 @@ import { FeaturesModule } from '../Features/Features.module';
PdfTemplatesModule,
BranchesModule,
WarehousesModule,
WarehousesTransfersModule,
CustomersModule,
VendorsModule,
SaleInvoicesModule,
@@ -160,6 +163,7 @@ import { FeaturesModule } from '../Features/Features.module';
SettingsModule,
FeaturesModule,
InventoryAdjustmentsModule,
InventoryCostModule,
PostHogModule,
EventTrackerModule,
FinancialStatementsModule,

View File

@@ -15,7 +15,7 @@ export class ImportAls {
* @returns The result of the callback function.
*/
public run<T>(callback: () => T): T {
return this.als.run<T>(new Map(), callback);
return this.als.run<T, any>(new Map(), callback);
}
/**

View File

@@ -8,6 +8,18 @@ import { InventoryItemsQuantitySyncService } from './commands/InventoryItemsQuan
import { InventoryTransactionsService } from './commands/InventoryTransactions.service';
import { LedgerModule } from '../Ledger/Ledger.module';
import { InventoryComputeCostService } from './commands/InventoryComputeCost.service';
import { InventoryCostApplication } from './InventoryCostApplication';
import { StoreInventoryLotsCostService } from './commands/StoreInventortyLotsCost.service';
import { ComputeItemCostProcessor } from './processors/ComputeItemCost.processor';
import { WriteInventoryTransactionsGLEntriesProcessor } from './processors/WriteInventoryTransactionsGLEntries.processor';
import {
ComputeItemCostQueue,
WriteInventoryTransactionsGLEntriesQueue,
} from './types/InventoryCost.types';
import { BullModule } from '@nestjs/bullmq';
import { InventoryAverageCostMethodService } from './commands/InventoryAverageCostMethod.service';
import { InventoryItemCostService } from './commands/InventoryCosts.service';
import { InventoryItemOpeningAvgCostService } from './commands/InventoryItemOpeningAvgCost.service';
const models = [
RegisterTenancyModel(InventoryCostLotTracker),
@@ -15,14 +27,28 @@ const models = [
];
@Module({
imports: [LedgerModule, ...models],
imports: [
LedgerModule,
...models,
BullModule.registerQueue({ name: ComputeItemCostQueue }),
BullModule.registerQueue({
name: WriteInventoryTransactionsGLEntriesQueue,
}),
],
providers: [
InventoryCostGLBeforeWriteSubscriber,
InventoryCostGLStorage,
InventoryItemsQuantitySyncService,
InventoryTransactionsService,
InventoryComputeCostService,
InventoryCostApplication,
StoreInventoryLotsCostService,
ComputeItemCostProcessor,
WriteInventoryTransactionsGLEntriesProcessor,
InventoryAverageCostMethodService,
InventoryItemCostService,
InventoryItemOpeningAvgCostService,
],
exports: [...models, InventoryTransactionsService],
exports: [...models, InventoryTransactionsService, InventoryItemCostService],
})
export class InventoryCostModule {}

View File

@@ -3,12 +3,6 @@ import { Knex } from 'knex';
import { InventoryTransaction } from '../models/InventoryTransaction';
export class InventoryAverageCostMethod {
/**
* Constructor method.
* @param {number} tenantId - The given tenant id.
* @param {Date} startingDate -
* @param {number} itemId - The given inventory item id.
*/
constructor() {}
/**

View File

@@ -72,7 +72,6 @@ export class InventoryComputeCostService {
* @param {Date} startingDate
*/
async scheduleComputeItemCost(
tenantId: number,
itemId: number,
startingDate: Date | string,
) {

View File

@@ -1,6 +1,4 @@
import { keyBy, get } from 'lodash';
import { Knex } from 'knex';
import * as R from 'ramda';
import { IInventoryItemCostMeta } from '../types/InventoryCost.types';
import { Inject, Injectable } from '@nestjs/common';
import { InventoryTransaction } from '../models/InventoryTransaction';
@@ -22,33 +20,30 @@ export class InventoryItemCostService {
/**
*
* @param {} INValuationMap -
* @param {} OUTValuationMap -
* @param {Map<number, IInventoryItemCostMeta>} INValuationMap -
* @param {Map<number, IInventoryItemCostMeta>} OUTValuationMap -
* @param {number} itemId
*/
private getItemInventoryMeta = R.curry(
(
INValuationMap,
OUTValuationMap,
itemId: number,
): IInventoryItemCostMeta => {
const INCost = get(INValuationMap, `[${itemId}].cost`, 0);
const INQuantity = get(INValuationMap, `[${itemId}].quantity`, 0);
private getItemInventoryMeta(
INValuationMap: Map<number, IInventoryItemCostMeta>,
OUTValuationMap: Map<number, IInventoryItemCostMeta>,
itemId: number,
) {
const INCost = get(INValuationMap, `[${itemId}].cost`, 0);
const INQuantity = get(INValuationMap, `[${itemId}].quantity`, 0);
const OUTCost = get(OUTValuationMap, `[${itemId}].cost`, 0);
const OUTQuantity = get(OUTValuationMap, `[${itemId}].quantity`, 0);
const OUTCost = get(OUTValuationMap, `[${itemId}].cost`, 0);
const OUTQuantity = get(OUTValuationMap, `[${itemId}].quantity`, 0);
const valuation = INCost - OUTCost;
const quantity = INQuantity - OUTQuantity;
const average = quantity ? valuation / quantity : 0;
const valuation = INCost - OUTCost;
const quantity = INQuantity - OUTQuantity;
const average = quantity ? valuation / quantity : 0;
return { itemId, valuation, quantity, average };
},
);
return { itemId, valuation, quantity, average };
}
/**
*
* @param {number} tenantId
* @param {number} itemsId
* @param {Date} date
* @returns
@@ -57,7 +52,7 @@ export class InventoryItemCostService {
itemsId: number[],
date: Date,
): Promise<any> => {
const commonBuilder = (builder: Knex.QueryBuilder) => {
const commonBuilder = (builder) => {
if (date) {
builder.where('date', '<', date);
}
@@ -84,7 +79,6 @@ export class InventoryItemCostService {
/**
*
* @param {number} tenantId -
* @param {number[]} itemsIds -
* @param {Date} date -
*/
@@ -122,11 +116,10 @@ export class InventoryItemCostService {
const [OUTValuationMap, INValuationMap] =
await this.getItemsInventoryInOutMap(itemsId, date);
const getItemValuation = this.getItemInventoryMeta(
INValuationMap,
OUTValuationMap,
);
const itemsValuations = inventoryItemsIds.map(getItemValuation);
const getItemValuation = (itemId: number) =>
this.getItemInventoryMeta(INValuationMap, OUTValuationMap, itemId);
const itemsValuations = inventoryItemsIds.map((id) => getItemValuation(id));
const itemsValuationsMap = new Map(
itemsValuations.map((i) => [i.itemId, i]),
);

View File

@@ -1,10 +1,11 @@
import { Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '../../System/models/TenantBaseModel';
import { InventoryCostLotTracker } from '../models/InventoryCostLotTracker';
@Injectable()
export class InventoryItemOpeningAvgCostService {
constructor(
@Inject(InventoryCostLotTracker.name)
private readonly inventoryCostLotTrackerModel: TenantModelProxy<
typeof InventoryCostLotTracker
>,
@@ -31,6 +32,10 @@ export class InventoryItemOpeningAvgCostService {
builder.sum('cost as cost');
builder.first();
};
interface QueryResult {
cost: number;
quantity: number;
}
// Calculates the total inventory total quantity and rate `IN` transactions.
const inInvSumationOper = this.inventoryCostLotTrackerModel()
.query()
@@ -43,10 +48,11 @@ export class InventoryItemOpeningAvgCostService {
.onBuild(commonBuilder)
.where('direction', 'OUT');
const [inInvSumation, outInvSumation] = await Promise.all([
const [inInvSumation, outInvSumation] = (await Promise.all([
inInvSumationOper,
outInvSumationOper,
]);
])) as unknown as [QueryResult, QueryResult];
return this.computeItemAverageCost(
inInvSumation?.cost || 0,
inInvSumation?.quantity || 0,

View File

@@ -33,7 +33,7 @@ export class InventoryTransactionsService {
* @return {Promise<void>}
*/
async recordInventoryTransactions(
transactions: InventoryTransaction[],
transactions: ModelObject<InventoryTransaction>[],
override: boolean = false,
trx?: Knex.Transaction,
): Promise<void> {

View File

@@ -7,21 +7,20 @@ import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
import { InventoryTransactionMeta } from './InventoryTransactionMeta';
export class InventoryTransaction extends TenantBaseModel {
date: Date | string;
direction: TInventoryTransactionDirection;
itemId: number;
quantity: number | null;
rate: number;
transactionType: string;
transactionId: number;
date!: Date | string;
direction!: TInventoryTransactionDirection;
itemId!: number;
quantity!: number | null;
rate!: number;
transactionType!: string;
transactionId!: number;
costAccountId?: number;
entryId: number;
entryId!: number;
createdAt?: Date;
updatedAt?: Date;
warehouseId?: number;
meta?: InventoryTransactionMeta;
/**
@@ -34,7 +33,7 @@ export class InventoryTransaction extends TenantBaseModel {
/**
* Model timestamps.
*/
get timestamps() {
static get timestamps() {
return ['createdAt', 'updatedAt'];
}

View File

@@ -1,40 +1,58 @@
import { JOB_REF, Processor } from '@nestjs/bullmq';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { JOB_REF, Processor, WorkerHost } from '@nestjs/bullmq';
import { Inject, Scope } from '@nestjs/common';
import { Job } from 'bullmq';
import { ClsService } from 'nestjs-cls';
import { TenantJobPayload } from '@/interfaces/Tenant';
import { InventoryComputeCostService } from '../commands/InventoryComputeCost.service';
import { events } from '@/common/events/events';
import { ComputeItemCostQueueJob } from '../types/InventoryCost.types';
interface ComputeItemCostJobPayload extends TenantJobPayload {
itemId: number;
startingDate: Date;
}
@Processor({
name: 'compute-item-cost',
name: ComputeItemCostQueueJob,
scope: Scope.REQUEST,
})
export class ComputeItemCostProcessor {
export class ComputeItemCostProcessor extends WorkerHost {
/**
* @param {InventoryComputeCostService} inventoryComputeCostService -
* @param {ClsService} clsService -
* @param {EventEmitter2} eventEmitter -
*/
constructor(
private readonly inventoryComputeCostService: InventoryComputeCostService,
private readonly clsService: ClsService,
@Inject(JOB_REF)
private readonly jobRef: Job<ComputeItemCostJobPayload>,
) {}
private readonly eventEmitter: EventEmitter2,
) {
super();
}
/**
* Handle compute item cost job.
* Process the compute item cost job.
* @param {Job<ComputeItemCostJobPayload>} job - The job to process
*/
async handleComputeItemCost() {
const { itemId, startingDate, organizationId, userId } = this.jobRef.data;
async process(job: Job<ComputeItemCostJobPayload>) {
const { itemId, startingDate, organizationId, userId } = job.data;
this.clsService.set('organizationId', organizationId);
this.clsService.set('userId', userId);
await this.inventoryComputeCostService.computeItemCost(
startingDate,
itemId,
);
try {
await this.inventoryComputeCostService.computeItemCost(
startingDate,
itemId,
);
// Emit job completed event
await this.eventEmitter.emitAsync(
events.inventory.onComputeItemCostJobCompleted,
{ startingDate, itemId, organizationId, userId },
);
} catch (error) {
console.error('Error computing item cost:', error);
throw error;
}
}
}

View File

@@ -0,0 +1,20 @@
import { Process } from '@nestjs/bull';
import {
WriteInventoryTransactionsGLEntriesQueue,
WriteInventoryTransactionsGLEntriesQueueJob,
} from '../types/InventoryCost.types';
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Scope } from '@nestjs/common';
@Processor({
name: WriteInventoryTransactionsGLEntriesQueue,
scope: Scope.REQUEST,
})
export class WriteInventoryTransactionsGLEntriesProcessor extends WorkerHost {
constructor() {
super();
}
@Process(WriteInventoryTransactionsGLEntriesQueueJob)
async process() {}
}

View File

@@ -0,0 +1,140 @@
import { map, head } from 'lodash';
import { OnEvent } from '@nestjs/event-emitter';
import {
IComputeItemCostJobCompletedPayload,
IInventoryTransactionsCreatedPayload,
IInventoryTransactionsDeletedPayload,
} from '../types/InventoryCost.types';
import { ImportAls } from '@/modules/Import/ImportALS';
import { InventoryItemsQuantitySyncService } from '../commands/InventoryItemsQuantitySync.service';
import { SaleInvoicesCost } from '@/modules/SaleInvoices/SalesInvoicesCost';
import { events } from '@/common/events/events';
import { runAfterTransaction } from '@/modules/Tenancy/TenancyDB/TransactionsHooks';
import { Injectable } from '@nestjs/common';
import { InventoryComputeCostService } from '../commands/InventoryComputeCost.service';
@Injectable()
export default class InventorySubscriber {
constructor(
private readonly saleInvoicesCost: SaleInvoicesCost,
private readonly itemsQuantitySync: InventoryItemsQuantitySyncService,
private readonly inventoryService: InventoryComputeCostService,
private readonly importAls: ImportAls,
) {}
/**
* Sync inventory items quantity once inventory transactions created.
* @param {IInventoryTransactionsCreatedPayload} payload -
*/
@OnEvent(events.inventory.onInventoryTransactionsCreated)
async syncItemsQuantityOnceInventoryTransactionsCreated({
inventoryTransactions,
trx,
}: IInventoryTransactionsCreatedPayload) {
const itemsQuantityChanges = this.itemsQuantitySync.getItemsQuantityChanges(
inventoryTransactions,
);
await this.itemsQuantitySync.changeItemsQuantity(itemsQuantityChanges, trx);
}
/**
* Handles schedule compute inventory items cost once inventory transactions created.
* @param {IInventoryTransactionsCreatedPayload} payload -
*/
@OnEvent(events.inventory.onInventoryTransactionsCreated)
async handleScheduleItemsCostOnInventoryTransactionsCreated({
inventoryTransactions,
trx,
}: IInventoryTransactionsCreatedPayload) {
const inImportPreviewScope = this.importAls.isImportPreview;
// Avoid running the cost items job if the async process is in import preview.
if (inImportPreviewScope) return;
await this.saleInvoicesCost.computeItemsCostByInventoryTransactions(
inventoryTransactions,
);
}
/**
* Marks items cost compute running state.
*/
@OnEvent(events.inventory.onInventoryTransactionsCreated)
async markGlobalSettingsComputeItems({}) {
await this.inventoryService.markItemsCostComputeRunning(true);
}
/**
* Marks items cost compute as completed.
*/
@OnEvent(events.inventory.onInventoryCostEntriesWritten)
async markGlobalSettingsComputeItemsCompeted({}) {
await this.inventoryService.markItemsCostComputeRunning(false);
}
/**
* Handle run writing the journal entries once the compute items jobs completed.
*/
@OnEvent(events.inventory.onComputeItemCostJobCompleted)
async onComputeItemCostJobFinished({
itemId,
startingDate,
}: IComputeItemCostJobCompletedPayload) {
// const dependsComputeJobs = await this.agenda.jobs({
// name: 'compute-item-cost',
// nextRunAt: { $ne: null },
// 'data.tenantId': tenantId,
// });
// // There is no scheduled compute jobs waiting.
// if (dependsComputeJobs.length === 0) {
// await this.saleInvoicesCost.scheduleWriteJournalEntries(startingDate);
// }
}
/**
* Sync inventory items quantity once inventory transactions deleted.
*/
@OnEvent(events.inventory.onInventoryTransactionsDeleted)
async syncItemsQuantityOnceInventoryTransactionsDeleted({
oldInventoryTransactions,
trx,
}: IInventoryTransactionsDeletedPayload) {
const itemsQuantityChanges =
this.itemsQuantitySync.getReverseItemsQuantityChanges(
oldInventoryTransactions,
);
await this.itemsQuantitySync.changeItemsQuantity(itemsQuantityChanges, trx);
}
/**
* Schedules compute items cost once the inventory transactions deleted.
*/
@OnEvent(events.inventory.onInventoryTransactionsDeleted)
async handleScheduleItemsCostOnInventoryTransactionsDeleted({
transactionType,
transactionId,
oldInventoryTransactions,
trx,
}: IInventoryTransactionsDeletedPayload) {
// Ignore compute item cost with theses transaction types.
const ignoreWithTransactionTypes = ['OpeningItem'];
if (ignoreWithTransactionTypes.indexOf(transactionType) !== -1) {
return;
}
const inventoryItemsIds = map(oldInventoryTransactions, 'itemId');
const startingDates = map(oldInventoryTransactions, 'date');
const startingDate: Date = head(startingDates);
runAfterTransaction(trx, async () => {
try {
await this.saleInvoicesCost.scheduleComputeCostByItemsIds(
inventoryItemsIds,
startingDate,
);
} catch (error) {
console.error(error);
}
});
}
}

View File

@@ -1,6 +1,13 @@
import { Knex } from "knex";
import { InventoryTransaction } from "../models/InventoryTransaction";
import { Knex } from 'knex';
import { InventoryTransaction } from '../models/InventoryTransaction';
export const ComputeItemCostQueue = 'ComputeItemCostQueue';
export const ComputeItemCostQueueJob = 'ComputeItemCostQueueJob';
export const WriteInventoryTransactionsGLEntriesQueue =
'WriteInventoryTransactionsGLEntriesQueue';
export const WriteInventoryTransactionsGLEntriesQueueJob =
'WriteInventoryTransactionsGLEntriesQueueJob';
export interface IInventoryItemCostMeta {
itemId: number;
@@ -10,8 +17,8 @@ export interface IInventoryItemCostMeta {
}
export interface IInventoryCostLotsGLEntriesWriteEvent {
startingDate: Date,
trx: Knex.Transaction
startingDate: Date;
trx: Knex.Transaction;
}
export type TInventoryTransactionDirection = 'IN' | 'OUT';

View File

@@ -1,7 +1,8 @@
import { Warehouse } from '@/modules/Warehouses/models/Warehouse.model';
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
import { Model } from 'objection';
export class Item extends TenantBaseModel{
export class Item extends TenantBaseModel {
public readonly quantityOnHand: number;
public readonly name: string;
public readonly active: boolean;
@@ -30,4 +31,164 @@ export class Item extends TenantBaseModel{
static get tableName() {
return 'items';
}
/**
* Relationship mapping.
*/
static get relationMappings() {
// const { Media } = require('../../Media/models/Media.model');
const { Account } = require('../../Accounts/models/Account.model');
const {
ItemCategory,
} = require('../../ItemCategories/models/ItemCategory.model');
const {
ItemWarehouseQuantity,
} = require('../../Warehouses/models/ItemWarehouseQuantity');
const {
ItemEntry,
} = require('../../TransactionItemEntry/models/ItemEntry');
// const WarehouseTransferEntry = require('../../Warehouses/');
const {
InventoryAdjustmentEntry,
} = require('../../InventoryAdjutments/models/InventoryAdjustment');
const { TaxRate } = require('../../TaxRates/models/TaxRate.model');
return {
/**
* Item may belongs to cateogory model.
*/
category: {
relation: Model.BelongsToOneRelation,
modelClass: ItemCategory,
join: {
from: 'items.categoryId',
to: 'items_categories.id',
},
},
/**
* Item may belongs to cost account.
*/
costAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account,
join: {
from: 'items.costAccountId',
to: 'accounts.id',
},
},
/**
* Item may belongs to sell account.
*/
sellAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account,
join: {
from: 'items.sellAccountId',
to: 'accounts.id',
},
},
/**
* Item may belongs to inventory account.
*/
inventoryAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account,
join: {
from: 'items.inventoryAccountId',
to: 'accounts.id',
},
},
/**
* Item has many warehouses quantities.
*/
itemWarehouses: {
relation: Model.HasManyRelation,
modelClass: ItemWarehouseQuantity,
join: {
from: 'items.id',
to: 'items_warehouses_quantity.itemId',
},
},
/**
* Item may has many item entries.
*/
itemEntries: {
relation: Model.HasManyRelation,
modelClass: ItemEntry,
join: {
from: 'items.id',
to: 'items_entries.itemId',
},
},
/**
* Item may has many warehouses transfers entries.
*/
// warehousesTransfersEntries: {
// relation: Model.HasManyRelation,
// modelClass: WarehouseTransferEntry,
// join: {
// from: 'items.id',
// to: 'warehouses_transfers_entries.itemId',
// },
// },
/**
* Item has many inventory adjustment entries.
*/
inventoryAdjustmentsEntries: {
relation: Model.HasManyRelation,
modelClass: InventoryAdjustmentEntry,
join: {
from: 'items.id',
to: 'inventory_adjustments_entries.itemId',
},
},
/**
*
*/
// media: {
// relation: Model.ManyToManyRelation,
// modelClass: Media.default,
// join: {
// from: 'items.id',
// through: {
// from: 'media_links.model_id',
// to: 'media_links.media_id',
// },
// to: 'media.id',
// },
// },
/**
* Item may has sell tax rate.
*/
sellTaxRate: {
relation: Model.BelongsToOneRelation,
modelClass: TaxRate,
join: {
from: 'items.sellTaxRateId',
to: 'tax_rates.id',
},
},
/**
* Item may has purchase tax rate.
*/
purchaseTaxRate: {
relation: Model.BelongsToOneRelation,
modelClass: TaxRate,
join: {
from: 'items.purchaseTaxRateId',
to: 'tax_rates.id',
},
},
};
}
}

View File

@@ -9,11 +9,12 @@ import { events } from '@/common/events/events';
import { ModelObject } from 'objection';
import { InventoryTransaction } from '../InventoryCost/models/InventoryTransaction';
import { IInventoryCostLotsGLEntriesWriteEvent } from '../InventoryCost/types/InventoryCost.types';
import { InventoryComputeCostService } from '../InventoryCost/commands/InventoryComputeCost.service';
@Injectable()
export class SaleInvoicesCost {
constructor(
private readonly inventoryService: InventoryService,
private readonly inventoryService: InventoryComputeCostService,
private readonly uow: UnitOfWork,
private readonly eventPublisher: EventEmitter2,
) {}
@@ -112,12 +113,12 @@ export class SaleInvoicesCost {
* @return {Promise<agenda>}
*/
scheduleWriteJournalEntries(startingDate?: Date) {
const agenda = Container.get('agenda');
// const agenda = Container.get('agenda');
return agenda.schedule('in 3 seconds', 'rewrite-invoices-journal-entries', {
startingDate,
tenantId,
});
// return agenda.schedule('in 3 seconds', 'rewrite-invoices-journal-entries', {
// startingDate,
// tenantId,
// });
}
/**

View File

@@ -19,6 +19,14 @@ import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class DeleteSaleInvoice {
/**
* @param {UnlinkConvertedSaleEstimate} unlockEstimateFromInvoice - Unlink converted sale estimate service.
* @param {EventEmitter2} eventPublisher - Event emitter.
* @param {UnitOfWork} uow - Unit of work.
* @param {TenantModelProxy<typeof PaymentReceivedEntry>} paymentReceivedEntryModel - Payment received entry model.
* @param {TenantModelProxy<typeof CreditNoteAppliedInvoice>} creditNoteAppliedInvoiceModel - Credit note applied invoice model.
* @param {TenantModelProxy<typeof SaleInvoice>} saleInvoiceModel - Sale invoice model.
*/
constructor(
private unlockEstimateFromInvoice: UnlinkConvertedSaleEstimate,
private eventPublisher: EventEmitter2,
@@ -36,6 +44,9 @@ export class DeleteSaleInvoice {
@Inject(SaleInvoice.name)
private saleInvoiceModel: TenantModelProxy<typeof SaleInvoice>,
@Inject(ItemEntry.name)
private itemEntryModel: TenantModelProxy<typeof ItemEntry>,
) {}
/**
@@ -113,12 +124,13 @@ export class DeleteSaleInvoice {
saleInvoiceId,
trx,
);
await ItemEntry.query(trx)
await this.itemEntryModel()
.query(trx)
.where('reference_id', saleInvoiceId)
.where('reference_type', 'SaleInvoice')
.delete();
await SaleInvoice.query(trx).findById(saleInvoiceId).delete();
await this.saleInvoiceModel().query(trx).findById(saleInvoiceId).delete();
// Triggers `onSaleInvoiceDeleted` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onDeleted, {

View File

@@ -17,6 +17,15 @@ import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class EditSaleInvoice {
/**
* @param {ItemsEntriesService} itemsEntriesService - Items entries service.
* @param {EventEmitter2} eventPublisher - Event emitter.
* @param {CommandSaleInvoiceValidators} validators - Command sale invoice validators.
* @param {CommandSaleInvoiceDTOTransformer} transformerDTO - Command sale invoice DTO transformer.
* @param {UnitOfWork} uow - Unit of work.
* @param {TenantModelProxy<typeof SaleInvoice>} saleInvoiceModel - Sale invoice model.
* @param {TenantModelProxy<typeof Customer>} customerModel - Customer model.
*/
constructor(
private readonly itemsEntriesService: ItemsEntriesService,
private readonly eventPublisher: EventEmitter2,

View File

@@ -13,9 +13,7 @@ import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
export class SubscriptionGuard implements CanActivate {
constructor(
@Inject(PlanSubscription.name)
private readonly planSubscriptionModel: TenantModelProxy<
typeof PlanSubscription
>,
private readonly planSubscriptionModel: typeof PlanSubscription,
private readonly tenancyContext: TenancyContext,
) {}
@@ -30,7 +28,7 @@ export class SubscriptionGuard implements CanActivate {
subscriptionSlug: string = 'main', // Default value
): Promise<boolean> {
const tenant = await this.tenancyContext.getTenant();
const subscription = await this.planSubscriptionModel()
const subscription = await this.planSubscriptionModel
.query()
.findOne('slug', subscriptionSlug)
.where('tenant_id', tenant.id);

View File

@@ -17,6 +17,8 @@ import { WriteInvoiceTaxTransactionsSubscriber } from './subscribers/WriteInvoic
import { BillTaxRateValidateSubscriber } from './subscribers/BillTaxRateValidateSubscriber';
import { SaleInvoiceTaxRateValidateSubscriber } from './subscribers/SaleInvoiceTaxRateValidateSubscriber';
import { SyncItemTaxRateOnEditTaxSubscriber } from './subscribers/SyncItemTaxRateOnEditTaxSubscriber';
import { WriteTaxTransactionsItemEntries } from './WriteTaxTransactionsItemEntries';
import { SyncItemTaxRateOnEditTaxRate } from './SyncItemTaxRateOnEditTaxRate';
@Module({
imports: [],
@@ -39,6 +41,8 @@ import { SyncItemTaxRateOnEditTaxSubscriber } from './subscribers/SyncItemTaxRat
BillTaxRateValidateSubscriber,
SaleInvoiceTaxRateValidateSubscriber,
SyncItemTaxRateOnEditTaxSubscriber,
WriteTaxTransactionsItemEntries,
SyncItemTaxRateOnEditTaxRate
],
exports: [ItemEntriesTaxTransactions],
})

View File

@@ -6,6 +6,7 @@ import { TaxRateModel } from './models/TaxRate.model';
import { Inject, Injectable } from '@nestjs/common';
import { ModelObject } from 'objection';
import { ItemEntry } from '../TransactionItemEntry/models/ItemEntry';
import { TaxRateTransaction } from './models/TaxRateTransaction.model';
@Injectable()
export class WriteTaxTransactionsItemEntries {
@@ -40,11 +41,11 @@ export class WriteTaxTransactionsItemEntries {
taxRateId: entry.taxRateId,
referenceType: entry.referenceType,
referenceId: entry.referenceId,
rate: entry.taxRate || taxRatesById[entry.taxRateId]?.rate,
rate: entry.taxRate || (taxRatesById[entry.taxRateId]?.rate as number),
}));
await this.taxRateTransactionModel()
.query(trx)
.upsertGraph(taxTransactions);
.upsertGraph(taxTransactions as ModelObject<TaxRateTransaction>[]);
}
/**

View File

@@ -4,12 +4,12 @@ import { mixin, Model, raw } from 'objection';
import { BaseModel } from '@/models/Model';
export class TaxRateTransaction extends BaseModel {
id: number;
taxRateId: number;
referenceType: string;
referenceId: number;
rate: number;
taxAccountId: number;
public id: number;
public taxRateId: number;
public referenceType: string;
public referenceId: string;
public rate: number;
public taxAccountId?: number;
/**
* Table name

View File

@@ -8,7 +8,7 @@ import { TENANCY_DB_CONNECTION } from '@/modules/Tenancy/TenancyDB/TenancyDB.con
export class UnitOfWork {
constructor(
@Inject(TENANCY_DB_CONNECTION)
private readonly tenantKex: Knex,
private readonly tenantKex: () => Knex,
) {}
/**
@@ -23,7 +23,7 @@ export class UnitOfWork {
trx?: Transaction,
isolationLevel: IsolationLevel = IsolationLevel.READ_UNCOMMITTED,
): Promise<T> => {
const knex = this.tenantKex;
const knex = this.tenantKex();
let _trx = trx;
if (!_trx) {

View File

@@ -1,23 +1,12 @@
import { Knex } from 'knex';
import { Warehouse } from './models/Warehouse.model';
import { WarehouseTransfer } from '../WarehousesTransfers/models/WarehouseTransfer';
import { ModelObject } from 'objection';
export interface IWarehouse {
id?: number;
}
export interface IWarehouseTransfer {
id?: number;
date: Date;
fromWarehouseId: number;
toWarehouseId: number;
reason?: string;
transactionNumber: string;
entries: IWarehouseTransferEntry[];
transferInitiatedAt?: Date;
transferDeliveredAt?: Date;
isInitiated?: boolean;
isTransferred?: boolean;
}
export interface IWarehouseTransferEntry {
id?: number;
index?: number;
@@ -118,37 +107,31 @@ export interface IWarehouseTransferCreate {
}
export interface IWarehouseTransferCreated {
trx: Knex.Transaction;
warehouseTransfer: IWarehouseTransfer;
trx?: Knex.Transaction;
warehouseTransfer: ModelObject<WarehouseTransfer>;
warehouseTransferDTO: ICreateWarehouseTransferDTO;
// tenantId: number;
}
export interface IWarehouseTransferEditPayload {
// tenantId: number;
editWarehouseDTO: IEditWarehouseTransferDTO;
oldWarehouseTransfer: IWarehouseTransfer;
oldWarehouseTransfer: ModelObject<WarehouseTransfer>;
trx: Knex.Transaction;
}
export interface IWarehouseTransferEditedPayload {
// tenantId: number;
editWarehouseDTO: IEditWarehouseTransferDTO;
oldWarehouseTransfer: IWarehouseTransfer;
warehouseTransfer: IWarehouseTransfer;
oldWarehouseTransfer: ModelObject<WarehouseTransfer>;
warehouseTransfer: ModelObject<WarehouseTransfer>;
trx: Knex.Transaction;
}
export interface IWarehouseTransferDeletePayload {
// tenantId: number;
oldWarehouseTransfer: IWarehouseTransfer;
oldWarehouseTransfer: ModelObject<WarehouseTransfer>;
trx: Knex.Transaction;
}
export interface IWarehouseTransferDeletedPayload {
// tenantId: number;
warehouseTransfer: IWarehouseTransfer;
oldWarehouseTransfer: IWarehouseTransfer;
oldWarehouseTransfer: ModelObject<WarehouseTransfer>;
trx: Knex.Transaction;
}
@@ -173,40 +156,33 @@ export interface IWarehousesActivatedPayload {
}
export interface IWarehouseMarkAsPrimaryPayload {
// tenantId: number;
oldWarehouse: Warehouse;
trx: Knex.Transaction;
}
export interface IWarehouseMarkedAsPrimaryPayload {
// tenantId: number;
oldWarehouse: Warehouse;
markedWarehouse: Warehouse;
trx: Knex.Transaction;
}
export interface IWarehouseTransferInitiatePayload {
// tenantId: number;
oldWarehouseTransfer: IWarehouseTransfer;
oldWarehouseTransfer: ModelObject<WarehouseTransfer>;
trx: Knex.Transaction;
}
export interface IWarehouseTransferInitiatedPayload {
// tenantId: number;
warehouseTransfer: IWarehouseTransfer;
oldWarehouseTransfer: IWarehouseTransfer;
warehouseTransfer: ModelObject<WarehouseTransfer>;
oldWarehouseTransfer: ModelObject<WarehouseTransfer>;
trx: Knex.Transaction;
}
export interface IWarehouseTransferTransferingPayload {
// tenantId: number;
oldWarehouseTransfer: IWarehouseTransfer;
oldWarehouseTransfer: ModelObject<WarehouseTransfer>;
trx: Knex.Transaction;
}
export interface IWarehouseTransferTransferredPayload {
// tenantId: number;
warehouseTransfer: IWarehouseTransfer;
oldWarehouseTransfer: IWarehouseTransfer;
warehouseTransfer: ModelObject<WarehouseTransfer>;
oldWarehouseTransfer: ModelObject<WarehouseTransfer>;
trx: Knex.Transaction;
}

View File

@@ -0,0 +1,121 @@
import {
ICreateWarehouseTransferDTO,
IEditWarehouseTransferDTO,
IGetWarehousesTransfersFilterDTO,
} from '@/modules/Warehouses/Warehouse.types';
import { CreateWarehouseTransfer } from './commands/CreateWarehouseTransfer';
import { DeleteWarehouseTransfer } from './commands/DeleteWarehouseTransfer';
import { EditWarehouseTransfer } from './commands/EditWarehouseTransfer';
import { GetWarehouseTransfer } from './queries/GetWarehouseTransfer';
import { GetWarehouseTransfers } from './queries/GetWarehouseTransfers';
import { InitiateWarehouseTransfer } from './commands/InitiateWarehouseTransfer';
import { TransferredWarehouseTransfer } from './commands/TransferredWarehouseTransfer';
import { Injectable } from '@nestjs/common';
import { WarehouseTransfer } from './models/WarehouseTransfer';
import { ModelObject } from 'objection';
@Injectable()
export class WarehouseTransferApplication {
constructor(
private readonly createWarehouseTransferService: CreateWarehouseTransfer,
private readonly editWarehouseTransferService: EditWarehouseTransfer,
private readonly deleteWarehouseTransferService: DeleteWarehouseTransfer,
private readonly getWarehouseTransferService: GetWarehouseTransfer,
private readonly getWarehousesTransfersService: GetWarehouseTransfers,
private readonly initiateWarehouseTransferService: InitiateWarehouseTransfer,
private readonly transferredWarehouseTransferService: TransferredWarehouseTransfer,
) {}
/**
* Creates a warehouse transfer transaction.
* @param {number} tenantId
* @param {ICreateWarehouseTransferDTO} createWarehouseTransferDTO
* @returns {}
*/
public createWarehouseTransfer = (
createWarehouseTransferDTO: ICreateWarehouseTransferDTO,
): Promise<ModelObject<WarehouseTransfer>> => {
return this.createWarehouseTransferService.createWarehouseTransfer(
createWarehouseTransferDTO,
);
};
/**
* Edits warehouse transfer transaction.
* @param {number} tenantId -
* @param {number} warehouseTransferId - number
* @param {IEditWarehouseTransferDTO} editWarehouseTransferDTO
*/
public editWarehouseTransfer = (
warehouseTransferId: number,
editWarehouseTransferDTO: IEditWarehouseTransferDTO,
): Promise<ModelObject<WarehouseTransfer>> => {
return this.editWarehouseTransferService.editWarehouseTransfer(
warehouseTransferId,
editWarehouseTransferDTO,
);
};
/**
* Deletes warehouse transfer transaction.
* @param {number} warehouseTransferId
* @returns {Promise<void>}
*/
public deleteWarehouseTransfer = (
warehouseTransferId: number,
): Promise<void> => {
return this.deleteWarehouseTransferService.deleteWarehouseTransfer(
warehouseTransferId,
);
};
/**
* Retrieves warehouse transfer transaction.
* @param {number} warehouseTransferId
* @returns {Promise<IWarehouseTransfer>}
*/
public getWarehouseTransfer = (
warehouseTransferId: number,
): Promise<ModelObject<WarehouseTransfer>> => {
return this.getWarehouseTransferService.getWarehouseTransfer(
warehouseTransferId,
);
};
/**
* Retrieves warehouses trans
* @param {IGetWarehousesTransfersFilterDTO} filterDTO
* @returns {Promise<IWarehouseTransfer>}
*/
public getWarehousesTransfers = (
filterDTO: IGetWarehousesTransfersFilterDTO,
) => {
return this.getWarehousesTransfersService.getWarehouseTransfers(filterDTO);
};
/**
* Marks the warehouse transfer order as transfered.
* @param {number} warehouseTransferId
* @returns {Promise<IWarehouseTransfer>}
*/
public transferredWarehouseTransfer = (
warehouseTransferId: number,
): Promise<ModelObject<WarehouseTransfer>> => {
return this.transferredWarehouseTransferService.transferredWarehouseTransfer(
warehouseTransferId,
);
};
/**
* Marks the warehouse transfer order as initiated.
* @param {number} warehouseTransferId
* @returns {Promise<IWarehouseTransfer>}
*/
public initiateWarehouseTransfer = (
warehouseTransferId: number,
): Promise<ModelObject<WarehouseTransfer>> => {
return this.initiateWarehouseTransferService.initiateWarehouseTransfer(
warehouseTransferId,
);
};
}

View File

@@ -0,0 +1,181 @@
import {
Controller,
Post,
Put,
Get,
Delete,
Body,
Param,
Query,
Inject,
} from '@nestjs/common';
import { WarehouseTransferApplication } from './WarehouseTransferApplication';
import {
ICreateWarehouseTransferDTO,
IEditWarehouseTransferDTO,
} from '@/modules/Warehouses/Warehouse.types';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { PublicRoute } from '../Auth/Jwt.guard';
@Controller('warehouse-transfers')
@ApiTags('warehouse-transfers')
@PublicRoute()
export class WarehouseTransfersController {
/**
* @param {WarehouseTransferApplication} warehouseTransferApplication - Warehouse transfer application.
*/
constructor(
@Inject(WarehouseTransferApplication)
private readonly warehouseTransferApplication: WarehouseTransferApplication,
) {}
/**
* Creates a new warehouse transfer transaction.
*/
@Post()
@ApiOperation({ summary: 'Create a new warehouse transfer transaction.' })
@ApiResponse({
status: 200,
description:
'The warehouse transfer transaction has been created successfully.',
})
async createWarehouseTransfer(
@Body() createWarehouseTransferDTO: ICreateWarehouseTransferDTO,
) {
const warehouse =
await this.warehouseTransferApplication.createWarehouseTransfer(
createWarehouseTransferDTO,
);
return {
id: warehouse.id,
message:
'The warehouse transfer transaction has been created successfully.',
};
}
/**
* Edits warehouse transfer transaction.
*/
@Post(':id')
@ApiOperation({ summary: 'Edit the given warehouse transfer transaction.' })
@ApiResponse({
status: 200,
description:
'The warehouse transfer transaction has been edited successfully.',
})
async editWarehouseTransfer(
@Param('id') id: number,
@Body() editWarehouseTransferDTO: IEditWarehouseTransferDTO,
) {
const warehouseTransfer =
await this.warehouseTransferApplication.editWarehouseTransfer(
id,
editWarehouseTransferDTO,
);
return {
id: warehouseTransfer.id,
message:
'The warehouse transfer transaction has been edited successfully.',
};
}
/**
* Initiates the warehouse transfer.
*/
@Put(':id/initiate')
@ApiOperation({ summary: 'Initiate the given warehouse transfer.' })
@ApiResponse({
status: 200,
description: 'The warehouse transfer has been initiated successfully.',
})
async initiateTransfer(@Param('id') id: number) {
await this.warehouseTransferApplication.initiateWarehouseTransfer(id);
return {
id,
message: 'The given warehouse transfer has been initialized.',
};
}
/**
* Marks the given warehouse transfer as transferred.
*/
@Put(':id/transferred')
@ApiOperation({
summary: 'Mark the given warehouse transfer as transferred.',
})
@ApiResponse({
status: 200,
description:
'The warehouse transfer has been marked as transferred successfully.',
})
async deliverTransfer(@Param('id') id: number) {
await this.warehouseTransferApplication.transferredWarehouseTransfer(id);
return {
id,
message: 'The given warehouse transfer has been delivered.',
};
}
/**
* Retrieves warehouse transfer transactions with pagination.
*/
@Get()
@ApiOperation({
summary: 'Retrieve warehouse transfer transactions with pagination.',
})
@ApiResponse({
status: 200,
description:
'The warehouse transfer transactions have been retrieved successfully.',
})
async getWarehousesTransfers(@Query() query: any) {
const { warehousesTransfers, pagination, filter } =
await this.warehouseTransferApplication.getWarehousesTransfers(query);
return {
data: warehousesTransfers,
pagination,
filter,
};
}
/**
* Retrieves warehouse transfer transaction details.
*/
@Get(':id')
@ApiOperation({ summary: 'Retrieve warehouse transfer transaction details.' })
@ApiResponse({
status: 200,
description:
'The warehouse transfer transaction details have been retrieved successfully.',
})
async getWarehouseTransfer(@Param('id') id: number) {
const warehouseTransfer =
await this.warehouseTransferApplication.getWarehouseTransfer(id);
return { data: warehouseTransfer };
}
/**
* Deletes the given warehouse transfer transaction.
*/
@Delete(':id')
@ApiOperation({ summary: 'Delete the given warehouse transfer transaction.' })
@ApiResponse({
status: 200,
description:
'The warehouse transfer transaction has been deleted successfully.',
})
async deleteWarehouseTransfer(@Param('id') id: number) {
await this.warehouseTransferApplication.deleteWarehouseTransfer(id);
return {
message:
'The warehouse transfer transaction has been deleted successfully.',
};
}
}

View File

@@ -0,0 +1,55 @@
import { Module } from '@nestjs/common';
import { CreateWarehouseTransfer } from './commands/CreateWarehouseTransfer';
import { EditWarehouseTransfer } from './commands/EditWarehouseTransfer';
import { DeleteWarehouseTransfer } from './commands/DeleteWarehouseTransfer';
import { GetWarehouseTransfer } from './queries/GetWarehouseTransfer';
import { GetWarehouseTransfers } from './queries/GetWarehouseTransfers';
import { WarehouseTransferApplication } from './WarehouseTransferApplication';
import { WarehouseTransfersController } from './WarehouseTransfers.controller';
import { WarehouseTransferInventoryTransactions } from './commands/WarehouseTransferWriteInventoryTransactions';
import { WarehouseTransferAutoIncrement } from './commands/WarehouseTransferAutoIncrement';
import { WarehouseTransferAutoIncrementSubscriber } from './susbcribers/WarehouseTransferAutoIncrementSubscriber';
import { WarehouseTransferInventoryTransactionsSubscriber } from './susbcribers/WarehouseTransferInventoryTransactionsSubscriber';
import { InitiateWarehouseTransfer } from './commands/InitiateWarehouseTransfer';
import { TransferredWarehouseTransfer } from './commands/TransferredWarehouseTransfer';
import { CommandWarehouseTransfer } from './commands/CommandWarehouseTransfer';
import { ItemsModule } from '../Items/items.module';
import { InventoryCostModule } from '../InventoryCost/InventoryCost.module';
import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module';
import { WarehouseTransfer } from './models/WarehouseTransfer';
import { WarehouseTransferEntry } from './models/WarehouseTransferEntry';
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
import { AutoIncrementOrdersModule } from '../AutoIncrementOrders/AutoIncrementOrders.module';
const models = [
RegisterTenancyModel(WarehouseTransfer),
RegisterTenancyModel(WarehouseTransferEntry),
];
@Module({
imports: [
ItemsModule,
InventoryCostModule,
DynamicListModule,
AutoIncrementOrdersModule,
...models,
],
providers: [
WarehouseTransferApplication,
CreateWarehouseTransfer,
EditWarehouseTransfer,
DeleteWarehouseTransfer,
GetWarehouseTransfer,
GetWarehouseTransfers,
WarehouseTransferInventoryTransactions,
WarehouseTransferAutoIncrement,
WarehouseTransferAutoIncrementSubscriber,
WarehouseTransferInventoryTransactionsSubscriber,
TransferredWarehouseTransfer,
InitiateWarehouseTransfer,
CommandWarehouseTransfer,
],
exports: [...models],
controllers: [WarehouseTransfersController],
})
export class WarehousesTransfersModule {}

View File

@@ -0,0 +1,118 @@
import { ERRORS } from '../constants';
import { WarehouseTransfer } from '../models/WarehouseTransfer';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
import { Inject, Injectable } from '@nestjs/common';
import { ServiceError } from '@/modules/Items/ServiceError';
import { ModelObject } from 'objection';
import { Item } from '@/modules/Items/models/Item';
import {
ICreateWarehouseTransferDTO,
IEditWarehouseTransferDTO,
} from '@/modules/Warehouses/Warehouse.types';
import { Warehouse } from '@/modules/Warehouses/models/Warehouse.model';
@Injectable()
export class CommandWarehouseTransfer {
constructor(
@Inject(Warehouse.name)
private readonly warehouseModel: TenantModelProxy<typeof Warehouse>,
@Inject(WarehouseTransfer.name)
private readonly warehouseTransferModel: TenantModelProxy<
typeof WarehouseTransfer
>,
) {}
/**
*
* @param {WarehouseTransfer} warehouseTransfer
*/
throwIfTransferNotFound = (warehouseTransfer: WarehouseTransfer) => {
if (!warehouseTransfer) {
throw new ServiceError(ERRORS.WAREHOUSE_TRANSFER_NOT_FOUND);
}
};
/**
*
* @param {number} branchId
* @returns
*/
async getWarehouseTransferOrThrowNotFound(branchId: number) {
const foundTransfer = await this.warehouseTransferModel()
.query()
.findById(branchId);
if (!foundTransfer) {
throw new ServiceError(ERRORS.WAREHOUSE_TRANSFER_NOT_FOUND);
}
return foundTransfer;
}
/**
* Validate the from/to warehouses should not be the same.
* @param {ICreateWarehouseTransferDTO|IEditWarehouseTransferDTO} warehouseTransferDTO
*/
validateWarehouseFromToNotSame(
warehouseTransferDTO:
| ICreateWarehouseTransferDTO
| IEditWarehouseTransferDTO,
) {
if (
warehouseTransferDTO.fromWarehouseId ===
warehouseTransferDTO.toWarehouseId
) {
throw new ServiceError(ERRORS.WAREHOUSES_TRANSFER_SHOULD_NOT_BE_SAME);
}
}
/**
* Validates entries items should be inventory.
* @param {IItem[]} items
* @returns {void}
*/
validateItemsShouldBeInventory = (
items: ModelObject<Item>[],
): void => {
const nonInventoryItems = items.filter((item) => item.type !== 'inventory');
if (nonInventoryItems.length > 0) {
throw new ServiceError(
ERRORS.WAREHOUSE_TRANSFER_ITEMS_SHOULD_BE_INVENTORY,
);
}
};
/**
*
* @param {number} fromWarehouseId
* @returns
*/
getToWarehouseOrThrow = async (fromWarehouseId: number) => {
const warehouse = await this.warehouseModel()
.query()
.findById(fromWarehouseId);
if (!warehouse) {
throw new ServiceError(ERRORS.TO_WAREHOUSE_NOT_FOUND);
}
return warehouse;
};
/**
*
* @param {number} fromWarehouseId
* @returns
*/
getFromWarehouseOrThrow = async (fromWarehouseId: number) => {
const warehouse = await this.warehouseModel()
.query()
.findById(fromWarehouseId);
if (!warehouse) {
throw new ServiceError(ERRORS.FROM_WAREHOUSE_NOT_FOUND);
}
return warehouse;
};
}

View File

@@ -0,0 +1,198 @@
import { Knex } from 'knex';
import { omit, get, isNumber } from 'lodash';
import * as R from 'ramda';
import {
ICreateWarehouseTransferDTO,
IWarehouseTransferCreate,
IWarehouseTransferCreated,
IWarehouseTransferEntryDTO,
} from '@/modules/Warehouses/Warehouse.types';
import { CommandWarehouseTransfer } from './CommandWarehouseTransfer';
import { WarehouseTransferAutoIncrement } from './WarehouseTransferAutoIncrement';
import { WarehouseTransfer } from '../models/WarehouseTransfer';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
import { InventoryItemCostService } from '@/modules/InventoryCost/commands/InventoryCosts.service';
import { Inject, Injectable } from '@nestjs/common';
import { events } from '@/common/events/events';
import { IInventoryItemCostMeta } from '@/modules/InventoryCost/types/InventoryCost.types';
import { ModelObject } from 'objection';
import { WarehouseTransferEntry } from '../models/WarehouseTransferEntry';
@Injectable()
export class CreateWarehouseTransfer {
/**
* @param {UnitOfWork} uow - Unit of work.
* @param {EventEmitter2} eventPublisher - Event publisher.
* @param {ItemsEntriesService} itemsEntries - Items entries service.
* @param {InventoryItemCostService} inventoryItemCost - Inventory item cost service.
* @param {WarehouseTransferAutoIncrement} autoIncrementOrders - Warehouse transfer auto increment.
* @param {CommandWarehouseTransfer} commandWarehouseTransfer - Command warehouse transfer.
* @param {TenantModelProxy<typeof WarehouseTransfer>} warehouseTransferModel - Warehouse transfer model.
*/
constructor(
private readonly uow: UnitOfWork,
private readonly eventPublisher: EventEmitter2,
private readonly itemsEntries: ItemsEntriesService,
private readonly inventoryItemCost: InventoryItemCostService,
private readonly autoIncrementOrders: WarehouseTransferAutoIncrement,
private readonly commandWarehouseTransfer: CommandWarehouseTransfer,
@Inject(WarehouseTransfer.name)
private readonly warehouseTransferModel: TenantModelProxy<
typeof WarehouseTransfer
>,
) {}
/**
* Transformes the givne new warehouse transfer DTO to model.
* @param {ICreateWarehouseTransferDTO} warehouseTransferDTO
* @returns {IWarehouseTransfer}
*/
private transformDTOToModel = async (
warehouseTransferDTO: ICreateWarehouseTransferDTO,
): Promise<ModelObject<WarehouseTransfer>> => {
const entries = await this.transformEntries(
warehouseTransferDTO,
warehouseTransferDTO.entries,
);
// Retrieves the auto-increment the warehouse transfer number.
const autoNextNumber = this.autoIncrementOrders.getNextTransferNumber();
// Warehouse transfer order transaction number.
const transactionNumber =
warehouseTransferDTO.transactionNumber || autoNextNumber;
return {
...omit(warehouseTransferDTO, ['transferDelivered', 'transferInitiated']),
transactionNumber,
...(warehouseTransferDTO.transferDelivered
? {
transferDeliveredAt: new Date(),
}
: {}),
...(warehouseTransferDTO.transferDelivered ||
warehouseTransferDTO.transferInitiated
? {
transferInitiatedAt: new Date(),
}
: {}),
entries,
};
};
/**
* Assoc average cost to the entry that has no cost.
* @param {Promise<Map<number, IInventoryItemCostMeta>} inventoryItemsCostMap -
* @param {IWarehouseTransferEntryDTO} entry -
*/
private transformEntryAssocAverageCost = R.curry(
(
inventoryItemsCostMap: Map<number, IInventoryItemCostMeta>,
entry: IWarehouseTransferEntryDTO,
): IWarehouseTransferEntryDTO => {
const itemValuation = inventoryItemsCostMap.get(entry.itemId);
const itemCost = get(itemValuation, 'average', 0);
return isNumber(entry.cost) ? entry : R.assoc('cost', itemCost, entry);
},
);
/**
* Transformes warehouse transfer entries.
* @param {ICreateWarehouseTransferDTO} warehouseTransferDTO
* @param {IWarehouseTransferEntryDTO[]} entries
* @returns {Promise<IWarehouseTransferEntryDTO[]>}
*/
public transformEntries = async (
warehouseTransferDTO: ICreateWarehouseTransferDTO,
entries: IWarehouseTransferEntryDTO[],
): Promise<ModelObject<WarehouseTransferEntry>[]> => {
const inventoryItemsIds = warehouseTransferDTO.entries.map((e) => e.itemId);
// Retrieves the inventory items valuation map.
const inventoryItemsCostMap =
await this.inventoryItemCost.getItemsInventoryValuation(
inventoryItemsIds,
warehouseTransferDTO.date,
);
// Assoc average cost to the entry.
const assocAverageCost = this.transformEntryAssocAverageCost(
inventoryItemsCostMap,
);
return entries.map((entry) => assocAverageCost(entry));
};
/**
* Authorize warehouse transfer before creating.
* @param {number} tenantId
* @param {ICreateWarehouseTransferDTO} warehouseTransferDTO
*/
public authorize = async (
warehouseTransferDTO: ICreateWarehouseTransferDTO,
) => {
// Validate warehouse from and to should not be the same.
this.commandWarehouseTransfer.validateWarehouseFromToNotSame(
warehouseTransferDTO,
);
// Retrieves the from warehouse or throw not found service error.
const fromWarehouse =
await this.commandWarehouseTransfer.getFromWarehouseOrThrow(
warehouseTransferDTO.fromWarehouseId,
);
// Retrieves the to warehouse or throw not found service error.
const toWarehouse =
await this.commandWarehouseTransfer.getToWarehouseOrThrow(
warehouseTransferDTO.toWarehouseId,
);
// Validates the not found entries items ids.
const items = await this.itemsEntries.validateItemsIdsExistance(
warehouseTransferDTO.entries,
);
// Validate the items entries should be inventory type.
this.commandWarehouseTransfer.validateItemsShouldBeInventory(items);
};
/**
* Creates a new warehouse transfer transaction.
* @param {ICreateWarehouseTransferDTO} warehouseDTO -
* @returns {Promise<ModelObject<WarehouseTransfer>>}
*/
public createWarehouseTransfer = async (
warehouseTransferDTO: ICreateWarehouseTransferDTO,
): Promise<ModelObject<WarehouseTransfer>> => {
// Authorize warehouse transfer before creating.
await this.authorize(warehouseTransferDTO);
// Transformes the warehouse transfer DTO to model.
const warehouseTransferModel =
await this.transformDTOToModel(warehouseTransferDTO);
// Create warehouse transfer under unit-of-work.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onWarehouseTransferCreate` event.
await this.eventPublisher.emitAsync(events.warehouseTransfer.onCreate, {
trx,
warehouseTransferDTO,
} as IWarehouseTransferCreate);
// Stores the warehouse transfer transaction graph to the storage.
const warehouseTransfer = await this.warehouseTransferModel()
.query(trx)
.upsertGraphAndFetch({
...warehouseTransferModel,
});
// Triggers `onWarehouseTransferCreated` event.
await this.eventPublisher.emitAsync(events.warehouseTransfer.onCreated, {
trx,
warehouseTransfer,
warehouseTransferDTO,
} as IWarehouseTransferCreated);
return warehouseTransfer;
});
};
}

View File

@@ -0,0 +1,77 @@
import { Knex } from 'knex';
import {
IWarehouseTransferDeletedPayload,
IWarehouseTransferDeletePayload,
} from '@/modules/Warehouses/Warehouse.types';
import { Inject, Injectable } from '@nestjs/common';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { events } from '@/common/events/events';
import { WarehouseTransfer } from '../models/WarehouseTransfer';
import { WarehouseTransferEntry } from '../models/WarehouseTransferEntry';
@Injectable()
export class DeleteWarehouseTransfer {
/**
* @param {UnitOfWork} uow - Unit of work service.
* @param {EventEmitter2} eventPublisher - Event emitter service.
* @param {TenantModelProxy<WarehouseTransfer>} warehouseTransferModel - Warehouse transfer model.
* @param {TenantModelProxy<WarehouseTransferEntry>} warehouseTransferEntryModel - Warehouse transfer entry model.
*/
constructor(
private readonly uow: UnitOfWork,
private readonly eventPublisher: EventEmitter2,
@Inject(WarehouseTransfer.name)
private readonly warehouseTransferModel: TenantModelProxy<
typeof WarehouseTransfer
>,
@Inject(WarehouseTransferEntry.name)
private readonly warehouseTransferEntryModel: TenantModelProxy<
typeof WarehouseTransferEntry
>,
) {}
/**
* Deletes warehouse transfer transaction.
* @param {number} warehouseTransferId
* @returns {Promise<void>}
*/
public deleteWarehouseTransfer = async (
warehouseTransferId: number,
): Promise<void> => {
// Retrieve the old warehouse transfer or throw not found service error.
const oldWarehouseTransfer = await this.warehouseTransferModel()
.query()
.findById(warehouseTransferId)
.throwIfNotFound();
// Deletes the warehouse transfer under unit-of-work envirement.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onWarehouseTransferCreate` event.
await this.eventPublisher.emitAsync(events.warehouseTransfer.onDelete, {
oldWarehouseTransfer,
trx,
} as IWarehouseTransferDeletePayload);
// Delete warehouse transfer entries.
await this.warehouseTransferEntryModel()
.query(trx)
.where('warehouseTransferId', warehouseTransferId)
.delete();
// Delete warehouse transfer.
await this.warehouseTransferModel()
.query(trx)
.findById(warehouseTransferId)
.delete();
// Triggers `onWarehouseTransferDeleted` event
await this.eventPublisher.emitAsync(events.warehouseTransfer.onDeleted, {
oldWarehouseTransfer,
trx,
} as IWarehouseTransferDeletedPayload);
});
};
}

View File

@@ -0,0 +1,95 @@
import { Knex } from 'knex';
import {
IEditWarehouseTransferDTO,
IWarehouseTransferEditPayload,
IWarehouseTransferEditedPayload,
} from '@/modules/Warehouses/Warehouse.types';
import { CommandWarehouseTransfer } from './CommandWarehouseTransfer';
import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '../../System/models/TenantBaseModel';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { WarehouseTransfer } from '../models/WarehouseTransfer';
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
import { ModelObject } from 'objection';
@Injectable()
export class EditWarehouseTransfer {
constructor(
private readonly uow: UnitOfWork,
private readonly eventPublisher: EventEmitter2,
private readonly commandWarehouseTransfer: CommandWarehouseTransfer,
private readonly itemsEntries: ItemsEntriesService,
@Inject(WarehouseTransfer.name)
private readonly warehouseTransferModel: TenantModelProxy<
typeof WarehouseTransfer
>,
) {}
/**
* Edits warehouse transfer.
* @param {number} warehouseTransferId - Warehouse transfer id.
* @param {IEditWarehouseTransferDTO} editWarehouseDTO -
* @returns {Promise<ModelObject<WarehouseTransfer>>}
*/
public editWarehouseTransfer = async (
warehouseTransferId: number,
editWarehouseDTO: IEditWarehouseTransferDTO,
): Promise<ModelObject<WarehouseTransfer>> => {
// Retrieves the old warehouse transfer transaction.
const oldWarehouseTransfer = await this.warehouseTransferModel()
.query()
.findById(warehouseTransferId)
.throwIfNotFound();
// Validate warehouse from and to should not be the same.
this.commandWarehouseTransfer.validateWarehouseFromToNotSame(
editWarehouseDTO,
);
// Retrieves the from warehouse or throw not found service error.
const fromWarehouse =
await this.commandWarehouseTransfer.getFromWarehouseOrThrow(
editWarehouseDTO.fromWarehouseId,
);
// Retrieves the to warehouse or throw not found service error.
const toWarehouse =
await this.commandWarehouseTransfer.getToWarehouseOrThrow(
editWarehouseDTO.toWarehouseId,
);
// Validates the not found entries items ids.
const items = await this.itemsEntries.validateItemsIdsExistance(
editWarehouseDTO.entries,
);
// Validate the items entries should be inventory type.
this.commandWarehouseTransfer.validateItemsShouldBeInventory(items);
// Edits warehouse transfer transaction under unit-of-work envirement.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onWarehouseTransferEdit` event.
await this.eventPublisher.emitAsync(events.warehouseTransfer.onEdit, {
editWarehouseDTO,
oldWarehouseTransfer,
trx,
} as IWarehouseTransferEditPayload);
// Updates warehouse transfer graph on the storage.
const warehouseTransfer = await this.warehouseTransferModel()
.query(trx)
.upsertGraphAndFetch({
id: warehouseTransferId,
...editWarehouseDTO,
});
// Triggers `onWarehouseTransferEdit` event
await this.eventPublisher.emitAsync(events.warehouseTransfer.onEdited, {
editWarehouseDTO,
warehouseTransfer,
oldWarehouseTransfer,
trx,
} as IWarehouseTransferEditedPayload);
return warehouseTransfer;
});
};
}

View File

@@ -0,0 +1,92 @@
import { Knex } from 'knex';
import {
IWarehouseTransferEditedPayload,
IWarehouseTransferInitiatedPayload,
IWarehouseTransferInitiatePayload,
} from '@/modules/Warehouses/Warehouse.types';
import { ERRORS } from '../constants';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { WarehouseTransfer } from '../models/WarehouseTransfer';
import { Inject } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { ServiceError } from '@/modules/Items/ServiceError';
import { events } from '@/common/events/events';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { ModelObject } from 'objection';
@Injectable()
export class InitiateWarehouseTransfer {
constructor(
private readonly uow: UnitOfWork,
private readonly eventPublisher: EventEmitter2,
@Inject(WarehouseTransfer.name)
private readonly warehouseTransferModel: TenantModelProxy<
typeof WarehouseTransfer
>,
) {}
/**
* Validate the given warehouse transfer not already initiated.
* @param {IWarehouseTransfer} warehouseTransfer
*/
private validateWarehouseTransferNotAlreadyInitiated = (
warehouseTransfer: ModelObject<WarehouseTransfer>,
) => {
if (warehouseTransfer.transferInitiatedAt) {
throw new ServiceError(ERRORS.WAREHOUSE_TRANSFER_ALREADY_INITIATED);
}
};
/**
* Initiate warehouse transfer.
* @param {number} warehouseTransferId
* @returns {Promise<IWarehouseTransfer>}
*/
public initiateWarehouseTransfer = async (
warehouseTransferId: number,
): Promise<ModelObject<WarehouseTransfer>> => {
// Retrieves the old warehouse transfer transaction.
const oldWarehouseTransfer = await this.warehouseTransferModel()
.query()
.findById(warehouseTransferId)
.throwIfNotFound();
// Validate the given warehouse transfer not already initiated.
this.validateWarehouseTransferNotAlreadyInitiated(oldWarehouseTransfer);
// Edits warehouse transfer transaction under unit-of-work envirement.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onWarehouseTransferInitiate` event.
await this.eventPublisher.emitAsync(events.warehouseTransfer.onInitiate, {
oldWarehouseTransfer,
trx,
} as IWarehouseTransferInitiatePayload);
// Updates warehouse transfer graph on the storage.
const warehouseTransferUpdated = await this.warehouseTransferModel()
.query(trx)
.findById(warehouseTransferId)
.patch({
transferInitiatedAt: new Date(),
});
// Fetches the warehouse transfer with entries.
const warehouseTransfer = await this.warehouseTransferModel()
.query(trx)
.findById(warehouseTransferId)
.withGraphFetched('entries');
// Triggers `onWarehouseTransferEdit` event
await this.eventPublisher.emitAsync(
events.warehouseTransfer.onInitiated,
{
warehouseTransfer,
oldWarehouseTransfer,
trx,
} as IWarehouseTransferInitiatedPayload,
);
return warehouseTransfer;
});
};
}

View File

@@ -0,0 +1,106 @@
import { Knex } from 'knex';
import {
IWarehouseTransferTransferingPayload,
IWarehouseTransferTransferredPayload,
} from '@/modules/Warehouses/Warehouse.types';
import { CommandWarehouseTransfer } from './CommandWarehouseTransfer';
import { ERRORS } from '../constants';
import { Inject, Injectable } from '@nestjs/common';
import { UnitOfWork } from '../../Tenancy/TenancyDB/UnitOfWork.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { TenantModelProxy } from '../../System/models/TenantBaseModel';
import { WarehouseTransfer } from '../models/WarehouseTransfer';
import { ServiceError } from '../../Items/ServiceError';
import { events } from '@/common/events/events';
import { ModelObject } from 'objection';
@Injectable()
export class TransferredWarehouseTransfer {
constructor(
private readonly uow: UnitOfWork,
private readonly eventPublisher: EventEmitter2,
@Inject(WarehouseTransfer.name)
private readonly warehouseTransferModel: TenantModelProxy<
typeof WarehouseTransfer
>,
) {}
/**
* Validate the warehouse transfer not already transferred.
* @param {IWarehouseTransfer} warehouseTransfer
*/
private validateWarehouseTransferNotTransferred = (
warehouseTransfer: ModelObject<WarehouseTransfer>,
) => {
if (warehouseTransfer.transferDeliveredAt) {
throw new ServiceError(ERRORS.WAREHOUSE_TRANSFER_ALREADY_TRANSFERRED);
}
};
/**
* Validate the warehouse transfer should be initiated.
* @param {IWarehouseTransfer} warehouseTransfer
*/
private validateWarehouseTranbsferShouldInitiated = (
warehouseTransfer: ModelObject<WarehouseTransfer>,
) => {
if (!warehouseTransfer.transferInitiatedAt) {
throw new ServiceError(ERRORS.WAREHOUSE_TRANSFER_NOT_INITIATED);
}
};
/**
* Transferred warehouse transfer.
* @param {number} warehouseTransferId
* @returns {Promise<IWarehouseTransfer>}
*/
public transferredWarehouseTransfer = async (
warehouseTransferId: number,
): Promise<ModelObject<WarehouseTransfer>> => {
// Retrieves the old warehouse transfer transaction.
const oldWarehouseTransfer = await this.warehouseTransferModel()
.query()
.findById(warehouseTransferId)
.throwIfNotFound();
// Validate the warehouse transfer not already transferred.
this.validateWarehouseTransferNotTransferred(oldWarehouseTransfer);
// Validate the warehouse transfer should be initiated.
this.validateWarehouseTranbsferShouldInitiated(oldWarehouseTransfer);
// Edits warehouse transfer transaction under unit-of-work envirement.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onWarehouseTransferInitiate` event.
await this.eventPublisher.emitAsync(events.warehouseTransfer.onTransfer, {
oldWarehouseTransfer,
trx,
} as IWarehouseTransferTransferingPayload);
// Updates warehouse transfer graph on the storage.
const warehouseTransferUpdated = await this.warehouseTransferModel()
.query(trx)
.findById(warehouseTransferId)
.patch({
transferDeliveredAt: new Date(),
});
// Fetches the warehouse transfer with entries.
const warehouseTransfer = await this.warehouseTransferModel()
.query(trx)
.findById(warehouseTransferId)
.withGraphFetched('entries');
// Triggers `onWarehouseTransferEdit` event
await this.eventPublisher.emitAsync(
events.warehouseTransfer.onTransferred,
{
warehouseTransfer,
oldWarehouseTransfer,
trx,
} as IWarehouseTransferTransferredPayload,
);
return warehouseTransfer;
});
};
}

View File

@@ -0,0 +1,28 @@
import { AutoIncrementOrdersService } from '@/modules/AutoIncrementOrders/AutoIncrementOrders.service';
import { Injectable } from '@nestjs/common';
@Injectable()
export class WarehouseTransferAutoIncrement {
constructor(
private readonly autoIncrementOrdersService: AutoIncrementOrdersService,
) {}
/**
* Retrieve the next unique invoice number.
* @return {Promise<string>}
*/
public getNextTransferNumber(): Promise<string> {
return this.autoIncrementOrdersService.getNextTransactionNumber(
'warehouse_transfers',
);
}
/**
* Increment the invoice next number.
*/
public incrementNextTransferNumber() {
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
'warehouse_transfers',
);
}
}

View File

@@ -0,0 +1,167 @@
import { Knex } from 'knex';
import { IWarehouseTransferEntry } from '@/modules/Warehouses/Warehouse.types';
import { Injectable } from '@nestjs/common';
import { InventoryTransactionsService } from '../../InventoryCost/commands/InventoryTransactions.service';
import { ModelObject } from 'objection';
import { WarehouseTransfer } from '../models/WarehouseTransfer';
import { InventoryTransaction } from '../../InventoryCost/models/InventoryTransaction';
import { WarehouseTransferEntry } from '../models/WarehouseTransferEntry';
@Injectable()
export class WarehouseTransferInventoryTransactions {
constructor(private readonly inventory: InventoryTransactionsService) {}
/**
* Writes all (initiate and transfer) inventory transactions.
* @param {ModelObject<WarehouseTransfer>} warehouseTransfer - Warehouse transfer.
* @param {Boolean} override - Override the inventory transactions.
* @param {Knex.Transaction} trx - Knex transcation.
* @returns {Promise<void>}
*/
public writeAllInventoryTransactions = async (
warehouseTransfer: ModelObject<WarehouseTransfer>,
override?: boolean,
trx?: Knex.Transaction,
): Promise<void> => {
const inventoryTransactions =
this.getWarehouseTransferInventoryTransactions(warehouseTransfer);
await this.inventory.recordInventoryTransactions(
inventoryTransactions,
override,
trx,
);
};
/**
* Writes initiate inventory transactions of warehouse transfer transaction.
* @param {ModelObject<WarehouseTransfer>} warehouseTransfer - Warehouse transfer.
* @param {boolean} override - Override the inventory transactions.
* @param {Knex.Transaction} trx - Knex transaction.
* @returns {Promise<void>}
*/
public writeInitiateInventoryTransactions = async (
warehouseTransfer: ModelObject<WarehouseTransfer>,
override?: boolean,
trx?: Knex.Transaction,
): Promise<void> => {
const inventoryTransactions =
this.getWarehouseFromTransferInventoryTransactions(warehouseTransfer);
await this.inventory.recordInventoryTransactions(
inventoryTransactions,
override,
trx,
);
};
/**
* Writes transferred inventory transaction of warehouse transfer transaction.
* @param {ModelObject<WarehouseTransfer>} warehouseTransfer - Warehouse transfer.
* @param {boolean} override - Override the inventory transactions.
* @param {Knex.Transaction} trx - Knex transaction.
* @returns {Promise<void>}
*/
public writeTransferredInventoryTransactions = async (
warehouseTransfer: ModelObject<WarehouseTransfer>,
override?: boolean,
trx?: Knex.Transaction,
): Promise<void> => {
const inventoryTransactions =
this.getWarehouseToTransferInventoryTransactions(warehouseTransfer);
await this.inventory.recordInventoryTransactions(
inventoryTransactions,
override,
trx,
);
};
/**
* Reverts warehouse transfer inventory transactions.
* @param {number} warehouseTransferId - Warehouse transfer id.
* @param {Knex.Transaction} trx - Knex transaction.
* @returns {Promise<void>}
*/
public revertInventoryTransactions = async (
warehouseTransferId: number,
trx?: Knex.Transaction,
): Promise<void> => {
await this.inventory.deleteInventoryTransactions(
warehouseTransferId,
'WarehouseTransfer',
trx,
);
};
/**
* Retrieves the inventory transactions of the given warehouse transfer.
* @param {IWarehouseTransfer} warehouseTransfer
* @returns {IInventoryTransaction[]}
*/
private getWarehouseFromTransferInventoryTransactions = (
warehouseTransfer: ModelObject<WarehouseTransfer>,
) => {
const commonEntry = {
date: warehouseTransfer.date,
transactionType: 'WarehouseTransfer',
transactionId: warehouseTransfer.id,
};
return warehouseTransfer.entries.map(
(entry: ModelObject<WarehouseTransferEntry>) => ({
...commonEntry,
entryId: entry.id,
itemId: entry.itemId,
quantity: entry.quantity,
rate: entry.cost,
direction: 'OUT',
warehouseId: warehouseTransfer.fromWarehouseId,
}),
);
};
/**
* Retrieves the inventory transactions of the given warehouse transfer.
* @param {ModelObject<WarehouseTransfer>} warehouseTransfer - Warehouse transfer.
* @returns {IInventoryTransaction[]}
*/
private getWarehouseToTransferInventoryTransactions = (
warehouseTransfer: ModelObject<WarehouseTransfer>,
) => {
const commonEntry = {
date: warehouseTransfer.date,
transactionType: 'WarehouseTransfer',
transactionId: warehouseTransfer.id,
};
return warehouseTransfer.entries.map(
(entry: ModelObject<WarehouseTransferEntry>) => ({
...commonEntry,
entryId: entry.id,
itemId: entry.itemId,
quantity: entry.quantity,
rate: entry.cost,
direction: 'IN',
warehouseId: warehouseTransfer.toWarehouseId,
}),
);
};
/**
* Retrieves the inventory transactions of the given warehouse transfer.
* @param {ModelObject<WarehouseTransfer>} warehouseTransfer - Warehouse transfer.
* @returns {IInventoryTransaction[]}
*/
private getWarehouseTransferInventoryTransactions = (
warehouseTransfer: ModelObject<WarehouseTransfer>,
) => {
// Retrieve the to inventory transactions of warehouse transfer.
const toTransactions =
this.getWarehouseToTransferInventoryTransactions(warehouseTransfer);
// Retrieve the from inventory transactions of warehouse transfer.
const fromTransactions =
this.getWarehouseFromTransferInventoryTransactions(warehouseTransfer);
return [...toTransactions, ...fromTransactions];
};
}

View File

@@ -0,0 +1,57 @@
export const ERRORS = {
WAREHOUSE_TRANSFER_NOT_FOUND: 'WAREHOUSE_TRANSFER_NOT_FOUND',
WAREHOUSES_TRANSFER_SHOULD_NOT_BE_SAME:
'WAREHOUSES_TRANSFER_SHOULD_NOT_BE_SAME',
FROM_WAREHOUSE_NOT_FOUND: 'FROM_WAREHOUSE_NOT_FOUND',
TO_WAREHOUSE_NOT_FOUND: 'TO_WAREHOUSE_NOT_FOUND',
WAREHOUSE_TRANSFER_ITEMS_SHOULD_BE_INVENTORY:
'WAREHOUSE_TRANSFER_ITEMS_SHOULD_BE_INVENTORY',
WAREHOUSE_TRANSFER_ALREADY_TRANSFERRED:
'WAREHOUSE_TRANSFER_ALREADY_TRANSFERRED',
WAREHOUSE_TRANSFER_ALREADY_INITIATED: 'WAREHOUSE_TRANSFER_ALREADY_INITIATED',
WAREHOUSE_TRANSFER_NOT_INITIATED: 'WAREHOUSE_TRANSFER_NOT_INITIATED',
};
// Warehouse transfers default views.
export const DEFAULT_VIEWS = [
{
name: 'warehouse_transfer.view.draft.name',
slug: 'draft',
rolesLogicExpression: '1',
roles: [
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'draft' },
],
columns: [],
},
{
name: 'warehouse_transfer.view.in_transit.name',
slug: 'in-transit',
rolesLogicExpression: '1',
roles: [
{
index: 1,
fieldKey: 'status',
comparator: 'equals',
value: 'in-transit',
},
],
columns: [],
},
{
name: 'warehouse_transfer.view.transferred.name',
slug: 'transferred',
rolesLogicExpression: '1',
roles: [
{
index: 1,
fieldKey: 'status',
comparator: 'equals',
value: 'tansferred',
},
],
columns: [],
},
];

View File

@@ -0,0 +1,152 @@
import { Model, mixin } from 'objection';
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
import { Warehouse } from '@/modules/Warehouses/models/Warehouse.model';
import { WarehouseTransferEntry } from './WarehouseTransferEntry';
export class WarehouseTransfer extends TenantBaseModel {
public date!: Date;
public transferInitiatedAt!: Date;
public transferDeliveredAt!: Date;
public fromWarehouseId!: number;
public toWarehouseId!: number;
public entries!: WarehouseTransferEntry[];
public fromWarehouse!: Warehouse;
public toWarehouse!: Warehouse;
/**
* Table name.
*/
static get tableName() {
return 'warehouses_transfers';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['isInitiated', 'isTransferred'];
}
/**
* Detarmines whether the warehouse transfer initiated.
* @retruns {boolean}
*/
get isInitiated() {
return !!this.transferInitiatedAt;
}
/**
* Detarmines whether the warehouse transfer transferred.
* @returns {boolean}
*/
get isTransferred() {
return !!this.transferDeliveredAt;
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
filterByDraft(query) {
query.whereNull('transferInitiatedAt');
query.whereNull('transferDeliveredAt');
},
filterByInTransit(query) {
query.whereNotNull('transferInitiatedAt');
query.whereNull('transferDeliveredAt');
},
filterByTransferred(query) {
query.whereNotNull('transferInitiatedAt');
query.whereNotNull('transferDeliveredAt');
},
filterByStatus(query, status) {
switch (status) {
case 'draft':
default:
return query.modify('filterByDraft');
case 'in-transit':
return query.modify('filterByInTransit');
case 'transferred':
return query.modify('filterByTransferred');
}
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const { WarehouseTransferEntry } = require('./WarehouseTransferEntry');
const { Warehouse } = require('../../Warehouses/models/Warehouse.model');
return {
/**
* View model may has many columns.
*/
entries: {
relation: Model.HasManyRelation,
modelClass: WarehouseTransferEntry,
join: {
from: 'warehouses_transfers.id',
to: 'warehouses_transfers_entries.warehouseTransferId',
},
},
/**
*
*/
fromWarehouse: {
relation: Model.BelongsToOneRelation,
modelClass: Warehouse,
join: {
from: 'warehouses_transfers.fromWarehouseId',
to: 'warehouses.id',
},
},
toWarehouse: {
relation: Model.BelongsToOneRelation,
modelClass: Warehouse,
join: {
from: 'warehouses_transfers.toWarehouseId',
to: 'warehouses.id',
},
},
};
}
/**
* Model settings.
*/
// static get meta() {
// return WarehouseTransferSettings;
// }
// /**
// * Retrieve the default custom views, roles and columns.
// */
// static get defaultViews() {
// return DEFAULT_VIEWS;
// }
/**
* Model search roles.
*/
static get searchRoles() {
return [
// { fieldKey: 'name', comparator: 'contains' },
// { condition: 'or', fieldKey: 'code', comparator: 'like' },
];
}
}

View File

@@ -0,0 +1,54 @@
import { Model } from 'objection';
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
import { WarehouseTransfer } from './WarehouseTransfer';
import { Item } from '@/modules/Items/models/Item';
export class WarehouseTransferEntry extends TenantBaseModel {
public warehouseTransferId!: number;
public itemId!: number;
public quantity!: number;
public cost!: number;
public warehouseTransfer!: WarehouseTransfer;
public item!: Item;
/**
* Table name.
*/
static get tableName() {
return 'warehouses_transfers_entries';
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['total'];
}
/**
* Invoice amount in local currency.
* @returns {number}
*/
get total() {
return this.cost * this.quantity;
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const { Item } = require('../../Items/models/Item');
return {
item: {
relation: Model.BelongsToOneRelation,
modelClass: Item,
join: {
from: 'warehouses_transfers_entries.itemId',
to: 'items.id',
},
},
};
}
}

View File

@@ -0,0 +1,43 @@
import { WarehouseTransferTransformer } from './WarehouseTransferTransfomer';
import { Inject, Injectable } from '@nestjs/common';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { WarehouseTransfer } from '../models/WarehouseTransfer';
import { ModelObject } from 'objection';
@Injectable()
export class GetWarehouseTransfer {
constructor(
private readonly transformer: TransformerInjectable,
@Inject(WarehouseTransfer.name)
private readonly warehouseTransferModel: TenantModelProxy<
typeof WarehouseTransfer
>,
) {}
/**
* Retrieves the specific warehouse transfer transaction.
* @param {number} warehouseTransferId
* @param {IEditWarehouseTransferDTO} editWarehouseDTO
* @returns {Promise<IWarehouseTransfer>}
*/
public getWarehouseTransfer = async (
warehouseTransferId: number,
): Promise<ModelObject<WarehouseTransfer>> => {
// Retrieves the old warehouse transfer transaction.
const warehouseTransfer = await this.warehouseTransferModel()
.query()
.findById(warehouseTransferId)
.withGraphFetched('entries.item')
.withGraphFetched('fromWarehouse')
.withGraphFetched('toWarehouse')
.throwIfNotFound();
// Retrieves the transfromed warehouse transfers.
return this.transformer.transform(
warehouseTransfer,
new WarehouseTransferTransformer(),
);
};
}

View File

@@ -0,0 +1,70 @@
import * as R from 'ramda';
import { WarehouseTransferTransformer } from './WarehouseTransferTransfomer';
import { IGetWarehousesTransfersFilterDTO } from '../../Warehouses/Warehouse.types';
import { TransformerInjectable } from '../../Transformer/TransformerInjectable.service';
import { Inject, Injectable } from '@nestjs/common';
import { DynamicListService } from '../../DynamicListing/DynamicList.service';
import { TenantModelProxy } from '../../System/models/TenantBaseModel';
import { WarehouseTransfer } from '../models/WarehouseTransfer';
@Injectable()
export class GetWarehouseTransfers {
constructor(
private readonly dynamicListService: DynamicListService,
private readonly transformer: TransformerInjectable,
@Inject(WarehouseTransfer.name)
private readonly warehouseTransferModel: TenantModelProxy<
typeof WarehouseTransfer
>,
) {}
/**
* Parses the sale invoice list filter DTO.
* @param filterDTO
* @returns
*/
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
/**
* Retrieves warehouse transfers paginated list.
* @param {number} tenantId
* @param {IGetWarehousesTransfersFilterDTO} filterDTO
* @returns {}
*/
public getWarehouseTransfers = async (
filterDTO: IGetWarehousesTransfersFilterDTO,
) => {
// Parses stringified filter roles.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList(
this.warehouseTransferModel(),
filter,
);
const { results, pagination } = await this.warehouseTransferModel()
.query()
.onBuild((query) => {
query.withGraphFetched('entries.item');
query.withGraphFetched('fromWarehouse');
query.withGraphFetched('toWarehouse');
dynamicFilter.buildQuery()(query);
})
.pagination(filter.page - 1, filter.pageSize);
// Retrieves the transformed warehouse transfers
const warehousesTransfers = await this.transformer.transform(
results,
new WarehouseTransferTransformer(),
);
return {
warehousesTransfers,
pagination,
filter,
};
};
}

View File

@@ -0,0 +1,38 @@
import { Transformer } from "../../Transformer/Transformer";
export class WarehouseTransferItemTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['formattedQuantity', 'formattedCost', 'formattedTotal'];
};
/**
* Formats the total.
* @param {IWarehouseTransferEntry} entry
* @returns {string}
*/
public formattedTotal = (entry) => {
return this.formatMoney(entry.total);
};
/**
* Formats the quantity.
* @param {IWarehouseTransferEntry} entry
* @returns {string}
*/
public formattedQuantity = (entry) => {
return this.formatNumber(entry.quantity);
};
/**
* Formats the cost.
* @param {IWarehouseTransferEntry} entry
* @returns {string}
*/
public formattedCost = (entry) => {
return this.formatMoney(entry.cost);
};
}

View File

@@ -0,0 +1,27 @@
import { WarehouseTransferItemTransformer } from './WarehouseTransferItemTransformer';
import { Transformer } from '@/modules/Transformer/Transformer';
export class WarehouseTransferTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['formattedDate', 'entries'];
};
/**
*
* @param transfer
* @returns
*/
protected formattedDate = (transfer) => {
return this.formatDate(transfer.date);
};
/**
*
*/
protected entries = (transfer) => {
return this.item(transfer.entries, new WarehouseTransferItemTransformer());
};
}

View File

@@ -0,0 +1,21 @@
import { Inject, Injectable } from '@nestjs/common';
import { events } from '@/common/events/events';
import { IWarehouseTransferCreated } from '../../Warehouses/Warehouse.types';
import { WarehouseTransferAutoIncrement } from '../commands/WarehouseTransferAutoIncrement';
import { OnEvent } from '@nestjs/event-emitter';
@Injectable()
export class WarehouseTransferAutoIncrementSubscriber {
constructor(
private readonly warehouseTransferAutoIncrement: WarehouseTransferAutoIncrement,
) {}
/**
* Writes inventory transactions once warehouse transfer created.
* @param {IInventoryTransactionsCreatedPayload} -
*/
@OnEvent(events.warehouseTransfer.onCreated)
async incrementTransferAutoIncrementOnCreated({}: IWarehouseTransferCreated) {
await this.warehouseTransferAutoIncrement.incrementNextTransferNumber();
}
}

View File

@@ -0,0 +1,123 @@
import {
IWarehouseTransferEditedPayload,
IWarehouseTransferDeletedPayload,
IWarehouseTransferCreated,
IWarehouseTransferInitiatedPayload,
IWarehouseTransferTransferredPayload,
} from '@/modules/Warehouses/Warehouse.types';
import { WarehouseTransferInventoryTransactions } from '../commands/WarehouseTransferWriteInventoryTransactions';
import { OnEvent } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { Injectable } from '@nestjs/common';
@Injectable()
export class WarehouseTransferInventoryTransactionsSubscriber {
constructor(
private readonly warehouseTransferInventoryTransactions: WarehouseTransferInventoryTransactions,
) {}
/**
* Writes inventory transactions once warehouse transfer created.
* @param {IInventoryTransactionsCreatedPayload} -
*/
@OnEvent(events.warehouseTransfer.onCreated)
async writeInventoryTransactionsOnWarehouseTransferCreated({
warehouseTransfer,
trx,
}: IWarehouseTransferCreated) {
// Can't continue if the warehouse transfer is not initiated yet.
if (!warehouseTransfer.isInitiated) return;
// Write all inventory transaction if warehouse transfer initiated and transferred.
if (warehouseTransfer.isInitiated && warehouseTransfer.isTransferred) {
await this.warehouseTransferInventoryTransactions.writeAllInventoryTransactions(
warehouseTransfer,
false,
trx,
);
// Write initiate inventory transaction if warehouse transfer initited and transferred yet.
} else if (warehouseTransfer.isInitiated) {
await this.warehouseTransferInventoryTransactions.writeInitiateInventoryTransactions(
warehouseTransfer,
false,
trx,
);
}
}
/**
* Rewrite inventory transactions once warehouse transfer edited.
* @param {IWarehouseTransferEditedPayload} -
*/
@OnEvent(events.warehouseTransfer.onEdited)
async rewriteInventoryTransactionsOnWarehouseTransferEdited({
warehouseTransfer,
trx,
}: IWarehouseTransferEditedPayload) {
// Can't continue if the warehouse transfer is not initiated yet.
if (!warehouseTransfer.isInitiated) return;
// Write all inventory transaction if warehouse transfer initiated and transferred.
if (warehouseTransfer.isInitiated && warehouseTransfer.isTransferred) {
await this.warehouseTransferInventoryTransactions.writeAllInventoryTransactions(
warehouseTransfer,
true,
trx,
);
// Write initiate inventory transaction if warehouse transfer initited and transferred yet.
} else if (warehouseTransfer.isInitiated) {
await this.warehouseTransferInventoryTransactions.writeInitiateInventoryTransactions(
warehouseTransfer,
true,
trx,
);
}
}
/**
* Reverts inventory transactions once warehouse transfer deleted.
* @parma {IWarehouseTransferDeletedPayload} -
*/
@OnEvent(events.warehouseTransfer.onDeleted)
async revertInventoryTransactionsOnWarehouseTransferDeleted({
oldWarehouseTransfer,
trx,
}: IWarehouseTransferDeletedPayload) {
await this.warehouseTransferInventoryTransactions.revertInventoryTransactions(
oldWarehouseTransfer.id,
trx,
);
}
/**
* Write inventory transactions of warehouse transfer once the transfer initiated.
* @param {IWarehouseTransferInitiatedPayload}
*/
@OnEvent(events.warehouseTransfer.onInitiated)
async writeInventoryTransactionsOnTransferInitiated({
trx,
warehouseTransfer,
}: IWarehouseTransferInitiatedPayload) {
await this.warehouseTransferInventoryTransactions.writeInitiateInventoryTransactions(
warehouseTransfer,
false,
trx,
);
}
/**
* Write inventory transactions of warehouse transfer once the transfer completed.
* @param {IWarehouseTransferTransferredPayload}
*/
@OnEvent(events.warehouseTransfer.onTransferred)
async writeInventoryTransactionsOnTransferred({
trx,
warehouseTransfer,
}: IWarehouseTransferTransferredPayload) {
await this.warehouseTransferInventoryTransactions.writeTransferredInventoryTransactions(
warehouseTransfer,
false,
trx,
);
}
}