mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 21:30:31 +00:00
feat(nestjs): migrate to NestJS
This commit is contained in:
45
packages/server/src/modules/Items/ActivateItem.service.ts
Normal file
45
packages/server/src/modules/Items/ActivateItem.service.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Knex } from 'knex';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Item } from './models/Item';
|
||||
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class ActivateItemService {
|
||||
constructor(
|
||||
@Inject(Item.name)
|
||||
private readonly itemModel: TenantModelProxy<typeof Item>,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Activates the given item on the storage.
|
||||
* @param {number} itemId -
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async activateItem(
|
||||
itemId: number,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
// Retrieves the given item or throw not found error.
|
||||
const oldItem = await this.itemModel()
|
||||
.query()
|
||||
.findById(itemId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Activate the given item with associated transactions under unit-of-work environment.
|
||||
return this.uow.withTransaction(async (trx) => {
|
||||
// Mutate item on the storage.
|
||||
await this.itemModel()
|
||||
.query(trx)
|
||||
.findById(itemId)
|
||||
.patch({ active: true });
|
||||
|
||||
// Triggers `onItemActivated` event.
|
||||
await this.eventEmitter.emitAsync(events.item.onActivated, {});
|
||||
}, trx);
|
||||
}
|
||||
}
|
||||
123
packages/server/src/modules/Items/CreateItem.service.ts
Normal file
123
packages/server/src/modules/Items/CreateItem.service.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { Knex } from 'knex';
|
||||
import { defaultTo } from 'lodash';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||
import { IItemDTO, IItemEventCreatedPayload } from '@/interfaces/Item';
|
||||
import { events } from '@/common/events/events';
|
||||
import { ItemsValidators } from './ItemValidator.service';
|
||||
import { Item } from './models/Item';
|
||||
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
import { CreateItemDto } from './dtos/Item.dto';
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class CreateItemService {
|
||||
/**
|
||||
* Constructor for the CreateItemService class.
|
||||
* @param {EventEmitter2} eventEmitter - Event emitter for publishing item creation events.
|
||||
* @param {UnitOfWork} uow - Unit of Work for tenant database transactions.
|
||||
* @param {ItemsValidators} validators - Service for validating item data.
|
||||
* @param {typeof Item} itemModel - The Item model class for database operations.
|
||||
*/
|
||||
constructor(
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly validators: ItemsValidators,
|
||||
|
||||
@Inject(Item.name)
|
||||
private readonly itemModel: TenantModelProxy<typeof Item>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Authorize the creating item.
|
||||
* @param {number} tenantId
|
||||
* @param {IItemDTO} itemDTO
|
||||
*/
|
||||
async authorize(itemDTO: CreateItemDto) {
|
||||
// Validate whether the given item name already exists on the storage.
|
||||
await this.validators.validateItemNameUniquiness(itemDTO.name);
|
||||
|
||||
if (itemDTO.categoryId) {
|
||||
await this.validators.validateItemCategoryExistance(itemDTO.categoryId);
|
||||
}
|
||||
if (itemDTO.sellAccountId) {
|
||||
await this.validators.validateItemSellAccountExistance(
|
||||
itemDTO.sellAccountId,
|
||||
);
|
||||
}
|
||||
// Validate the income account id existance if the item is sellable.
|
||||
this.validators.validateIncomeAccountExistance(
|
||||
itemDTO.sellable,
|
||||
itemDTO.sellAccountId,
|
||||
);
|
||||
if (itemDTO.costAccountId) {
|
||||
await this.validators.validateItemCostAccountExistance(
|
||||
itemDTO.costAccountId,
|
||||
);
|
||||
}
|
||||
// Validate the cost account id existance if the item is purchasable.
|
||||
this.validators.validateCostAccountExistance(
|
||||
itemDTO.purchasable,
|
||||
itemDTO.costAccountId,
|
||||
);
|
||||
if (itemDTO.inventoryAccountId) {
|
||||
await this.validators.validateItemInventoryAccountExistance(
|
||||
itemDTO.inventoryAccountId,
|
||||
);
|
||||
}
|
||||
if (itemDTO.purchaseTaxRateId) {
|
||||
await this.validators.validatePurchaseTaxRateExistance(
|
||||
itemDTO.purchaseTaxRateId,
|
||||
);
|
||||
}
|
||||
if (itemDTO.sellTaxRateId) {
|
||||
await this.validators.validateSellTaxRateExistance(itemDTO.sellTaxRateId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the item DTO to model.
|
||||
* @param {CreateItemDto} itemDTO - Item DTO.
|
||||
* @return {IItem}
|
||||
*/
|
||||
private transformNewItemDTOToModel(itemDTO: CreateItemDto) {
|
||||
return {
|
||||
...itemDTO,
|
||||
active: defaultTo(itemDTO.active, 1),
|
||||
quantityOnHand: itemDTO.type === 'inventory' ? 0 : null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new item.
|
||||
* @param {IItemDTO} itemDTO
|
||||
* @return {Promise<number>} - The created item id.
|
||||
*/
|
||||
public async createItem(
|
||||
itemDTO: CreateItemDto,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<number> {
|
||||
// Authorize the item before creating.
|
||||
await this.authorize(itemDTO);
|
||||
|
||||
// Creates a new item with associated transactions under unit-of-work envirement.
|
||||
return this.uow.withTransaction<number>(async (trx: Knex.Transaction) => {
|
||||
const itemInsert = this.transformNewItemDTOToModel(itemDTO);
|
||||
|
||||
// Inserts a new item and fetch the created item.
|
||||
const item = await this.itemModel()
|
||||
.query(trx)
|
||||
.insertAndFetch({
|
||||
...itemInsert,
|
||||
});
|
||||
// Triggers `onItemCreated` event.
|
||||
await this.eventEmitter.emitAsync(events.item.onCreated, {
|
||||
item,
|
||||
itemId: item.id,
|
||||
trx,
|
||||
} as IItemEventCreatedPayload);
|
||||
|
||||
return item.id;
|
||||
}, trx);
|
||||
}
|
||||
}
|
||||
67
packages/server/src/modules/Items/DeleteItem.service.ts
Normal file
67
packages/server/src/modules/Items/DeleteItem.service.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import {
|
||||
IItemEventDeletedPayload,
|
||||
IItemEventDeletingPayload,
|
||||
} from '@/interfaces/Item';
|
||||
import { events } from '@/common/events/events';
|
||||
import { Item } from './models/Item';
|
||||
import { ERRORS } from './Items.constants';
|
||||
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteItemService {
|
||||
/**
|
||||
* Constructor for the class.
|
||||
* @param {EventEmitter2} eventEmitter - Event emitter for publishing item deletion events.
|
||||
* @param {UnitOfWork} uow - Unit of Work for tenant database transactions.
|
||||
* @param {typeof Item} itemModel - The Item model class for database operations.
|
||||
*/
|
||||
constructor(
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
|
||||
@Inject(Item.name)
|
||||
private readonly itemModel: TenantModelProxy<typeof Item>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Delete the given item from the storage.
|
||||
* @param {number} itemId - Item id.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async deleteItem(
|
||||
itemId: number,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
// Retrieve the given item or throw not found service error.
|
||||
const oldItem = await this.itemModel()
|
||||
.query()
|
||||
.findById(itemId)
|
||||
.throwIfNotFound();
|
||||
// .queryAndThrowIfHasRelations({
|
||||
// type: ERRORS.ITEM_HAS_ASSOCIATED_TRANSACTIONS,
|
||||
// });
|
||||
|
||||
// Delete item in unit of work.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Triggers `onItemDeleting` event.
|
||||
await this.eventEmitter.emitAsync(events.item.onDeleting, {
|
||||
trx,
|
||||
oldItem,
|
||||
} as IItemEventDeletingPayload);
|
||||
|
||||
// Deletes the item.
|
||||
await this.itemModel().query(trx).findById(itemId).delete();
|
||||
|
||||
// Triggers `onItemDeleted` event.
|
||||
await this.eventEmitter.emitAsync(events.item.onDeleted, {
|
||||
itemId,
|
||||
oldItem,
|
||||
trx,
|
||||
} as IItemEventDeletedPayload);
|
||||
}, trx);
|
||||
}
|
||||
}
|
||||
146
packages/server/src/modules/Items/EditItem.service.ts
Normal file
146
packages/server/src/modules/Items/EditItem.service.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { IItemDTO, IItemEventEditedPayload } from '@/interfaces/Item';
|
||||
import { events } from '@/common/events/events';
|
||||
import { ItemsValidators } from './ItemValidator.service';
|
||||
import { Item } from './models/Item';
|
||||
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
import { EditItemDto } from './dtos/Item.dto';
|
||||
|
||||
@Injectable()
|
||||
export class EditItemService {
|
||||
/**
|
||||
* Constructor for the class.
|
||||
* @param {EventEmitter2} eventEmitter - Event emitter for publishing item edit events.
|
||||
* @param {UnitOfWork} uow - Unit of Work for tenant database transactions.
|
||||
* @param {ItemsValidators} validators - Service for validating item data.
|
||||
* @param {typeof Item} itemModel - The Item model class for database operations.
|
||||
*/
|
||||
constructor(
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly validators: ItemsValidators,
|
||||
|
||||
@Inject(Item.name)
|
||||
private readonly itemModel: TenantModelProxy<typeof Item>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Authorize the editing item.
|
||||
* @param {IItemDTO} itemDTO
|
||||
* @param {Item} oldItem
|
||||
*/
|
||||
async authorize(itemDTO: EditItemDto, oldItem: Item) {
|
||||
// Validate edit item type from inventory type.
|
||||
this.validators.validateEditItemFromInventory(itemDTO, oldItem);
|
||||
|
||||
// Validate edit item type to inventory type.
|
||||
await this.validators.validateEditItemTypeToInventory(oldItem, itemDTO);
|
||||
|
||||
// Validate whether the given item name already exists on the storage.
|
||||
await this.validators.validateItemNameUniquiness(itemDTO.name, oldItem.id);
|
||||
|
||||
if (itemDTO.categoryId) {
|
||||
await this.validators.validateItemCategoryExistance(itemDTO.categoryId);
|
||||
}
|
||||
if (itemDTO.sellAccountId) {
|
||||
await this.validators.validateItemSellAccountExistance(
|
||||
itemDTO.sellAccountId,
|
||||
);
|
||||
}
|
||||
// Validate the income account id existance if the item is sellable.
|
||||
this.validators.validateIncomeAccountExistance(
|
||||
itemDTO.sellable,
|
||||
itemDTO.sellAccountId,
|
||||
);
|
||||
if (itemDTO.costAccountId) {
|
||||
await this.validators.validateItemCostAccountExistance(
|
||||
itemDTO.costAccountId,
|
||||
);
|
||||
}
|
||||
// Validate the cost account id existance if the item is purchasable.
|
||||
this.validators.validateCostAccountExistance(
|
||||
itemDTO.purchasable,
|
||||
itemDTO.costAccountId,
|
||||
);
|
||||
if (itemDTO.inventoryAccountId) {
|
||||
await this.validators.validateItemInventoryAccountExistance(
|
||||
itemDTO.inventoryAccountId,
|
||||
);
|
||||
}
|
||||
if (itemDTO.purchaseTaxRateId) {
|
||||
await this.validators.validatePurchaseTaxRateExistance(
|
||||
itemDTO.purchaseTaxRateId,
|
||||
);
|
||||
}
|
||||
if (itemDTO.sellTaxRateId) {
|
||||
await this.validators.validateSellTaxRateExistance(itemDTO.sellTaxRateId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the edit item DTO to model.
|
||||
* @param {IItemDTO} itemDTO - Item DTO.
|
||||
* @param {Item} oldItem - Old item.
|
||||
* @return {Partial<Item>}
|
||||
*/
|
||||
private transformEditItemDTOToModel(
|
||||
itemDTO: EditItemDto,
|
||||
oldItem: Item,
|
||||
): Partial<Item> {
|
||||
return {
|
||||
...itemDTO,
|
||||
...(itemDTO.type === 'inventory' && oldItem.type !== 'inventory'
|
||||
? {
|
||||
quantityOnHand: 0,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the item metadata.
|
||||
* @param {number} itemId - The item id.
|
||||
* @param {IItemDTO} itemDTO - The item DTO.
|
||||
* @return {Promise<number>} - The updated item id.
|
||||
*/
|
||||
public async editItem(
|
||||
itemId: number,
|
||||
itemDTO: EditItemDto,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<number> {
|
||||
// Validates the given item existance on the storage.
|
||||
const oldItem = await this.itemModel()
|
||||
.query()
|
||||
.findById(itemId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Authorize before editing item.
|
||||
await this.authorize(itemDTO, oldItem);
|
||||
|
||||
// Transform the edit item DTO to model.
|
||||
const itemModel = this.transformEditItemDTOToModel(itemDTO, oldItem);
|
||||
|
||||
// Edits the item with associated transactions under unit-of-work environment.
|
||||
return this.uow.withTransaction<number>(async (trx: Knex.Transaction) => {
|
||||
// Updates the item on the storage and fetches the updated one.
|
||||
const newItem = await this.itemModel()
|
||||
.query(trx)
|
||||
.patchAndFetchById(itemId, itemModel);
|
||||
|
||||
// Edit event payload.
|
||||
const eventPayload: IItemEventEditedPayload = {
|
||||
item: newItem,
|
||||
oldItem,
|
||||
itemId: newItem.id,
|
||||
trx,
|
||||
};
|
||||
// Triggers `onItemEdited` event.
|
||||
await this.eventEmitter.emitAsync(events.item.onEdited, eventPayload);
|
||||
|
||||
return newItem.id;
|
||||
}, trx);
|
||||
}
|
||||
}
|
||||
49
packages/server/src/modules/Items/GetItem.service.ts
Normal file
49
packages/server/src/modules/Items/GetItem.service.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Item } from './models/Item';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
||||
import { ItemTransformer } from './Item.transformer';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
|
||||
@Injectable()
|
||||
export class GetItemService {
|
||||
constructor(
|
||||
@Inject(Item.name)
|
||||
private readonly itemModel: TenantModelProxy<typeof Item>,
|
||||
private readonly eventEmitter2: EventEmitter2,
|
||||
private readonly transformerInjectable: TransformerInjectable,
|
||||
private readonly clsService: ClsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve the item details of the given id with associated details.
|
||||
* @param {number} tenantId - The tenant id.
|
||||
* @param {number} itemId - The item id.
|
||||
*/
|
||||
public async getItem(itemId: number): Promise<any> {
|
||||
const item = await this.itemModel()
|
||||
.query()
|
||||
.findById(itemId)
|
||||
.withGraphFetched('sellAccount')
|
||||
.withGraphFetched('inventoryAccount')
|
||||
.withGraphFetched('category')
|
||||
.withGraphFetched('costAccount')
|
||||
.withGraphFetched('itemWarehouses.warehouse')
|
||||
.withGraphFetched('sellTaxRate')
|
||||
.withGraphFetched('purchaseTaxRate')
|
||||
.throwIfNotFound();
|
||||
|
||||
const transformed = await this.transformerInjectable.transform(
|
||||
item,
|
||||
new ItemTransformer(),
|
||||
);
|
||||
const eventPayload = { itemId };
|
||||
|
||||
// Triggers the `onItemViewed` event.
|
||||
await this.eventEmitter2.emitAsync(events.item.onViewed, eventPayload);
|
||||
|
||||
return transformed;
|
||||
}
|
||||
}
|
||||
67
packages/server/src/modules/Items/GetItems.service.ts
Normal file
67
packages/server/src/modules/Items/GetItems.service.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import * as R from 'ramda';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
||||
import { DynamicListService } from '../DynamicListing/DynamicList.service';
|
||||
import { Item } from './models/Item';
|
||||
import { IItemsFilter } from './types/Items.types';
|
||||
import { ItemTransformer } from './Item.transformer';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetItemsService {
|
||||
constructor(
|
||||
private readonly dynamicListService: DynamicListService,
|
||||
private readonly transformer: TransformerInjectable,
|
||||
@Inject(Item.name)
|
||||
private readonly itemModel: TenantModelProxy<typeof Item>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Parses items list filter DTO.
|
||||
* @param {} filterDTO - Filter DTO.
|
||||
*/
|
||||
private parseItemsListFilterDTO(filterDTO: IItemsFilter) {
|
||||
return R.compose(
|
||||
this.dynamicListService.parseStringifiedFilter<IItemsFilter>,
|
||||
)(filterDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves items datatable list.
|
||||
* @param {IItemsFilter} itemsFilter - Items filter.
|
||||
*/
|
||||
public async getItems(filterDTO: IItemsFilter) {
|
||||
// Parses items list filter DTO.
|
||||
const filter = this.parseItemsListFilterDTO(filterDTO);
|
||||
|
||||
// Dynamic list service.
|
||||
const dynamicFilter = await this.dynamicListService.dynamicList(
|
||||
Item,
|
||||
filter,
|
||||
);
|
||||
const { results: items, pagination } = await this.itemModel()
|
||||
.query()
|
||||
.onBuild((builder) => {
|
||||
builder.modify('inactiveMode', filter.inactiveMode);
|
||||
|
||||
builder.withGraphFetched('inventoryAccount');
|
||||
builder.withGraphFetched('sellAccount');
|
||||
builder.withGraphFetched('costAccount');
|
||||
builder.withGraphFetched('category');
|
||||
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
})
|
||||
.pagination(filter.page - 1, filter.pageSize);
|
||||
|
||||
// Retrieves the transformed items.
|
||||
const transformedItems = await this.transformer.transform(
|
||||
items,
|
||||
new ItemTransformer(),
|
||||
);
|
||||
return {
|
||||
items: transformedItems,
|
||||
pagination,
|
||||
filterMeta: dynamicFilter.getResponseMeta(),
|
||||
};
|
||||
}
|
||||
}
|
||||
44
packages/server/src/modules/Items/InactivateItem.service.ts
Normal file
44
packages/server/src/modules/Items/InactivateItem.service.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { Item } from './models/Item';
|
||||
import { events } from '@/common/events/events';
|
||||
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class InactivateItem {
|
||||
constructor(
|
||||
@Inject(Item.name) private itemModel: TenantModelProxy<typeof Item>,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Inactivates the given item on the storage.
|
||||
* @param {number} itemId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async inactivateItem(
|
||||
itemId: number,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
// Retrieves the item or throw not found error.
|
||||
const oldItem = await this.itemModel()
|
||||
.query()
|
||||
.findById(itemId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Inactivate item under unit-of-work environment.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Inactivate item on the storage.
|
||||
await this.itemModel()
|
||||
.query(trx)
|
||||
.findById(itemId)
|
||||
.patch({ active: false });
|
||||
|
||||
// Triggers `onItemInactivated` event.
|
||||
await this.eventEmitter.emitAsync(events.item.onInactivated, { trx });
|
||||
}, trx);
|
||||
}
|
||||
}
|
||||
335
packages/server/src/modules/Items/Item.controller.ts
Normal file
335
packages/server/src/modules/Items/Item.controller.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Param,
|
||||
Post,
|
||||
UseGuards,
|
||||
Patch,
|
||||
Get,
|
||||
Put,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { TenantController } from '../Tenancy/Tenant.controller';
|
||||
import { SubscriptionGuard } from '../Subscription/interceptors/Subscription.guard';
|
||||
import { JwtAuthGuard } from '../Auth/guards/jwt.guard';
|
||||
import { ItemsApplicationService } from './ItemsApplication.service';
|
||||
import {
|
||||
ApiOperation,
|
||||
ApiParam,
|
||||
ApiQuery,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import { IItemsFilter } from './types/Items.types';
|
||||
import { CreateItemDto, EditItemDto } from './dtos/Item.dto';
|
||||
|
||||
@Controller('/items')
|
||||
@UseGuards(SubscriptionGuard)
|
||||
@ApiTags('items')
|
||||
export class ItemsController extends TenantController {
|
||||
constructor(private readonly itemsApplication: ItemsApplicationService) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Retrieves the item list.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The item list has been successfully retrieved.',
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'customViewId',
|
||||
required: false,
|
||||
type: Number,
|
||||
description: 'Custom view ID for filtering',
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'filterRoles',
|
||||
required: false,
|
||||
type: Array,
|
||||
description: 'Array of filter roles',
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'columnSortBy',
|
||||
required: false,
|
||||
type: String,
|
||||
description: 'Column sort direction',
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'sortOrder',
|
||||
required: false,
|
||||
type: String,
|
||||
enum: ['DESC', 'ASC'],
|
||||
description: 'Sort order direction',
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'stringifiedFilterRoles',
|
||||
required: false,
|
||||
type: String,
|
||||
description: 'Stringified filter roles',
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'searchKeyword',
|
||||
required: false,
|
||||
type: String,
|
||||
description: 'Search keyword',
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'viewSlug',
|
||||
required: false,
|
||||
type: String,
|
||||
description: 'View slug for filtering',
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'page',
|
||||
required: false,
|
||||
type: Number,
|
||||
description: 'Page number for pagination',
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'pageSize',
|
||||
required: false,
|
||||
type: Number,
|
||||
description: 'Number of items per page',
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'inactiveMode',
|
||||
required: false,
|
||||
type: Boolean,
|
||||
description: 'Filter for inactive items',
|
||||
})
|
||||
async getItems(@Query() filterDTO: IItemsFilter): Promise<any> {
|
||||
return this.itemsApplication.getItems(filterDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit item.
|
||||
* @param id - The item id.
|
||||
* @param editItemDto - The item DTO.
|
||||
* @returns The updated item id.
|
||||
*/
|
||||
@Put(':id')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({ summary: 'Edit the given item (product or service).' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The item has been successfully updated.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The item not found.' })
|
||||
async editItem(
|
||||
@Param('id') id: string,
|
||||
@Body() editItemDto: EditItemDto,
|
||||
): Promise<number> {
|
||||
const itemId = parseInt(id, 10);
|
||||
return this.itemsApplication.editItem(itemId, editItemDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create item.
|
||||
* @param createItemDto - The item DTO.
|
||||
* @returns The created item id.
|
||||
*/
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create a new item (product or service).' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The item has been successfully created.',
|
||||
})
|
||||
// @UsePipes(new ZodValidationPipe(createItemSchema))
|
||||
async createItem(@Body() createItemDto: CreateItemDto): Promise<number> {
|
||||
return this.itemsApplication.createItem(createItemDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete item.
|
||||
* @param id - The item id.
|
||||
*/
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Delete the given item (product or service).' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The item has been successfully deleted.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The item not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The item id',
|
||||
})
|
||||
async deleteItem(@Param('id') id: string): Promise<void> {
|
||||
const itemId = parseInt(id, 10);
|
||||
return this.itemsApplication.deleteItem(itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inactivate item.
|
||||
* @param id - The item id.
|
||||
*/
|
||||
@Patch(':id/inactivate')
|
||||
@ApiOperation({ summary: 'Inactivate the given item (product or service).' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The item has been successfully inactivated.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The item not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The item id',
|
||||
})
|
||||
async inactivateItem(@Param('id') id: string): Promise<void> {
|
||||
const itemId = parseInt(id, 10);
|
||||
return this.itemsApplication.inactivateItem(itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate item.
|
||||
* @param id - The item id.
|
||||
*/
|
||||
@Patch(':id/activate')
|
||||
@ApiOperation({ summary: 'Activate the given item (product or service).' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The item has been successfully activated.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The item not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The item id',
|
||||
})
|
||||
async activateItem(@Param('id') id: string): Promise<void> {
|
||||
const itemId = parseInt(id, 10);
|
||||
return this.itemsApplication.activateItem(itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get item.
|
||||
* @param id - The item id.
|
||||
*/
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get the given item (product or service).' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'The item has been successfully retrieved.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The item not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The item id',
|
||||
})
|
||||
async getItem(@Param('id') id: string): Promise<any> {
|
||||
const itemId = parseInt(id, 10);
|
||||
return this.itemsApplication.getItem(itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the item associated invoices transactions.
|
||||
* @param {string} id
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
@Get(':id/invoices')
|
||||
@ApiOperation({
|
||||
summary: 'Retrieves the item associated invoices transactions.',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description:
|
||||
'The item associated invoices transactions have been successfully retrieved.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The item not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The item id',
|
||||
})
|
||||
async getItemInvoicesTransactions(@Param('id') id: string): Promise<any> {
|
||||
const itemId = parseInt(id, 10);
|
||||
return this.itemsApplication.getItemInvoicesTransactions(itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the item associated bills transactions.
|
||||
* @param {string} id
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
@Get(':id/bills')
|
||||
@ApiOperation({
|
||||
summary: 'Retrieves the item associated bills transactions.',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description:
|
||||
'The item associated bills transactions have been successfully retrieved.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The item not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The item id',
|
||||
})
|
||||
async getItemBillTransactions(@Param('id') id: string): Promise<any> {
|
||||
const itemId = parseInt(id, 10);
|
||||
return this.itemsApplication.getItemBillTransactions(itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the item associated estimates transactions.
|
||||
* @param {string} id
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
@Get(':id/estimates')
|
||||
@ApiOperation({
|
||||
summary: 'Retrieves the item associated estimates transactions.',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description:
|
||||
'The item associated estimate transactions have been successfully retrieved.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The item not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The item id',
|
||||
})
|
||||
async getItemEstimatesTransactions(@Param('id') id: string): Promise<any> {
|
||||
const itemId = parseInt(id, 10);
|
||||
return this.itemsApplication.getItemEstimatesTransactions(itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the item associated receipts transactions.
|
||||
* @param {string} id
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
@Get(':id/receipts')
|
||||
@ApiOperation({
|
||||
summary: 'Retrieves the item associated receipts transactions.',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description:
|
||||
'The item associated receipts transactions have been successfully retrieved.',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'The item not found.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: Number,
|
||||
description: 'The item id',
|
||||
})
|
||||
async getItemReceiptTransactions(@Param('id') id: string): Promise<any> {
|
||||
const itemId = parseInt(id, 10);
|
||||
return this.itemsApplication.getItemReceiptsTransactions(itemId);
|
||||
}
|
||||
}
|
||||
112
packages/server/src/modules/Items/Item.schema.ts
Normal file
112
packages/server/src/modules/Items/Item.schema.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { DATATYPES_LENGTH } from '@/constants/data-types';
|
||||
import z from 'zod';
|
||||
|
||||
export const createItemSchema = z
|
||||
.object({
|
||||
name: z.string().max(DATATYPES_LENGTH.STRING),
|
||||
type: z.enum(['service', 'non-inventory', 'inventory']),
|
||||
code: z.string().max(DATATYPES_LENGTH.STRING).nullable().optional(),
|
||||
purchasable: z.boolean().optional(),
|
||||
cost_price: z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(DATATYPES_LENGTH.DECIMAL_13_3)
|
||||
.nullable()
|
||||
.optional(),
|
||||
cost_account_id: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(DATATYPES_LENGTH.INT_10)
|
||||
.nullable()
|
||||
.optional(),
|
||||
sellable: z.boolean().optional(),
|
||||
sell_price: z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(DATATYPES_LENGTH.DECIMAL_13_3)
|
||||
.nullable()
|
||||
.optional(),
|
||||
sell_account_id: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(DATATYPES_LENGTH.INT_10)
|
||||
.nullable()
|
||||
.optional(),
|
||||
inventory_account_id: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(DATATYPES_LENGTH.INT_10)
|
||||
.nullable()
|
||||
.optional(),
|
||||
sell_description: z
|
||||
.string()
|
||||
.max(DATATYPES_LENGTH.TEXT)
|
||||
.nullable()
|
||||
.optional(),
|
||||
purchase_description: z
|
||||
.string()
|
||||
.max(DATATYPES_LENGTH.TEXT)
|
||||
.nullable()
|
||||
.optional(),
|
||||
sell_tax_rate_id: z.number().int().nullable().optional(),
|
||||
purchase_tax_rate_id: z.number().int().nullable().optional(),
|
||||
category_id: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(DATATYPES_LENGTH.INT_10)
|
||||
.nullable()
|
||||
.optional(),
|
||||
note: z.string().max(DATATYPES_LENGTH.TEXT).optional(),
|
||||
active: z.boolean().optional(),
|
||||
media_ids: z.array(z.number().int()).optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.purchasable) {
|
||||
return (
|
||||
data.cost_price !== undefined && data.cost_account_id !== undefined
|
||||
);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message:
|
||||
'Cost price and cost account ID are required when item is purchasable',
|
||||
path: ['cost_price', 'cost_account_id'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.sellable) {
|
||||
return (
|
||||
data.sell_price !== undefined && data.sell_account_id !== undefined
|
||||
);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message:
|
||||
'Sell price and sell account ID are required when item is sellable',
|
||||
path: ['sell_price', 'sell_account_id'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.type === 'inventory') {
|
||||
return data.inventory_account_id !== undefined;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: 'Inventory account ID is required for inventory items',
|
||||
path: ['inventory_account_id'],
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
export type createItemDTO = z.infer<typeof createItemSchema>;
|
||||
export type editItemDTOSchema = z.infer<typeof createItemSchema>;
|
||||
62
packages/server/src/modules/Items/Item.transformer.ts
Normal file
62
packages/server/src/modules/Items/Item.transformer.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Transformer } from '../Transformer/Transformer';
|
||||
import { Item } from './models/Item';
|
||||
// import { GetItemWarehouseTransformer } from '@/services/Warehouses/Items/GettItemWarehouseTransformer';
|
||||
|
||||
export class ItemTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale invoice object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'typeFormatted',
|
||||
'sellPriceFormatted',
|
||||
'costPriceFormatted',
|
||||
'itemWarehouses',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatted item type.
|
||||
* @param {IItem} item
|
||||
* @returns {string}
|
||||
*/
|
||||
public typeFormatted(item: Item): string {
|
||||
return this.context.i18n.t(`item.field.type.${item.type}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatted sell price.
|
||||
* @param item
|
||||
* @returns {string}
|
||||
*/
|
||||
public sellPriceFormatted(item: Item): string {
|
||||
return this.formatNumber(item.sellPrice, {
|
||||
currencyCode: this.context.organization.baseCurrency,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatted cost price.
|
||||
* @param item
|
||||
* @returns {string}
|
||||
*/
|
||||
public costPriceFormatted(item: Item): string {
|
||||
return this.formatNumber(item.costPrice, {
|
||||
currencyCode: this.context.organization.baseCurrency,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Associate the item warehouses quantity.
|
||||
* @param item
|
||||
* @returns
|
||||
*/
|
||||
// public itemWarehouses = (item) => {
|
||||
// return this.item(
|
||||
// item.itemWarehouses,
|
||||
// new GetItemWarehouseTransformer(),
|
||||
// {},
|
||||
// );
|
||||
// };
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Transformer } from '../Transformer/Transformer';
|
||||
|
||||
export class ItemBillTransactionTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale invoice object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'formattedAmount',
|
||||
'formattedBillDate',
|
||||
'formattedRate',
|
||||
'formattedCost',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatted sell price.
|
||||
* @param item
|
||||
* @returns {string}
|
||||
*/
|
||||
public formattedAmount(item): string {
|
||||
return this.formatNumber(item.amount, {
|
||||
currencyCode: item.bill.currencyCode,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatted bill date.
|
||||
* @param item
|
||||
* @returns {string}
|
||||
*/
|
||||
public formattedBillDate = (entry): string => {
|
||||
return this.formatDate(entry.bill.billDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatted quantity.
|
||||
* @returns {string}
|
||||
*/
|
||||
public formattedQuantity = (entry): string => {
|
||||
return entry.quantity;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatted rate.
|
||||
* @param entry
|
||||
* @returns {string}
|
||||
*/
|
||||
public formattedRate = (entry): string => {
|
||||
return this.formatNumber(entry.rate, {
|
||||
currencyCode: entry.bill.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatted bill due date.
|
||||
* @param entry
|
||||
* @returns
|
||||
*/
|
||||
public formattedBillDueDate = (entry): string => {
|
||||
return this.formatDate(entry.bill.dueDate);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param entry
|
||||
* @returns
|
||||
*/
|
||||
public transform = (entry) => {
|
||||
return {
|
||||
billId: entry.bill.id,
|
||||
|
||||
billNumber: entry.bill.billNumber,
|
||||
referenceNumber: entry.bill.referenceNo,
|
||||
|
||||
billDate: entry.bill.billDate,
|
||||
formattedBillDate: entry.formattedBillDate,
|
||||
|
||||
billDueDate: entry.bill.dueDate,
|
||||
formattedBillDueDate: entry.formattedBillDueDate,
|
||||
|
||||
amount: entry.amount,
|
||||
formattedAmount: entry.formattedAmount,
|
||||
|
||||
quantity: entry.quantity,
|
||||
formattedQuantity: entry.formattedQuantity,
|
||||
|
||||
rate: entry.rate,
|
||||
formattedRate: entry.formattedRate,
|
||||
|
||||
vendorDisplayName: entry.bill.vendor.displayName,
|
||||
vendorCurrencyCode: entry.bill.vendor.currencyCode,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Transformer } from '../Transformer/Transformer';
|
||||
|
||||
export class ItemEstimateTransactionTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale invoice object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'formattedAmount',
|
||||
'formattedEstimateDate',
|
||||
'formattedRate',
|
||||
'formattedCost',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatted sell price.
|
||||
* @returns {string}
|
||||
*/
|
||||
public formattedAmount(item): string {
|
||||
return this.formatNumber(item.amount, {
|
||||
currencyCode: item.estimate.currencyCode,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatted estimate date.
|
||||
* @returns {string}
|
||||
*/
|
||||
public formattedEstimateDate = (entry): string => {
|
||||
return this.formatDate(entry.estimate.estimateDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatted quantity.
|
||||
* @returns {string}
|
||||
*/
|
||||
public formattedQuantity = (entry): string => {
|
||||
return entry.quantity;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatted rate.
|
||||
* @returns {string}
|
||||
*/
|
||||
public formattedRate = (entry): string => {
|
||||
return this.formatNumber(entry.rate, {
|
||||
currencyCode: entry.estimate.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform the entry.
|
||||
* @param {any} entry
|
||||
* @returns {any}
|
||||
*/
|
||||
public transform = (entry) => {
|
||||
return {
|
||||
estimateId: entry.estimate.id,
|
||||
|
||||
estimateNumber: entry.estimate.estimateNumber,
|
||||
referenceNumber: entry.estimate.referenceNo,
|
||||
|
||||
estimateDate: entry.estimate.estimateDate,
|
||||
formattedEstimateDate: entry.formattedEstimateDate,
|
||||
|
||||
amount: entry.amount,
|
||||
formattedAmount: entry.formattedAmount,
|
||||
|
||||
quantity: entry.quantity,
|
||||
formattedQuantity: entry.formattedQuantity,
|
||||
|
||||
rate: entry.rate,
|
||||
formattedRate: entry.formattedRate,
|
||||
|
||||
customerDisplayName: entry.estimate.customer.displayName,
|
||||
customerCurrencyCode: entry.estimate.customer.currencyCode,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Transformer } from '../Transformer/Transformer';
|
||||
|
||||
export class ItemInvoicesTransactionsTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale invoice object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'formattedAmount',
|
||||
'formattedInvoiceDate',
|
||||
'formattedRate',
|
||||
'formattedCost',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatted sell price.
|
||||
* @param item
|
||||
* @returns {string}
|
||||
*/
|
||||
public formattedAmount(item): string {
|
||||
return this.formatNumber(item.amount, {
|
||||
currencyCode: item.invoice.currencyCode,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatted invoice date.
|
||||
* @param item
|
||||
* @returns
|
||||
*/
|
||||
public formattedInvoiceDate = (entry): string => {
|
||||
return this.formatDate(entry.invoice.invoiceDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatted item quantity.
|
||||
* @returns {string}
|
||||
*/
|
||||
public formattedQuantity = (entry): string => {
|
||||
return entry.quantity;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatted date.
|
||||
* @param entry
|
||||
* @returns {string}
|
||||
*/
|
||||
public formattedRate = (entry): string => {
|
||||
return this.formatNumber(entry.rate, {
|
||||
currencyCode: entry.invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {any} entry
|
||||
* @returns {any}
|
||||
*/
|
||||
public transform = (entry) => {
|
||||
return {
|
||||
invoiceId: entry.invoice.id,
|
||||
|
||||
invoiceNumber: entry.invoice.invoiceNo,
|
||||
referenceNumber: entry.invoice.referenceNo,
|
||||
|
||||
invoiceDate: entry.invoice.invoiceDate,
|
||||
formattedInvoiceDate: entry.formattedInvoiceDate,
|
||||
|
||||
amount: entry.amount,
|
||||
formattedAmount: entry.formattedAmount,
|
||||
|
||||
quantity: entry.quantity,
|
||||
formattedQuantity: entry.formattedQuantity,
|
||||
|
||||
rate: entry.rate,
|
||||
formattedRate: entry.formattedRate,
|
||||
|
||||
customerDisplayName: entry.invoice.customer.displayName,
|
||||
customerCurrencyCode: entry.invoice.customer.currencyCode,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Transformer } from '../Transformer/Transformer';
|
||||
|
||||
export class ItemReceiptTransactionTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale invoice object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'formattedAmount',
|
||||
'formattedReceiptDate',
|
||||
'formattedRate',
|
||||
'formattedCost',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatted sell price.
|
||||
* @param item
|
||||
* @returns {string}
|
||||
*/
|
||||
public formattedAmount(item): string {
|
||||
return this.formatNumber(item.amount, {
|
||||
currencyCode: item.receipt.currencyCode,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatted receipt date.
|
||||
* @param {any} entry
|
||||
* @returns {string}
|
||||
*/
|
||||
public formattedReceiptDate = (entry): string => {
|
||||
return this.formatDate(entry.receipt.receiptDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatted quantity.
|
||||
* @param {any} entry
|
||||
* @returns {string}
|
||||
*/
|
||||
public formattedQuantity = (entry): string => {
|
||||
return entry.quantity;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatted rate.
|
||||
* @param {any} entry
|
||||
* @returns {string}
|
||||
*/
|
||||
public formattedRate = (entry): string => {
|
||||
return this.formatNumber(entry.rate, {
|
||||
currencyCode: entry.receipt.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform the entry.
|
||||
* @param {any} entry
|
||||
* @returns {any}
|
||||
*/
|
||||
public transform = (entry) => {
|
||||
return {
|
||||
receiptId: entry.receipt.id,
|
||||
|
||||
receipNumber: entry.receipt.receiptNumber,
|
||||
referenceNumber: entry.receipt.referenceNo,
|
||||
|
||||
receiptDate: entry.receipt.receiptDate,
|
||||
formattedReceiptDate: entry.formattedReceiptDate,
|
||||
|
||||
amount: entry.amount,
|
||||
formattedAmount: entry.formattedAmount,
|
||||
|
||||
quantity: entry.quantity,
|
||||
formattedQuantity: entry.formattedQuantity,
|
||||
|
||||
rate: entry.rate,
|
||||
formattedRate: entry.formattedRate,
|
||||
|
||||
customerDisplayName: entry.receipt.customer.displayName,
|
||||
customerCurrencyCode: entry.receipt.customer.currencyCode,
|
||||
};
|
||||
};
|
||||
}
|
||||
113
packages/server/src/modules/Items/ItemTransactions.service.ts
Normal file
113
packages/server/src/modules/Items/ItemTransactions.service.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ItemInvoicesTransactionsTransformer } from './ItemInvoicesTransactions.transformer';
|
||||
import { ItemEstimateTransactionTransformer } from './ItemEstimatesTransaction.transformer';
|
||||
import { ItemBillTransactionTransformer } from './ItemBillsTransactions.transformer';
|
||||
import { ItemReceiptTransactionTransformer } from './ItemReceiptsTransactions.transformer';
|
||||
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
||||
import { ItemEntry } from '../TransactionItemEntry/models/ItemEntry';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class ItemTransactionsService {
|
||||
constructor(
|
||||
private transformer: TransformerInjectable,
|
||||
|
||||
@Inject(ItemEntry.name)
|
||||
private readonly itemEntry: TenantModelProxy<typeof ItemEntry>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the item associated invoices transactions.
|
||||
* @param {number} itemId -
|
||||
*/
|
||||
public async getItemInvoicesTransactions(itemId: number) {
|
||||
const invoiceEntries = await this.itemEntry()
|
||||
.query()
|
||||
.where('itemId', itemId)
|
||||
.where('referenceType', 'SaleInvoice')
|
||||
.withGraphJoined('invoice.customer(selectCustomerColumns)')
|
||||
.orderBy('invoice:invoiceDate', 'ASC')
|
||||
.modifiers({
|
||||
selectCustomerColumns: (builder) => {
|
||||
builder.select('displayName', 'currencyCode', 'id');
|
||||
},
|
||||
});
|
||||
// Retrieves the transformed invoice entries.
|
||||
return this.transformer.transform(
|
||||
invoiceEntries,
|
||||
new ItemInvoicesTransactionsTransformer(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the item associated invoices transactions.
|
||||
* @param {number} itemId
|
||||
* @returns
|
||||
*/
|
||||
public async getItemBillTransactions(itemId: number) {
|
||||
const billEntries = await this.itemEntry()
|
||||
.query()
|
||||
.where('itemId', itemId)
|
||||
.where('referenceType', 'Bill')
|
||||
.withGraphJoined('bill.vendor(selectVendorColumns)')
|
||||
.orderBy('bill:billDate', 'ASC')
|
||||
.modifiers({
|
||||
selectVendorColumns: (builder) => {
|
||||
builder.select('displayName', 'currencyCode', 'id');
|
||||
},
|
||||
});
|
||||
// Retrieves the transformed bill entries.
|
||||
return this.transformer.transform(
|
||||
billEntries,
|
||||
new ItemBillTransactionTransformer(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the item associated estimates transactions.
|
||||
* @param {number} itemId
|
||||
* @returns
|
||||
*/
|
||||
public async getItemEstimateTransactions(itemId: number) {
|
||||
const estimatesEntries = await this.itemEntry()
|
||||
.query()
|
||||
.where('itemId', itemId)
|
||||
.where('referenceType', 'SaleEstimate')
|
||||
.withGraphJoined('estimate.customer(selectCustomerColumns)')
|
||||
.orderBy('estimate:estimateDate', 'ASC')
|
||||
.modifiers({
|
||||
selectCustomerColumns: (builder) => {
|
||||
builder.select('displayName', 'currencyCode', 'id');
|
||||
},
|
||||
});
|
||||
// Retrieves the transformed estimates entries.
|
||||
return this.transformer.transform(
|
||||
estimatesEntries,
|
||||
new ItemEstimateTransactionTransformer(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the item associated receipts transactions.
|
||||
* @param {number} itemId
|
||||
* @returns
|
||||
*/
|
||||
public async getItemReceiptTransactions(itemId: number) {
|
||||
const receiptsEntries = await this.itemEntry()
|
||||
.query()
|
||||
.where('itemId', itemId)
|
||||
.where('referenceType', 'SaleReceipt')
|
||||
.withGraphJoined('receipt.customer(selectCustomerColumns)')
|
||||
.orderBy('receipt:receiptDate', 'ASC')
|
||||
.modifiers({
|
||||
selectCustomerColumns: (builder) => {
|
||||
builder.select('displayName', 'currencyCode', 'id');
|
||||
},
|
||||
});
|
||||
// Retrieves the transformed receipts entries.
|
||||
return this.transformer.transform(
|
||||
receiptsEntries,
|
||||
new ItemReceiptTransactionTransformer(),
|
||||
);
|
||||
}
|
||||
}
|
||||
321
packages/server/src/modules/Items/ItemValidator.service.ts
Normal file
321
packages/server/src/modules/Items/ItemValidator.service.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
ACCOUNT_PARENT_TYPE,
|
||||
ACCOUNT_ROOT_TYPE,
|
||||
ACCOUNT_TYPE,
|
||||
} from '@/constants/accounts';
|
||||
import { ServiceError } from './ServiceError';
|
||||
import { ERRORS } from './Items.constants';
|
||||
import { Item } from './models/Item';
|
||||
import { Account } from '../Accounts/models/Account.model';
|
||||
import { TaxRateModel } from '../TaxRates/models/TaxRate.model';
|
||||
import { ItemEntry } from '../TransactionItemEntry/models/ItemEntry';
|
||||
import { ItemCategory } from '../ItemCategories/models/ItemCategory.model';
|
||||
import { AccountTransaction } from '../Accounts/models/AccountTransaction.model';
|
||||
import { InventoryAdjustment } from '../InventoryAdjutments/models/InventoryAdjustment';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
import { CreateItemDto, EditItemDto } from './dtos/Item.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ItemsValidators {
|
||||
/**
|
||||
* @param {typeof Item} itemModel - The Item model.
|
||||
* @param {typeof Account} accountModel - The Account model.
|
||||
* @param {typeof TaxRateModel} taxRateModel - The TaxRateModel model.
|
||||
* @param {typeof ItemEntry} itemEntryModel - The ItemEntry model.
|
||||
* @param {typeof ItemCategory} itemCategoryModel - The ItemCategory model.
|
||||
* @param {typeof AccountTransaction} accountTransactionModel - The AccountTransaction model.
|
||||
* @param {typeof InventoryAdjustment} inventoryAdjustmentEntryModel - The InventoryAdjustment model.
|
||||
*/
|
||||
constructor(
|
||||
@Inject(Item.name) private itemModel: TenantModelProxy<typeof Item>,
|
||||
|
||||
@Inject(Account.name)
|
||||
private accountModel: TenantModelProxy<typeof Account>,
|
||||
|
||||
@Inject(TaxRateModel.name)
|
||||
private taxRateModel: TenantModelProxy<typeof TaxRateModel>,
|
||||
|
||||
@Inject(ItemEntry.name)
|
||||
private itemEntryModel: TenantModelProxy<typeof ItemEntry>,
|
||||
|
||||
@Inject(ItemCategory.name)
|
||||
private itemCategoryModel: TenantModelProxy<typeof ItemCategory>,
|
||||
|
||||
@Inject(AccountTransaction.name)
|
||||
private accountTransactionModel: TenantModelProxy<
|
||||
typeof AccountTransaction
|
||||
>,
|
||||
|
||||
@Inject(InventoryAdjustment.name)
|
||||
private inventoryAdjustmentEntryModel: TenantModelProxy<
|
||||
typeof InventoryAdjustment
|
||||
>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validate wether the given item name already exists on the storage.
|
||||
* @param {string} itemName
|
||||
* @param {number} notItemId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async validateItemNameUniquiness(
|
||||
itemName: string,
|
||||
notItemId?: number,
|
||||
): Promise<void> {
|
||||
const foundItems = await this.itemModel()
|
||||
.query()
|
||||
.onBuild((builder: any) => {
|
||||
builder.where('name', itemName);
|
||||
if (notItemId) {
|
||||
builder.whereNot('id', notItemId);
|
||||
}
|
||||
});
|
||||
|
||||
if (foundItems.length > 0) {
|
||||
throw new ServiceError(
|
||||
ERRORS.ITEM_NAME_EXISTS,
|
||||
'The item name is already exist.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate item COGS account existance and type.
|
||||
* @param {number} costAccountId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async validateItemCostAccountExistance(
|
||||
costAccountId: number,
|
||||
): Promise<void> {
|
||||
const foundAccount = await this.accountModel()
|
||||
.query()
|
||||
.findById(costAccountId);
|
||||
|
||||
if (!foundAccount) {
|
||||
throw new ServiceError(ERRORS.COST_ACCOUNT_NOT_FOUMD);
|
||||
|
||||
// Detarmines the cost of goods sold account.
|
||||
} else if (!foundAccount.isParentType(ACCOUNT_PARENT_TYPE.EXPENSE)) {
|
||||
throw new ServiceError(ERRORS.COST_ACCOUNT_NOT_COGS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate item sell account existance and type.
|
||||
* @param {number} sellAccountId - Sell account id.
|
||||
*/
|
||||
public async validateItemSellAccountExistance(sellAccountId: number) {
|
||||
const foundAccount = await this.accountModel()
|
||||
.query()
|
||||
.findById(sellAccountId);
|
||||
|
||||
if (!foundAccount) {
|
||||
throw new ServiceError(ERRORS.SELL_ACCOUNT_NOT_FOUND);
|
||||
} else if (!foundAccount.isParentType(ACCOUNT_ROOT_TYPE.INCOME)) {
|
||||
throw new ServiceError(ERRORS.SELL_ACCOUNT_NOT_INCOME);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates income account existance.
|
||||
* @param {number|null} sellable - Detarmines if the item sellable.
|
||||
* @param {number|null} incomeAccountId - Income account id.
|
||||
* @throws {ServiceError(ERRORS.INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM)}
|
||||
*/
|
||||
public validateIncomeAccountExistance(
|
||||
sellable?: boolean,
|
||||
incomeAccountId?: number,
|
||||
) {
|
||||
if (sellable && !incomeAccountId) {
|
||||
throw new ServiceError(
|
||||
ERRORS.INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM,
|
||||
'Income account is require with sellable item.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the cost account existance.
|
||||
* @param {boolean|null} purchasable - Detarmines if the item purchasble.
|
||||
* @param {number|null} costAccountId - Cost account id.
|
||||
* @throws {ServiceError(ERRORS.COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM)}
|
||||
*/
|
||||
public validateCostAccountExistance(
|
||||
purchasable: boolean,
|
||||
costAccountId?: number,
|
||||
) {
|
||||
if (purchasable && !costAccountId) {
|
||||
throw new ServiceError(
|
||||
ERRORS.COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM,
|
||||
'The cost account is required with purchasable item.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate item inventory account existance and type.
|
||||
* @param {number} inventoryAccountId
|
||||
*/
|
||||
public async validateItemInventoryAccountExistance(
|
||||
inventoryAccountId: number,
|
||||
) {
|
||||
const foundAccount = await this.accountModel()
|
||||
.query()
|
||||
.findById(inventoryAccountId);
|
||||
|
||||
if (!foundAccount) {
|
||||
throw new ServiceError(ERRORS.INVENTORY_ACCOUNT_NOT_FOUND);
|
||||
} else if (!foundAccount.isAccountType(ACCOUNT_TYPE.INVENTORY)) {
|
||||
throw new ServiceError(ERRORS.INVENTORY_ACCOUNT_NOT_INVENTORY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate item category existance.
|
||||
* @param {number} itemCategoryId
|
||||
*/
|
||||
public async validateItemCategoryExistance(itemCategoryId: number) {
|
||||
const foundCategory = await this.itemCategoryModel()
|
||||
.query()
|
||||
.findById(itemCategoryId);
|
||||
|
||||
if (!foundCategory) {
|
||||
throw new ServiceError(ERRORS.ITEM_CATEOGRY_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given item or items have no associated invoices or bills.
|
||||
* @param {number|number[]} itemId - Item id.
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
public async validateHasNoInvoicesOrBills(itemId: number[] | number) {
|
||||
const ids = Array.isArray(itemId) ? itemId : [itemId];
|
||||
const foundItemEntries = await this.itemEntryModel()
|
||||
.query()
|
||||
.whereIn('item_id', ids);
|
||||
|
||||
if (foundItemEntries.length > 0) {
|
||||
throw new ServiceError(
|
||||
ids.length > 1
|
||||
? ERRORS.ITEMS_HAVE_ASSOCIATED_TRANSACTIONS
|
||||
: ERRORS.ITEM_HAS_ASSOCIATED_TRANSACTINS,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given item has no associated inventory adjustment transactions.
|
||||
* @param {number} itemId -
|
||||
*/
|
||||
public async validateHasNoInventoryAdjustments(
|
||||
itemId: number[] | number,
|
||||
): Promise<void> {
|
||||
const itemsIds = Array.isArray(itemId) ? itemId : [itemId];
|
||||
const inventoryAdjEntries = await this.inventoryAdjustmentEntryModel()
|
||||
.query()
|
||||
.whereIn('item_id', itemsIds);
|
||||
|
||||
if (inventoryAdjEntries.length > 0) {
|
||||
throw new ServiceError(ERRORS.ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates edit item type from service/non-inventory to inventory.
|
||||
* Should item has no any relations with accounts transactions.
|
||||
* @param {number} itemId - Item id.
|
||||
*/
|
||||
public async validateEditItemTypeToInventory(
|
||||
oldItem: Item,
|
||||
newItemDTO: CreateItemDto | EditItemDto,
|
||||
) {
|
||||
// We have no problem in case the item type not modified.
|
||||
if (newItemDTO.type === oldItem.type || oldItem.type === 'inventory') {
|
||||
return;
|
||||
}
|
||||
// Retrieve all transactions that associated to the given item id.
|
||||
const itemTransactionsCount = await this.accountTransactionModel()
|
||||
.query()
|
||||
.where('item_id', oldItem.id)
|
||||
.count('item_id', { as: 'transactions' })
|
||||
.first();
|
||||
|
||||
// @ts-ignore
|
||||
if (itemTransactionsCount.transactions > 0) {
|
||||
throw new ServiceError(
|
||||
ERRORS.TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the item inventory account whether modified and item
|
||||
* has associated inventory transactions.
|
||||
* @param {Item} oldItem - Old item.
|
||||
* @param {CreateItemDto | EditItemDto} newItemDTO - New item DTO.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async validateItemInvnetoryAccountModified(
|
||||
oldItem: Item,
|
||||
newItemDTO: CreateItemDto | EditItemDto,
|
||||
) {
|
||||
if (
|
||||
newItemDTO.type !== 'inventory' ||
|
||||
oldItem.inventoryAccountId === newItemDTO.inventoryAccountId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Inventory transactions associated to the given item id.
|
||||
const transactions = await this.accountTransactionModel().query().where({
|
||||
itemId: oldItem.id,
|
||||
});
|
||||
// Throw the service error in case item has associated inventory transactions.
|
||||
if (transactions.length > 0) {
|
||||
throw new ServiceError(ERRORS.INVENTORY_ACCOUNT_CANNOT_MODIFIED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate edit item type from inventory to another type that not allowed.
|
||||
* @param {CreateItemDto | EditItemDto} itemDTO - Item DTO.
|
||||
* @param {IItem} oldItem - Old item.
|
||||
*/
|
||||
public validateEditItemFromInventory(
|
||||
itemDTO: CreateItemDto | EditItemDto,
|
||||
oldItem: Item,
|
||||
) {
|
||||
if (
|
||||
itemDTO.type &&
|
||||
oldItem.type === 'inventory' &&
|
||||
itemDTO.type !== oldItem.type
|
||||
) {
|
||||
throw new ServiceError(ERRORS.ITEM_CANNOT_CHANGE_INVENTORY_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the purchase tax rate id existance.
|
||||
* @param {number} taxRateId - Tax rate id.
|
||||
*/
|
||||
public async validatePurchaseTaxRateExistance(taxRateId: number) {
|
||||
const foundTaxRate = await this.taxRateModel().query().findById(taxRateId);
|
||||
|
||||
if (!foundTaxRate) {
|
||||
throw new ServiceError(ERRORS.PURCHASE_TAX_RATE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the sell tax rate id existance.
|
||||
* @param {number} taxRateId - Tax rate id.
|
||||
*/
|
||||
public async validateSellTaxRateExistance(taxRateId: number) {
|
||||
const foundTaxRate = await this.taxRateModel().query().findById(taxRateId);
|
||||
|
||||
if (!foundTaxRate) {
|
||||
throw new ServiceError(ERRORS.SELL_TAX_RATE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
141
packages/server/src/modules/Items/Items.constants.ts
Normal file
141
packages/server/src/modules/Items/Items.constants.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
export const ERRORS = {
|
||||
NOT_FOUND: 'NOT_FOUND',
|
||||
ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND',
|
||||
|
||||
ITEM_NAME_EXISTS: 'ITEM_NAME_EXISTS',
|
||||
ITEM_CATEOGRY_NOT_FOUND: 'ITEM_CATEOGRY_NOT_FOUND',
|
||||
COST_ACCOUNT_NOT_COGS: 'COST_ACCOUNT_NOT_COGS',
|
||||
COST_ACCOUNT_NOT_FOUMD: 'COST_ACCOUNT_NOT_FOUMD',
|
||||
SELL_ACCOUNT_NOT_FOUND: 'SELL_ACCOUNT_NOT_FOUND',
|
||||
SELL_ACCOUNT_NOT_INCOME: 'SELL_ACCOUNT_NOT_INCOME',
|
||||
|
||||
INVENTORY_ACCOUNT_NOT_FOUND: 'INVENTORY_ACCOUNT_NOT_FOUND',
|
||||
INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY',
|
||||
|
||||
ITEMS_HAVE_ASSOCIATED_TRANSACTIONS: 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS',
|
||||
ITEM_HAS_ASSOCIATED_TRANSACTINS: 'ITEM_HAS_ASSOCIATED_TRANSACTINS',
|
||||
|
||||
ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT:
|
||||
'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT',
|
||||
ITEM_CANNOT_CHANGE_INVENTORY_TYPE: 'ITEM_CANNOT_CHANGE_INVENTORY_TYPE',
|
||||
TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS:
|
||||
'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
|
||||
INVENTORY_ACCOUNT_CANNOT_MODIFIED: 'INVENTORY_ACCOUNT_CANNOT_MODIFIED',
|
||||
|
||||
ITEM_HAS_ASSOCIATED_TRANSACTIONS: 'ITEM_HAS_ASSOCIATED_TRANSACTIONS',
|
||||
|
||||
PURCHASE_TAX_RATE_NOT_FOUND: 'PURCHASE_TAX_RATE_NOT_FOUND',
|
||||
SELL_TAX_RATE_NOT_FOUND: 'SELL_TAX_RATE_NOT_FOUND',
|
||||
|
||||
INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM:
|
||||
'INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM',
|
||||
COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM:
|
||||
'COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM',
|
||||
};
|
||||
|
||||
export const DEFAULT_VIEW_COLUMNS = [];
|
||||
export const DEFAULT_VIEWS = [
|
||||
{
|
||||
name: 'Services',
|
||||
slug: 'services',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{ index: 1, fieldKey: 'type', comparator: 'equals', value: 'service' },
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Inventory',
|
||||
slug: 'inventory',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{ index: 1, fieldKey: 'type', comparator: 'equals', value: 'inventory' },
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Non Inventory',
|
||||
slug: 'non-inventory',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
index: 1,
|
||||
fieldKey: 'type',
|
||||
comparator: 'equals',
|
||||
value: 'non-inventory',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
];
|
||||
|
||||
export const ItemsSampleData = [
|
||||
{
|
||||
'Item Type': 'Inventory',
|
||||
'Item Name': 'Hettinger, Schumm and Bartoletti',
|
||||
'Item Code': '1000',
|
||||
Sellable: 'T',
|
||||
Purchasable: 'T',
|
||||
'Cost Price': '10000',
|
||||
'Sell Price': '1000',
|
||||
'Cost Account': 'Cost of Goods Sold',
|
||||
'Sell Account': 'Other Income',
|
||||
'Inventory Account': 'Inventory Asset',
|
||||
'Sell Description': 'Description ....',
|
||||
'Purchase Description': 'Description ....',
|
||||
Category: 'sdafasdfsadf',
|
||||
Note: 'At dolor est non tempore et quisquam.',
|
||||
Active: 'TRUE',
|
||||
},
|
||||
{
|
||||
'Item Type': 'Inventory',
|
||||
'Item Name': 'Schmitt Group',
|
||||
'Item Code': '1001',
|
||||
Sellable: 'T',
|
||||
Purchasable: 'T',
|
||||
'Cost Price': '10000',
|
||||
'Sell Price': '1000',
|
||||
'Cost Account': 'Cost of Goods Sold',
|
||||
'Sell Account': 'Other Income',
|
||||
'Inventory Account': 'Inventory Asset',
|
||||
'Sell Description': 'Description ....',
|
||||
'Purchase Description': 'Description ....',
|
||||
Category: 'sdafasdfsadf',
|
||||
Note: 'Id perspiciatis at adipisci minus accusamus dolor iure dolore.',
|
||||
Active: 'TRUE',
|
||||
},
|
||||
{
|
||||
'Item Type': 'Inventory',
|
||||
'Item Name': 'Marks - Carroll',
|
||||
'Item Code': '1002',
|
||||
Sellable: 'T',
|
||||
Purchasable: 'T',
|
||||
'Cost Price': '10000',
|
||||
'Sell Price': '1000',
|
||||
'Cost Account': 'Cost of Goods Sold',
|
||||
'Sell Account': 'Other Income',
|
||||
'Inventory Account': 'Inventory Asset',
|
||||
'Sell Description': 'Description ....',
|
||||
'Purchase Description': 'Description ....',
|
||||
Category: 'sdafasdfsadf',
|
||||
Note: 'Odio odio minus similique.',
|
||||
Active: 'TRUE',
|
||||
},
|
||||
{
|
||||
'Item Type': 'Inventory',
|
||||
'Item Name': 'VonRueden, Ruecker and Hettinger',
|
||||
'Item Code': '1003',
|
||||
Sellable: 'T',
|
||||
Purchasable: 'T',
|
||||
'Cost Price': '10000',
|
||||
'Sell Price': '1000',
|
||||
'Cost Account': 'Cost of Goods Sold',
|
||||
'Sell Account': 'Other Income',
|
||||
'Inventory Account': 'Inventory Asset',
|
||||
'Sell Description': 'Description ....',
|
||||
'Purchase Description': 'Description ....',
|
||||
Category: 'sdafasdfsadf',
|
||||
Note: 'Quibusdam dolores illo.',
|
||||
Active: 'TRUE',
|
||||
},
|
||||
];
|
||||
44
packages/server/src/modules/Items/Items.module.ts
Normal file
44
packages/server/src/modules/Items/Items.module.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ItemsController } from './Item.controller';
|
||||
import { CreateItemService } from './CreateItem.service';
|
||||
import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module';
|
||||
import { ItemsValidators } from './ItemValidator.service';
|
||||
import { DeleteItemService } from './DeleteItem.service';
|
||||
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
||||
import { EditItemService } from './EditItem.service';
|
||||
import { InactivateItem } from './InactivateItem.service';
|
||||
import { ActivateItemService } from './ActivateItem.service';
|
||||
import { ItemsApplicationService } from './ItemsApplication.service';
|
||||
import { ItemTransactionsService } from './ItemTransactions.service';
|
||||
import { GetItemService } from './GetItem.service';
|
||||
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
||||
import { ItemsEntriesService } from './ItemsEntries.service';
|
||||
import { GetItemsService } from './GetItems.service';
|
||||
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
||||
import { InventoryAdjustmentsModule } from '../InventoryAdjutments/InventoryAdjustments.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TenancyDatabaseModule,
|
||||
DynamicListModule,
|
||||
InventoryAdjustmentsModule,
|
||||
],
|
||||
controllers: [ItemsController],
|
||||
providers: [
|
||||
ItemsValidators,
|
||||
CreateItemService,
|
||||
EditItemService,
|
||||
InactivateItem,
|
||||
ActivateItemService,
|
||||
DeleteItemService,
|
||||
ItemsApplicationService,
|
||||
GetItemService,
|
||||
GetItemsService,
|
||||
ItemTransactionsService,
|
||||
TenancyContext,
|
||||
TransformerInjectable,
|
||||
ItemsEntriesService,
|
||||
],
|
||||
exports: [ItemsEntriesService],
|
||||
})
|
||||
export class ItemsModule {}
|
||||
136
packages/server/src/modules/Items/ItemsApplication.service.ts
Normal file
136
packages/server/src/modules/Items/ItemsApplication.service.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { Item } from './models/Item';
|
||||
import { CreateItemService } from './CreateItem.service';
|
||||
import { DeleteItemService } from './DeleteItem.service';
|
||||
import { EditItemService } from './EditItem.service';
|
||||
import { IItem, IItemDTO } from '@/interfaces/Item';
|
||||
import { Knex } from 'knex';
|
||||
import { InactivateItem } from './InactivateItem.service';
|
||||
import { ActivateItemService } from './ActivateItem.service';
|
||||
import { GetItemService } from './GetItem.service';
|
||||
import { ItemTransactionsService } from './ItemTransactions.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { GetItemsService } from './GetItems.service';
|
||||
import { IItemsFilter } from './types/Items.types';
|
||||
import { EditItemDto, CreateItemDto } from './dtos/Item.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ItemsApplicationService {
|
||||
constructor(
|
||||
private readonly createItemService: CreateItemService,
|
||||
private readonly editItemService: EditItemService,
|
||||
private readonly deleteItemService: DeleteItemService,
|
||||
private readonly activateItemService: ActivateItemService,
|
||||
private readonly inactivateItemService: InactivateItem,
|
||||
private readonly getItemService: GetItemService,
|
||||
private readonly getItemsService: GetItemsService,
|
||||
private readonly itemTransactionsService: ItemTransactionsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates a new item.
|
||||
* @param {IItemDTO} createItemDto - The item DTO.
|
||||
* @param {Knex.Transaction} [trx] - The transaction.
|
||||
* @return {Promise<number>} - The created item id.
|
||||
*/
|
||||
async createItem(
|
||||
createItemDto: CreateItemDto,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<number> {
|
||||
return this.createItemService.createItem(createItemDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits an existing item.
|
||||
* @param {number} itemId - The item id.
|
||||
* @param {IItemDTO} editItemDto - The item DTO.
|
||||
* @param {Knex.Transaction} [trx] - The transaction.
|
||||
* @return {Promise<number>} - The updated item id.
|
||||
*/
|
||||
async editItem(
|
||||
itemId: number,
|
||||
editItemDto: EditItemDto,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<number> {
|
||||
return this.editItemService.editItem(itemId, editItemDto, trx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an existing item.
|
||||
* @param {number} itemId - The item id.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async deleteItem(itemId: number): Promise<void> {
|
||||
return this.deleteItemService.deleteItem(itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates an item.
|
||||
* @param {number} itemId - The item id.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async activateItem(itemId: number): Promise<void> {
|
||||
return this.activateItemService.activateItem(itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inactivates an item.
|
||||
* @param {number} itemId - The item id.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async inactivateItem(itemId: number): Promise<void> {
|
||||
return this.inactivateItemService.inactivateItem(itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the item details of the given id with associated details.
|
||||
* @param {number} itemId - The item id.
|
||||
* @returns {Promise<IItem>} - The item details.
|
||||
*/
|
||||
async getItem(itemId: number): Promise<any> {
|
||||
return this.getItemService.getItem(itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the paginated filterable items list.
|
||||
* @param {IItemsFilter} filterDTO
|
||||
*/
|
||||
async getItems(filterDTO: IItemsFilter) {
|
||||
return this.getItemsService.getItems(filterDTO)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the item associated invoices transactions.
|
||||
* @param {number} itemId
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
async getItemInvoicesTransactions(itemId: number): Promise<any> {
|
||||
return this.itemTransactionsService.getItemInvoicesTransactions(itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the item associated bills transactions.
|
||||
* @param {number} itemId
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
async getItemBillTransactions(itemId: number): Promise<any> {
|
||||
return this.itemTransactionsService.getItemBillTransactions(itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the item associated estimates transactions.
|
||||
* @param {number} itemId
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
async getItemEstimatesTransactions(itemId: number): Promise<any> {
|
||||
return this.itemTransactionsService.getItemEstimateTransactions(itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the item associated receipts transactions.
|
||||
* @param {number} itemId
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
async getItemReceiptsTransactions(itemId: number): Promise<any> {
|
||||
return this.itemTransactionsService.getItemReceiptTransactions(itemId);
|
||||
}
|
||||
}
|
||||
256
packages/server/src/modules/Items/ItemsEntries.service.ts
Normal file
256
packages/server/src/modules/Items/ItemsEntries.service.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
import { Knex } from 'knex';
|
||||
import { sumBy, difference, map } from 'lodash';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Item } from './models/Item';
|
||||
import { ItemEntry } from '../TransactionItemEntry/models/ItemEntry';
|
||||
import { ServiceError } from './ServiceError';
|
||||
import { IItemEntryDTO } from '../TransactionItemEntry/ItemEntry.types';
|
||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||
import { entriesAmountDiff } from '@/utils/entries-amount-diff';
|
||||
import { ItemEntryDto } from '../TransactionItemEntry/dto/ItemEntry.dto';
|
||||
|
||||
const ERRORS = {
|
||||
ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND',
|
||||
ENTRIES_IDS_NOT_FOUND: 'ENTRIES_IDS_NOT_FOUND',
|
||||
NOT_PURCHASE_ABLE_ITEMS: 'NOT_PURCHASE_ABLE_ITEMS',
|
||||
NOT_SELL_ABLE_ITEMS: 'NOT_SELL_ABLE_ITEMS',
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class ItemsEntriesService {
|
||||
/**
|
||||
* @param {TenantModelProxy<typeof Item>} itemModel - Item model.
|
||||
* @param {TenantModelProxy<typeof ItemEntry>} itemEntryModel - Item entry model.
|
||||
*/
|
||||
constructor(
|
||||
@Inject(Item.name)
|
||||
private readonly itemModel: TenantModelProxy<typeof Item>,
|
||||
|
||||
@Inject(ItemEntry.name)
|
||||
private readonly itemEntryModel: TenantModelProxy<typeof ItemEntry>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve the inventory items entries of the reference id and type.
|
||||
* @param {string} referenceType - Reference type.
|
||||
* @param {number} referenceId - Reference id.
|
||||
* @return {Promise<IItemEntry[]>}
|
||||
*/
|
||||
public async getInventoryEntries(
|
||||
referenceType: string,
|
||||
referenceId: number,
|
||||
): Promise<ItemEntry[]> {
|
||||
const itemsEntries = await this.itemEntryModel()
|
||||
.query()
|
||||
.where('reference_type', referenceType)
|
||||
.where('reference_id', referenceId);
|
||||
|
||||
const inventoryItems = await this.itemModel()
|
||||
.query()
|
||||
.whereIn('id', map(itemsEntries, 'itemId'))
|
||||
.where('type', 'inventory');
|
||||
|
||||
const inventoryItemsIds = map(inventoryItems, 'id');
|
||||
|
||||
return itemsEntries.filter(
|
||||
(itemEntry) => inventoryItemsIds.indexOf(itemEntry.itemId) !== -1,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the given entries to inventory entries.
|
||||
* @param {IItemEntry[]} entries - Items entries.
|
||||
* @param {Knex.Transaction} trx - Knex transaction.
|
||||
* @returns {Promise<IItemEntry[]>}
|
||||
*/
|
||||
public async filterInventoryEntries(
|
||||
entries: ItemEntry[],
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<ItemEntry[]> {
|
||||
const entriesItemsIds = entries.map((e) => e.itemId);
|
||||
|
||||
const inventoryItems = await this.itemModel()
|
||||
.query(trx)
|
||||
.whereIn('id', entriesItemsIds)
|
||||
.where('type', 'inventory');
|
||||
|
||||
return entries.filter((entry) =>
|
||||
inventoryItems.some((item) => item.id === entry.itemId),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the entries items ids.
|
||||
* @param {IItemEntryDTO[]} itemEntries - Items entries.
|
||||
* @returns {Promise<Item[]>}
|
||||
*/
|
||||
public async validateItemsIdsExistance(itemEntries: Array<{ itemId: number }>) {
|
||||
const itemsIds = itemEntries.map((e) => e.itemId);
|
||||
|
||||
const foundItems = await this.itemModel().query().whereIn('id', itemsIds);
|
||||
|
||||
const foundItemsIds = foundItems.map((item: Item) => item.id);
|
||||
const notFoundItemsIds = difference(itemsIds, foundItemsIds);
|
||||
|
||||
if (notFoundItemsIds.length > 0) {
|
||||
throw new ServiceError(ERRORS.ITEMS_NOT_FOUND);
|
||||
}
|
||||
return foundItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the entries ids existance on the storage.
|
||||
* @param {number} referenceId -
|
||||
* @param {string} referenceType -
|
||||
* @param {IItemEntryDTO[]} entries -
|
||||
*/
|
||||
public async validateEntriesIdsExistance(
|
||||
referenceId: number,
|
||||
referenceType: string,
|
||||
billEntries: ItemEntryDto[],
|
||||
) {
|
||||
const entriesIds = billEntries
|
||||
.filter((e: ItemEntry) => e.id)
|
||||
.map((e: ItemEntry) => e.id);
|
||||
|
||||
const storedEntries = await this.itemEntryModel()
|
||||
.query()
|
||||
.whereIn('reference_id', [referenceId])
|
||||
.whereIn('reference_type', [referenceType]);
|
||||
|
||||
const storedEntriesIds = storedEntries.map((entry) => entry.id);
|
||||
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
|
||||
|
||||
if (notFoundEntriesIds.length > 0) {
|
||||
throw new ServiceError(ERRORS.ENTRIES_IDS_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the entries items that not purchase-able.
|
||||
* @param {IItemEntryDTO[]} itemEntries -
|
||||
*/
|
||||
public async validateNonPurchasableEntriesItems(
|
||||
itemEntries: ItemEntryDto[],
|
||||
) {
|
||||
const itemsIds = itemEntries.map((e: ItemEntryDto) => e.itemId);
|
||||
const purchasbleItems = await this.itemModel()
|
||||
.query()
|
||||
.where('purchasable', true)
|
||||
.whereIn('id', itemsIds);
|
||||
|
||||
const purchasbleItemsIds = purchasbleItems.map((item: Item) => item.id);
|
||||
const notPurchasableItems = difference(itemsIds, purchasbleItemsIds);
|
||||
|
||||
if (notPurchasableItems.length > 0) {
|
||||
throw new ServiceError(ERRORS.NOT_PURCHASE_ABLE_ITEMS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the entries items that not sell-able.
|
||||
* @param {IItemEntryDTO[]} itemEntries -
|
||||
*/
|
||||
public async validateNonSellableEntriesItems(itemEntries: ItemEntryDto[]) {
|
||||
const itemsIds = itemEntries.map((e: ItemEntryDto) => e.itemId);
|
||||
|
||||
const sellableItems = await this.itemModel()
|
||||
.query()
|
||||
.where('sellable', true)
|
||||
.whereIn('id', itemsIds);
|
||||
|
||||
const sellableItemsIds = sellableItems.map((item: Item) => item.id);
|
||||
const nonSellableItems = difference(itemsIds, sellableItemsIds);
|
||||
|
||||
if (nonSellableItems.length > 0) {
|
||||
throw new ServiceError(ERRORS.NOT_SELL_ABLE_ITEMS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes items quantity from the given items entries the new and old onces.
|
||||
* @param {IItemEntry[]} entries - Items entries.
|
||||
* @param {IItemEntry[]} oldEntries - Old items entries.
|
||||
*/
|
||||
public async changeItemsQuantity(
|
||||
entries: ItemEntry[],
|
||||
oldEntries?: ItemEntry[],
|
||||
): Promise<void> {
|
||||
const opers = [];
|
||||
|
||||
const diffEntries = entriesAmountDiff(
|
||||
entries,
|
||||
oldEntries,
|
||||
'quantity',
|
||||
'itemId',
|
||||
);
|
||||
diffEntries.forEach((entry: ItemEntry) => {
|
||||
const changeQuantityOper = this.itemModel()
|
||||
.query()
|
||||
.where({ id: entry.itemId, type: 'inventory' })
|
||||
.modify('quantityOnHand', entry.quantity);
|
||||
|
||||
opers.push(changeQuantityOper);
|
||||
});
|
||||
await Promise.all(opers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment items quantity from the given items entries.
|
||||
* @param {IItemEntry[]} entries - Items entries.
|
||||
*/
|
||||
public async incrementItemsEntries(entries: ItemEntry[]): Promise<void> {
|
||||
return this.changeItemsQuantity(entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement items quantity from the given items entries.
|
||||
* @param {IItemEntry[]} entries - Items entries.
|
||||
*/
|
||||
public async decrementItemsQuantity(entries: ItemEntry[]): Promise<void> {
|
||||
// return this.changeItemsQuantity(
|
||||
// entries.map((entry) => ({
|
||||
// ...entry,
|
||||
// quantity: entry.quantity * -1,
|
||||
// })),
|
||||
// );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the cost/sell accounts to the invoice entries.
|
||||
*/
|
||||
public setItemsEntriesDefaultAccounts = async (entries: ItemEntryDto[]) => {
|
||||
const entriesItemsIds = entries.map((e) => e.itemId);
|
||||
const items = await this.itemModel().query().whereIn('id', entriesItemsIds);
|
||||
|
||||
return entries.map((entry) => {
|
||||
const item = items.find((i) => i.id === entry.itemId);
|
||||
|
||||
return {
|
||||
...entry,
|
||||
sellAccountId: entry.sellAccountId || item.sellAccountId,
|
||||
...(item.type === 'inventory' && {
|
||||
costAccountId: entry.costAccountId || item.costAccountId,
|
||||
}),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the total items entries.
|
||||
* @param {ItemEntry[]} entries - Items entries.
|
||||
* @returns {number}
|
||||
*/
|
||||
public getTotalItemsEntries(entries: ItemEntryDto[]): number {
|
||||
return sumBy(entries, (e) => ItemEntry.calcAmount(e));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the non-zero tax items entries.
|
||||
* @param {ItemEntry[]} entries -
|
||||
* @returns {ItemEntry[]}
|
||||
*/
|
||||
public getNonZeroEntries(entries: ItemEntry[]): ItemEntry[] {
|
||||
return entries.filter((e) => e.taxRate > 0);
|
||||
}
|
||||
}
|
||||
19
packages/server/src/modules/Items/ServiceError.ts
Normal file
19
packages/server/src/modules/Items/ServiceError.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { HttpStatus } from '@nestjs/common';
|
||||
|
||||
export class ServiceError extends Error {
|
||||
errorType: string;
|
||||
message: string;
|
||||
payload: any;
|
||||
|
||||
constructor(errorType: string, message?: string, payload?: any) {
|
||||
super(message);
|
||||
|
||||
this.errorType = errorType;
|
||||
this.message = message || null;
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
getStatus(): HttpStatus {
|
||||
return HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
}
|
||||
204
packages/server/src/modules/Items/dtos/Item.dto.ts
Normal file
204
packages/server/src/modules/Items/dtos/Item.dto.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import {
|
||||
IsString,
|
||||
IsIn,
|
||||
IsOptional,
|
||||
IsBoolean,
|
||||
IsNumber,
|
||||
IsInt,
|
||||
IsArray,
|
||||
ValidateIf,
|
||||
MaxLength,
|
||||
Min,
|
||||
Max,
|
||||
IsNotEmpty,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CommandItemDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(255)
|
||||
@ApiProperty({ description: 'Item name', example: 'Office Chair' })
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(['service', 'non-inventory', 'inventory'])
|
||||
@ApiProperty({
|
||||
description: 'Item type',
|
||||
enum: ['service', 'non-inventory', 'inventory'],
|
||||
example: 'inventory',
|
||||
})
|
||||
type: 'service' | 'non-inventory' | 'inventory';
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(255)
|
||||
@ApiProperty({
|
||||
description: 'Item code/SKU',
|
||||
required: false,
|
||||
example: 'ITEM-001',
|
||||
})
|
||||
code?: string;
|
||||
|
||||
// Purchase attributes
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiProperty({
|
||||
description: 'Whether the item can be purchased',
|
||||
required: false,
|
||||
example: true,
|
||||
})
|
||||
purchasable?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber({ maxDecimalPlaces: 3 })
|
||||
@Min(0)
|
||||
@ValidateIf((o) => o.purchasable === true)
|
||||
@ApiProperty({
|
||||
description: 'Cost price of the item',
|
||||
required: false,
|
||||
minimum: 0,
|
||||
example: 100.5,
|
||||
})
|
||||
costPrice?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@ValidateIf((o) => o.purchasable === true)
|
||||
@ApiProperty({
|
||||
description: 'ID of the cost account',
|
||||
required: false,
|
||||
minimum: 0,
|
||||
example: 1,
|
||||
})
|
||||
costAccountId?: number;
|
||||
|
||||
// Sell attributes
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiProperty({
|
||||
description: 'Whether the item can be sold',
|
||||
required: false,
|
||||
example: true,
|
||||
})
|
||||
sellable?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber({ maxDecimalPlaces: 3 })
|
||||
@Min(0)
|
||||
@ValidateIf((o) => o.sellable === true)
|
||||
@ApiProperty({
|
||||
description: 'Selling price of the item',
|
||||
required: false,
|
||||
minimum: 0,
|
||||
example: 150.75,
|
||||
})
|
||||
sellPrice?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@ValidateIf((o) => o.sellable === true)
|
||||
@ApiProperty({
|
||||
description: 'ID of the sell account',
|
||||
required: false,
|
||||
minimum: 0,
|
||||
example: 2,
|
||||
})
|
||||
sellAccountId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@ValidateIf((o) => o.type === 'inventory')
|
||||
@ApiProperty({
|
||||
description: 'ID of the inventory account (required for inventory items)',
|
||||
required: false,
|
||||
minimum: 0,
|
||||
example: 3,
|
||||
})
|
||||
inventoryAccountId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: 'Description shown on sales documents',
|
||||
required: false,
|
||||
example: 'High-quality ergonomic office chair',
|
||||
})
|
||||
sellDescription?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: 'Description shown on purchase documents',
|
||||
required: false,
|
||||
example: 'Ergonomic office chair with adjustable height',
|
||||
})
|
||||
purchaseDescription?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiProperty({
|
||||
description: 'ID of the tax rate applied to sales',
|
||||
required: false,
|
||||
example: 1,
|
||||
})
|
||||
sellTaxRateId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiProperty({
|
||||
description: 'ID of the tax rate applied to purchases',
|
||||
required: false,
|
||||
example: 1,
|
||||
})
|
||||
purchaseTaxRateId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@ApiProperty({
|
||||
description: 'ID of the item category',
|
||||
required: false,
|
||||
minimum: 0,
|
||||
example: 5,
|
||||
})
|
||||
categoryId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: 'Additional notes about the item',
|
||||
required: false,
|
||||
example: 'Available in multiple colors',
|
||||
})
|
||||
note?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiProperty({
|
||||
description: 'Whether the item is active',
|
||||
required: false,
|
||||
default: true,
|
||||
example: true,
|
||||
})
|
||||
active?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@Type(() => Number)
|
||||
@IsInt({ each: true })
|
||||
@ApiProperty({
|
||||
description: 'IDs of media files associated with the item',
|
||||
required: false,
|
||||
type: [Number],
|
||||
example: [1, 2, 3],
|
||||
})
|
||||
mediaIds?: number[];
|
||||
}
|
||||
|
||||
export class CreateItemDto extends CommandItemDto {}
|
||||
export class EditItemDto extends CommandItemDto {}
|
||||
@@ -0,0 +1,4 @@
|
||||
export class ItemCreatedEvent {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { ItemCreatedEvent } from '../events/ItemCreated.event';
|
||||
|
||||
@Injectable()
|
||||
export class ItemCreatedListener {
|
||||
|
||||
@OnEvent('order.created')
|
||||
handleItemCreatedEvent(event: ItemCreatedEvent) {
|
||||
// handle and process "OrderCreatedEvent" event
|
||||
console.log(event);
|
||||
}
|
||||
}
|
||||
213
packages/server/src/modules/Items/models/Item.ts
Normal file
213
packages/server/src/modules/Items/models/Item.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import { Warehouse } from '@/modules/Warehouses/models/Warehouse.model';
|
||||
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
|
||||
import { Model } from 'objection';
|
||||
|
||||
export class Item extends TenantBaseModel {
|
||||
public readonly quantityOnHand: number;
|
||||
public readonly name: string;
|
||||
public readonly active: boolean;
|
||||
public readonly type: string;
|
||||
public readonly code: string;
|
||||
public readonly sellable: boolean;
|
||||
public readonly purchasable: boolean;
|
||||
public readonly costPrice: number;
|
||||
public readonly sellPrice: number;
|
||||
public readonly currencyCode: string;
|
||||
public readonly costAccountId: number;
|
||||
public readonly inventoryAccountId: number;
|
||||
public readonly categoryId: number;
|
||||
public readonly pictureUri: string;
|
||||
public readonly sellAccountId: number;
|
||||
public readonly sellDescription: string;
|
||||
public readonly purchaseDescription: string;
|
||||
public readonly landedCost: boolean;
|
||||
public readonly note: string;
|
||||
public readonly userId: number;
|
||||
public readonly sellTaxRateId: number;
|
||||
public readonly purchaseTaxRateId: number;
|
||||
|
||||
public readonly warehouse!: Warehouse;
|
||||
|
||||
static get tableName() {
|
||||
return 'items';
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {
|
||||
updateQuantityOnHand(query, value: number) {
|
||||
const q = query.where('type', 'inventory');
|
||||
|
||||
if (value > 0) {
|
||||
q.increment('quantityOnHand', value);
|
||||
}
|
||||
if (value < 0) {
|
||||
q.decrement('quantityOnHand', Math.abs(value));
|
||||
}
|
||||
return q;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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/InventoryAdjustmentEntry');
|
||||
const { TaxRateModel } = 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: TaxRateModel,
|
||||
join: {
|
||||
from: 'items.sellTaxRateId',
|
||||
to: 'tax_rates.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Item may has purchase tax rate.
|
||||
*/
|
||||
purchaseTaxRate: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: TaxRateModel,
|
||||
join: {
|
||||
from: 'items.purchaseTaxRateId',
|
||||
to: 'tax_rates.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
8
packages/server/src/modules/Items/types/Items.types.ts
Normal file
8
packages/server/src/modules/Items/types/Items.types.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { IDynamicListFilter } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types';
|
||||
|
||||
export interface IItemsFilter extends IDynamicListFilter {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
inactiveMode: boolean;
|
||||
viewSlug?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user