mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
add server to monorepo.
This commit is contained in:
40
packages/server/src/services/Items/ActivateItem.ts
Normal file
40
packages/server/src/services/Items/ActivateItem.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class ActivateItem {
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
/**
|
||||
* Activates the given item on the storage.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} itemId -
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async activateItem(tenantId: number, itemId: number): Promise<void> {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retreives the given item or throw not found error.
|
||||
const oldItem = await Item.query().findById(itemId).throwIfNotFound();
|
||||
|
||||
// Activate the given item with associated transactions under unit-of-work envirement.
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Mutate item on the storage.
|
||||
await Item.query(trx).findById(itemId).patch({ active: true });
|
||||
|
||||
// Triggers `onItemActivated` event.
|
||||
await this.eventPublisher.emitAsync(events.item.onActivated, {});
|
||||
});
|
||||
}
|
||||
}
|
||||
106
packages/server/src/services/Items/CreateItem.ts
Normal file
106
packages/server/src/services/Items/CreateItem.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { Knex } from 'knex';
|
||||
import { defaultTo } from 'lodash';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { IItem, IItemDTO, IItemEventCreatedPayload } from '@/interfaces';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import events from '@/subscribers/events';
|
||||
import { ItemsValidators } from './ItemValidators';
|
||||
|
||||
@Service()
|
||||
export class CreateItem {
|
||||
@Inject()
|
||||
private validators: ItemsValidators;
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
/**
|
||||
* Authorize the creating item.
|
||||
* @param {number} tenantId
|
||||
* @param {IItemDTO} itemDTO
|
||||
*/
|
||||
async authorize(tenantId: number, itemDTO: IItemDTO) {
|
||||
// Validate whether the given item name already exists on the storage.
|
||||
await this.validators.validateItemNameUniquiness(tenantId, itemDTO.name);
|
||||
|
||||
if (itemDTO.categoryId) {
|
||||
await this.validators.validateItemCategoryExistance(
|
||||
tenantId,
|
||||
itemDTO.categoryId
|
||||
);
|
||||
}
|
||||
if (itemDTO.sellAccountId) {
|
||||
await this.validators.validateItemSellAccountExistance(
|
||||
tenantId,
|
||||
itemDTO.sellAccountId
|
||||
);
|
||||
}
|
||||
if (itemDTO.costAccountId) {
|
||||
await this.validators.validateItemCostAccountExistance(
|
||||
tenantId,
|
||||
itemDTO.costAccountId
|
||||
);
|
||||
}
|
||||
if (itemDTO.inventoryAccountId) {
|
||||
await this.validators.validateItemInventoryAccountExistance(
|
||||
tenantId,
|
||||
itemDTO.inventoryAccountId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the item DTO to model.
|
||||
* @param {IItemDTO} itemDTO - Item DTO.
|
||||
* @return {IItem}
|
||||
*/
|
||||
private transformNewItemDTOToModel(itemDTO: IItemDTO) {
|
||||
return {
|
||||
...itemDTO,
|
||||
active: defaultTo(itemDTO.active, 1),
|
||||
quantityOnHand: itemDTO.type === 'inventory' ? 0 : null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new item.
|
||||
* @param {number} tenantId DTO
|
||||
* @param {IItemDTO} item
|
||||
* @return {Promise<IItem>}
|
||||
*/
|
||||
public async createItem(tenantId: number, itemDTO: IItemDTO): Promise<IItem> {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
|
||||
// Authorize the item before creating.
|
||||
await this.authorize(tenantId, itemDTO);
|
||||
|
||||
// Creates a new item with associated transactions under unit-of-work envirement.
|
||||
const item = this.uow.withTransaction(
|
||||
tenantId,
|
||||
async (trx: Knex.Transaction) => {
|
||||
// Inserts a new item and fetch the created item.
|
||||
const item = await Item.query(trx).insertAndFetch({
|
||||
...this.transformNewItemDTOToModel(itemDTO),
|
||||
});
|
||||
// Triggers `onItemCreated` event.
|
||||
await this.eventPublisher.emitAsync(events.item.onCreated, {
|
||||
tenantId,
|
||||
item,
|
||||
itemId: item.id,
|
||||
trx,
|
||||
} as IItemEventCreatedPayload);
|
||||
|
||||
return item;
|
||||
}
|
||||
);
|
||||
return item;
|
||||
}
|
||||
}
|
||||
66
packages/server/src/services/Items/DeleteItem.ts
Normal file
66
packages/server/src/services/Items/DeleteItem.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import {
|
||||
IItemEventDeletedPayload,
|
||||
IItemEventDeletingPayload,
|
||||
} from '@/interfaces';
|
||||
import { Knex } from 'knex';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import events from '@/subscribers/events';
|
||||
import { ERRORS } from './constants';
|
||||
import { ItemsValidators } from './ItemValidators';
|
||||
|
||||
@Service()
|
||||
export class DeleteItem {
|
||||
@Inject()
|
||||
private validators: ItemsValidators;
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
/**
|
||||
* Delete the given item from the storage.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} itemId - Item id.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async deleteItem(tenantId: number, itemId: number) {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retreive the given item or throw not found service error.
|
||||
const oldItem = await Item.query()
|
||||
.findById(itemId)
|
||||
.throwIfNotFound()
|
||||
.queryAndThrowIfHasRelations({
|
||||
type: ERRORS.ITEM_HAS_ASSOCIATED_TRANSACTIONS,
|
||||
});
|
||||
// Delete item in unit of work.
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onItemDeleting` event.
|
||||
await this.eventPublisher.emitAsync(events.item.onDeleting, {
|
||||
tenantId,
|
||||
trx,
|
||||
oldItem,
|
||||
} as IItemEventDeletingPayload);
|
||||
|
||||
// Deletes the item.
|
||||
await Item.query(trx).findById(itemId).delete();
|
||||
|
||||
const eventPayload: IItemEventDeletedPayload = {
|
||||
tenantId,
|
||||
oldItem,
|
||||
itemId,
|
||||
trx,
|
||||
};
|
||||
// Triggers `onItemDeleted` event.
|
||||
await this.eventPublisher.emitAsync(events.item.onDeleted, eventPayload);
|
||||
});
|
||||
}
|
||||
}
|
||||
147
packages/server/src/services/Items/EditItem.ts
Normal file
147
packages/server/src/services/Items/EditItem.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import {
|
||||
IItem,
|
||||
IItemDTO,
|
||||
IItemEditDTO,
|
||||
IItemEventEditedPayload,
|
||||
} from '@/interfaces';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import { ItemsValidators } from './ItemValidators';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class EditItem {
|
||||
@Inject()
|
||||
private validators: ItemsValidators;
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
/**
|
||||
* Authorize the editing item.
|
||||
* @param {number} tenantId
|
||||
* @param {IItemEditDTO} itemDTO
|
||||
* @param {IItem} oldItem
|
||||
*/
|
||||
async authorize(tenantId: number, itemDTO: IItemEditDTO, oldItem: IItem) {
|
||||
// Validate edit item type from inventory type.
|
||||
this.validators.validateEditItemFromInventory(itemDTO, oldItem);
|
||||
|
||||
// Validate edit item type to inventory type.
|
||||
await this.validators.validateEditItemTypeToInventory(
|
||||
tenantId,
|
||||
oldItem,
|
||||
itemDTO
|
||||
);
|
||||
// Validate whether the given item name already exists on the storage.
|
||||
await this.validators.validateItemNameUniquiness(
|
||||
tenantId,
|
||||
itemDTO.name,
|
||||
oldItem.id
|
||||
);
|
||||
// Validate the item category existance on the storage,
|
||||
if (itemDTO.categoryId) {
|
||||
await this.validators.validateItemCategoryExistance(
|
||||
tenantId,
|
||||
itemDTO.categoryId
|
||||
);
|
||||
}
|
||||
// Validate the sell account existance on the storage.
|
||||
if (itemDTO.sellAccountId) {
|
||||
await this.validators.validateItemSellAccountExistance(
|
||||
tenantId,
|
||||
itemDTO.sellAccountId
|
||||
);
|
||||
}
|
||||
// Validate the cost account existance on the storage.
|
||||
if (itemDTO.costAccountId) {
|
||||
await this.validators.validateItemCostAccountExistance(
|
||||
tenantId,
|
||||
itemDTO.costAccountId
|
||||
);
|
||||
}
|
||||
// Validate the inventory account existance onthe storage.
|
||||
if (itemDTO.inventoryAccountId) {
|
||||
await this.validators.validateItemInventoryAccountExistance(
|
||||
tenantId,
|
||||
itemDTO.inventoryAccountId
|
||||
);
|
||||
}
|
||||
// Validate inventory account should be modified in inventory item
|
||||
// has inventory transactions.
|
||||
await this.validators.validateItemInvnetoryAccountModified(
|
||||
tenantId,
|
||||
oldItem,
|
||||
itemDTO
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes edit item DTO to model.
|
||||
* @param {IItemDTO} itemDTO - Item DTO.
|
||||
* @param {IItem} oldItem -
|
||||
*/
|
||||
private transformEditItemDTOToModel(itemDTO: IItemDTO, oldItem: IItem) {
|
||||
return {
|
||||
...itemDTO,
|
||||
...(itemDTO.type === 'inventory' && oldItem.type !== 'inventory'
|
||||
? {
|
||||
quantityOnHand: 0,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the item metadata.
|
||||
* @param {number} tenantId
|
||||
* @param {number} itemId
|
||||
* @param {IItemDTO} itemDTO
|
||||
*/
|
||||
public async editItem(tenantId: number, itemId: number, itemDTO: IItemDTO) {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
|
||||
// Validates the given item existance on the storage.
|
||||
const oldItem = await Item.query().findById(itemId).throwIfNotFound();
|
||||
|
||||
// Authorize before editing item.
|
||||
await this.authorize(tenantId, 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 envirement.
|
||||
const newItem = this.uow.withTransaction(
|
||||
tenantId,
|
||||
async (trx: Knex.Transaction) => {
|
||||
// Updates the item on the storage and fetches the updated once.
|
||||
const newItem = await Item.query(trx).patchAndFetchById(itemId, {
|
||||
...itemModel,
|
||||
});
|
||||
// Edit event payload.
|
||||
const eventPayload: IItemEventEditedPayload = {
|
||||
tenantId,
|
||||
item: newItem,
|
||||
oldItem,
|
||||
itemId: newItem.id,
|
||||
trx,
|
||||
};
|
||||
// Triggers `onItemEdited` event.
|
||||
await this.eventPublisher.emitAsync(events.item.onEdited, eventPayload);
|
||||
|
||||
return newItem;
|
||||
}
|
||||
);
|
||||
|
||||
return newItem;
|
||||
}
|
||||
}
|
||||
34
packages/server/src/services/Items/GetItem.ts
Normal file
34
packages/server/src/services/Items/GetItem.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Inject } from 'typedi';
|
||||
import { IItem } from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import ItemTransformer from './ItemTransformer';
|
||||
|
||||
@Inject()
|
||||
export class GetItem {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Retrieve the item details of the given id with associated details.
|
||||
* @param {number} tenantId
|
||||
* @param {number} itemId
|
||||
*/
|
||||
public async getItem(tenantId: number, itemId: number): Promise<IItem> {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
|
||||
const item = await Item.query()
|
||||
.findById(itemId)
|
||||
.withGraphFetched('sellAccount')
|
||||
.withGraphFetched('inventoryAccount')
|
||||
.withGraphFetched('category')
|
||||
.withGraphFetched('costAccount')
|
||||
.withGraphFetched('itemWarehouses.warehouse')
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.transformer.transform(tenantId, item, new ItemTransformer());
|
||||
}
|
||||
}
|
||||
70
packages/server/src/services/Items/GetItems.ts
Normal file
70
packages/server/src/services/Items/GetItems.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import * as R from 'ramda';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { IItemsFilter } from '@/interfaces';
|
||||
import ItemTransformer from './ItemTransformer';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
|
||||
@Service()
|
||||
export class GetItems {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private dynamicListService: DynamicListingService;
|
||||
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Parses items list filter DTO.
|
||||
* @param {} filterDTO - Filter DTO.
|
||||
*/
|
||||
private parseItemsListFilterDTO(filterDTO) {
|
||||
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve items datatable list.
|
||||
* @param {number} tenantId -
|
||||
* @param {IItemsFilter} itemsFilter -
|
||||
*/
|
||||
public async getItems(tenantId: number, filterDTO: IItemsFilter) {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
|
||||
// Parses items list filter DTO.
|
||||
const filter = this.parseItemsListFilterDTO(filterDTO);
|
||||
|
||||
// Dynamic list service.
|
||||
const dynamicFilter = await this.dynamicListService.dynamicList(
|
||||
tenantId,
|
||||
Item,
|
||||
filter
|
||||
);
|
||||
const { results: items, pagination } = await Item.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(
|
||||
tenantId,
|
||||
items,
|
||||
new ItemTransformer()
|
||||
);
|
||||
return {
|
||||
items: transformedItems,
|
||||
pagination,
|
||||
filterMeta: dynamicFilter.getResponseMeta(),
|
||||
};
|
||||
}
|
||||
}
|
||||
40
packages/server/src/services/Items/InactivateItem.ts
Normal file
40
packages/server/src/services/Items/InactivateItem.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class InactivateItem {
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
/**
|
||||
* Inactivates the given item on the storage.
|
||||
* @param {number} tenantId
|
||||
* @param {number} itemId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async inactivateItem(tenantId: number, itemId: number): Promise<void> {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieves the item or throw not found error.
|
||||
const oldItem = await Item.query().findById(itemId).throwIfNotFound();
|
||||
|
||||
// Inactivate item under unit-of-work envirement.
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Activate item on the storage.
|
||||
await Item.query(trx).findById(itemId).patch({ active: false });
|
||||
|
||||
// Triggers `onItemInactivated` event.
|
||||
await this.eventPublisher.emitAsync(events.item.onInactivated, { trx });
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
import { formatNumber } from 'utils';
|
||||
|
||||
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 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 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,82 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
import { formatNumber } from 'utils';
|
||||
|
||||
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 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 formatNumber(entry.rate, {
|
||||
currencyCode: entry.estimate.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param entry
|
||||
* @returns
|
||||
*/
|
||||
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 '@/lib/Transformer/Transformer';
|
||||
import { formatNumber } from 'utils';
|
||||
|
||||
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 formatNumber(item.amount, {
|
||||
currencyCode: item.invoice.currencyCode,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param item
|
||||
* @returns
|
||||
*/
|
||||
public formattedInvoiceDate = (entry): string => {
|
||||
return this.formatDate(entry.invoice.invoiceDate);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public formattedQuantity = (entry): string => {
|
||||
return entry.quantity;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param entry
|
||||
* @returns
|
||||
*/
|
||||
public formattedRate = (entry): string => {
|
||||
return formatNumber(entry.rate, {
|
||||
currencyCode: entry.invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param entry
|
||||
* @returns
|
||||
*/
|
||||
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,84 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
import { formatNumber } from 'utils';
|
||||
|
||||
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 formatNumber(item.amount, {
|
||||
currencyCode: item.receipt.currencyCode,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param item
|
||||
* @returns
|
||||
*/
|
||||
public formattedReceiptDate = (entry): string => {
|
||||
return this.formatDate(entry.receipt.receiptDate);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public formattedQuantity = (entry): string => {
|
||||
return entry.quantity;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param entry
|
||||
* @returns
|
||||
*/
|
||||
public formattedRate = (entry): string => {
|
||||
return formatNumber(entry.rate, {
|
||||
currencyCode: entry.receipt.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param entry
|
||||
* @returns
|
||||
*/
|
||||
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,
|
||||
};
|
||||
};
|
||||
}
|
||||
123
packages/server/src/services/Items/ItemTransactionsService.ts
Normal file
123
packages/server/src/services/Items/ItemTransactionsService.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ItemInvoicesTransactionsTransformer } from './ItemInvoicesTransactionsTransformer';
|
||||
import { ItemEstimateTransactionTransformer } from './ItemEstimatesTransactionTransformer';
|
||||
import { ItemBillTransactionTransformer } from './ItemBillsTransactionsTransformer';
|
||||
import { ItemReceiptTransactionTransformer } from './ItemReceiptsTransactionsTransformer';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
|
||||
@Service()
|
||||
export default class ItemTransactionsService {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Retrieve the item associated invoices transactions.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} itemId -
|
||||
*/
|
||||
public async getItemInvoicesTransactions(tenantId: number, itemId: number) {
|
||||
const { ItemEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
const invoiceEntries = await 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(
|
||||
tenantId,
|
||||
invoiceEntries,
|
||||
new ItemInvoicesTransactionsTransformer()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the item associated invoices transactions.
|
||||
* @param {number} tenantId
|
||||
* @param {number} itemId
|
||||
* @returns
|
||||
*/
|
||||
public async getItemBillTransactions(tenantId: number, itemId: number) {
|
||||
const { ItemEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
const billEntries = await 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(
|
||||
tenantId,
|
||||
billEntries,
|
||||
new ItemBillTransactionTransformer()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {number} itemId
|
||||
* @returns
|
||||
*/
|
||||
public async getItemEstimateTransactions(tenantId: number, itemId: number) {
|
||||
const { ItemEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
const estimatesEntries = await 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(
|
||||
tenantId,
|
||||
estimatesEntries,
|
||||
new ItemEstimateTransactionTransformer()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {number} itemId
|
||||
* @returns
|
||||
*/
|
||||
public async getItemReceiptTransactions(tenantId: number, itemId: number) {
|
||||
const { ItemEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
const receiptsEntries = await 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(
|
||||
tenantId,
|
||||
receiptsEntries,
|
||||
new ItemReceiptTransactionTransformer()
|
||||
);
|
||||
}
|
||||
}
|
||||
62
packages/server/src/services/Items/ItemTransformer.ts
Normal file
62
packages/server/src/services/Items/ItemTransformer.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
import { GetItemWarehouseTransformer } from '@/services/Warehouses/Items/GettItemWarehouseTransformer';
|
||||
import { formatNumber } from 'utils';
|
||||
|
||||
export default 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): string {
|
||||
return this.context.i18n.__(`item.field.type.${item.type}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatted sell price.
|
||||
* @param item
|
||||
* @returns {string}
|
||||
*/
|
||||
public sellPriceFormatted(item): string {
|
||||
return formatNumber(item.sellPrice, {
|
||||
currencyCode: this.context.organization.baseCurrency,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatted cost price.
|
||||
* @param item
|
||||
* @returns {string}
|
||||
*/
|
||||
public costPriceFormatted(item): string {
|
||||
return 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(),
|
||||
{}
|
||||
);
|
||||
};
|
||||
}
|
||||
244
packages/server/src/services/Items/ItemValidators.ts
Normal file
244
packages/server/src/services/Items/ItemValidators.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import {
|
||||
ACCOUNT_PARENT_TYPE,
|
||||
ACCOUNT_ROOT_TYPE,
|
||||
ACCOUNT_TYPE,
|
||||
} from '@/data/AccountTypes';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { IItem, IItemDTO } from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ERRORS } from './constants';
|
||||
|
||||
@Service()
|
||||
export class ItemsValidators {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Validate wether the given item name already exists on the storage.
|
||||
* @param {number} tenantId
|
||||
* @param {string} itemName
|
||||
* @param {number} notItemId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async validateItemNameUniquiness(
|
||||
tenantId: number,
|
||||
itemName: string,
|
||||
notItemId?: number
|
||||
): Promise<void> {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
|
||||
const foundItems: [] = await Item.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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate item COGS account existance and type.
|
||||
* @param {number} tenantId
|
||||
* @param {number} costAccountId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async validateItemCostAccountExistance(
|
||||
tenantId: number,
|
||||
costAccountId: number
|
||||
): Promise<void> {
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
const foundAccount = await accountRepository.findOneById(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} tenantId - Tenant id.
|
||||
* @param {number} sellAccountId - Sell account id.
|
||||
*/
|
||||
public async validateItemSellAccountExistance(
|
||||
tenantId: number,
|
||||
sellAccountId: number
|
||||
) {
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
const foundAccount = await accountRepository.findOneById(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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate item inventory account existance and type.
|
||||
* @param {number} tenantId
|
||||
* @param {number} inventoryAccountId
|
||||
*/
|
||||
public async validateItemInventoryAccountExistance(
|
||||
tenantId: number,
|
||||
inventoryAccountId: number
|
||||
) {
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
const foundAccount = await accountRepository.findOneById(
|
||||
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} tenantId
|
||||
* @param {number} itemCategoryId
|
||||
*/
|
||||
public async validateItemCategoryExistance(
|
||||
tenantId: number,
|
||||
itemCategoryId: number
|
||||
) {
|
||||
const { ItemCategory } = this.tenancy.models(tenantId);
|
||||
const foundCategory = await ItemCategory.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} tenantId - Tenant id.
|
||||
* @param {number|number[]} itemId - Item id.
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
public async validateHasNoInvoicesOrBills(
|
||||
tenantId: number,
|
||||
itemId: number[] | number
|
||||
) {
|
||||
const { ItemEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
const ids = Array.isArray(itemId) ? itemId : [itemId];
|
||||
const foundItemEntries = await ItemEntry.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} tenantId -
|
||||
* @param {number} itemId -
|
||||
*/
|
||||
public async validateHasNoInventoryAdjustments(
|
||||
tenantId: number,
|
||||
itemId: number[] | number
|
||||
): Promise<void> {
|
||||
const { InventoryAdjustmentEntry } = this.tenancy.models(tenantId);
|
||||
const itemsIds = Array.isArray(itemId) ? itemId : [itemId];
|
||||
|
||||
const inventoryAdjEntries = await InventoryAdjustmentEntry.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} tenantId - Tenant id.
|
||||
* @param {number} itemId - Item id.
|
||||
*/
|
||||
public async validateEditItemTypeToInventory(
|
||||
tenantId: number,
|
||||
oldItem: IItem,
|
||||
newItemDTO: IItemDTO
|
||||
) {
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
// 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 AccountTransaction.query()
|
||||
.where('item_id', oldItem.id)
|
||||
.count('item_id', { as: 'transactions' })
|
||||
.first();
|
||||
|
||||
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 assocaited inventory transactions.
|
||||
* @param {numnber} tenantId
|
||||
* @param {IItem} oldItem
|
||||
* @param {IItemDTO} newItemDTO
|
||||
* @returns
|
||||
*/
|
||||
async validateItemInvnetoryAccountModified(
|
||||
tenantId: number,
|
||||
oldItem: IItem,
|
||||
newItemDTO: IItemDTO
|
||||
) {
|
||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
if (
|
||||
newItemDTO.type !== 'inventory' ||
|
||||
oldItem.inventoryAccountId === newItemDTO.inventoryAccountId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Inventory transactions associated to the given item id.
|
||||
const transactions = await AccountTransaction.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 {IItemDTO} itemDTO - Item DTO.
|
||||
* @param {IItem} oldItem - Old item.
|
||||
*/
|
||||
public validateEditItemFromInventory(itemDTO: IItemDTO, oldItem: IItem) {
|
||||
if (
|
||||
itemDTO.type &&
|
||||
oldItem.type === 'inventory' &&
|
||||
itemDTO.type !== oldItem.type
|
||||
) {
|
||||
throw new ServiceError(ERRORS.ITEM_CANNOT_CHANGE_INVENTORY_TYPE);
|
||||
}
|
||||
}
|
||||
}
|
||||
112
packages/server/src/services/Items/ItemsApplication.ts
Normal file
112
packages/server/src/services/Items/ItemsApplication.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import {
|
||||
IItem,
|
||||
IItemCreateDTO,
|
||||
IItemDTO,
|
||||
IItemEditDTO,
|
||||
IItemsFilter,
|
||||
} from '@/interfaces';
|
||||
import { CreateItem } from './CreateItem';
|
||||
import { EditItem } from './EditItem';
|
||||
import { DeleteItem } from './DeleteItem';
|
||||
import { GetItem } from './GetItem';
|
||||
import { GetItems } from './GetItems';
|
||||
import { ActivateItem } from './ActivateItem';
|
||||
import { InactivateItem } from './InactivateItem';
|
||||
|
||||
@Service()
|
||||
export class ItemsApplication {
|
||||
@Inject()
|
||||
private createItemService: CreateItem;
|
||||
|
||||
@Inject()
|
||||
private editItemService: EditItem;
|
||||
|
||||
@Inject()
|
||||
private getItemService: GetItem;
|
||||
|
||||
@Inject()
|
||||
private getItemsService: GetItems;
|
||||
|
||||
@Inject()
|
||||
private deleteItemService: DeleteItem;
|
||||
|
||||
@Inject()
|
||||
private activateItemService: ActivateItem;
|
||||
|
||||
@Inject()
|
||||
private inactivateItemService: InactivateItem;
|
||||
|
||||
/**
|
||||
* Creates a new item (service/product).
|
||||
* @param {number} tenantId
|
||||
* @param {IItemCreateDTO} itemDTO
|
||||
* @returns {Promise<IItem>}
|
||||
*/
|
||||
public async createItem(
|
||||
tenantId: number,
|
||||
itemDTO: IItemCreateDTO
|
||||
): Promise<IItem> {
|
||||
return this.createItemService.createItem(tenantId, itemDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the given item.
|
||||
* @param {number} tenantId
|
||||
* @param {number} itemId
|
||||
* @returns {Promise<IItem>}
|
||||
*/
|
||||
public getItem(tenantId: number, itemId: number): Promise<IItem> {
|
||||
return this.getItemService.getItem(tenantId, itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the given item (service/product).
|
||||
* @param {number} tenantId
|
||||
* @param {number} itemId
|
||||
* @param {IItemEditDTO} itemDTO
|
||||
* @returns {Promise<IItem>}
|
||||
*/
|
||||
public editItem(tenantId: number, itemId: number, itemDTO: IItemEditDTO) {
|
||||
return this.editItemService.editItem(tenantId, itemId, itemDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given item (service/product).
|
||||
* @param {number} tenantId
|
||||
* @param {number} itemId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public deleteItem(tenantId: number, itemId: number) {
|
||||
return this.deleteItemService.deleteItem(tenantId, itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the given item (service/product).
|
||||
* @param {number} tenantId
|
||||
* @param {number} itemId
|
||||
* @returns
|
||||
*/
|
||||
public activateItem(tenantId: number, itemId: number): Promise<void> {
|
||||
return this.activateItemService.activateItem(tenantId, itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inactivates the given item.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} itemId -
|
||||
*/
|
||||
public inactivateItem(tenantId: number, itemId: number): Promise<void> {
|
||||
return this.inactivateItemService.inactivateItem(tenantId, itemId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the items paginated list.
|
||||
* @param {number} tenantId
|
||||
* @param {IItemsFilter} filterDTO
|
||||
* @returns {}
|
||||
*/
|
||||
public getItems(tenantId: number, filterDTO: IItemsFilter) {
|
||||
return this.getItemsService.getItems(tenantId, filterDTO);
|
||||
}
|
||||
}
|
||||
5
packages/server/src/services/Items/ItemsCostService.ts
Normal file
5
packages/server/src/services/Items/ItemsCostService.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
export default class ItemsCostService {
|
||||
|
||||
}
|
||||
267
packages/server/src/services/Items/ItemsEntriesService.ts
Normal file
267
packages/server/src/services/Items/ItemsEntriesService.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
import { sumBy, difference, map } from 'lodash';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { IItemEntry, IItemEntryDTO, IItem } from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { entriesAmountDiff } from 'utils';
|
||||
import { ItemEntry } from 'models';
|
||||
|
||||
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',
|
||||
};
|
||||
|
||||
@Service()
|
||||
export default class ItemsEntriesService {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
/**
|
||||
* Retrieve the inventory items entries of the reference id and type.
|
||||
* @param {number} tenantId
|
||||
* @param {string} referenceType
|
||||
* @param {string} referenceId
|
||||
* @return {Promise<IItemEntry[]>}
|
||||
*/
|
||||
public async getInventoryEntries(
|
||||
tenantId: number,
|
||||
referenceType: string,
|
||||
referenceId: number
|
||||
): Promise<IItemEntry[]> {
|
||||
const { Item, ItemEntry } = this.tenancy.models(tenantId);
|
||||
|
||||
const itemsEntries = await ItemEntry.query()
|
||||
.where('reference_type', referenceType)
|
||||
.where('reference_id', referenceId);
|
||||
|
||||
// Inventory items.
|
||||
const inventoryItems = await Item.query()
|
||||
.whereIn('id', map(itemsEntries, 'itemId'))
|
||||
.where('type', 'inventory');
|
||||
|
||||
// Inventory items ids.
|
||||
const inventoryItemsIds = map(inventoryItems, 'id');
|
||||
|
||||
// Filtering the inventory items entries.
|
||||
const inventoryItemsEntries = itemsEntries.filter(
|
||||
(itemEntry) => inventoryItemsIds.indexOf(itemEntry.itemId) !== -1
|
||||
);
|
||||
return inventoryItemsEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the given entries to inventory entries.
|
||||
* @param {IItemEntry[]} entries -
|
||||
* @returns {IItemEntry[]}
|
||||
*/
|
||||
public async filterInventoryEntries(
|
||||
tenantId: number,
|
||||
entries: IItemEntry[]
|
||||
): Promise<IItemEntry[]> {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
const entriesItemsIds = entries.map((e) => e.itemId);
|
||||
|
||||
// Retrieve entries inventory items.
|
||||
const inventoryItems = await Item.query()
|
||||
.whereIn('id', entriesItemsIds)
|
||||
.where('type', 'inventory');
|
||||
|
||||
const inventoryEntries = entries.filter((entry) =>
|
||||
inventoryItems.some((item) => item.id === entry.itemId)
|
||||
);
|
||||
return inventoryEntries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the entries items ids.
|
||||
* @async
|
||||
* @param {number} tenantId -
|
||||
* @param {IItemEntryDTO} itemEntries -
|
||||
*/
|
||||
public async validateItemsIdsExistance(
|
||||
tenantId: number,
|
||||
itemEntries: IItemEntryDTO[]
|
||||
) {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
const itemsIds = itemEntries.map((e) => e.itemId);
|
||||
|
||||
const foundItems = await Item.query().whereIn('id', itemsIds);
|
||||
|
||||
const foundItemsIds = foundItems.map((item: IItem) => 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} tenantId -
|
||||
* @param {number} billId -
|
||||
* @param {IItemEntry[]} billEntries -
|
||||
*/
|
||||
public async validateEntriesIdsExistance(
|
||||
tenantId: number,
|
||||
referenceId: number,
|
||||
referenceType: string,
|
||||
billEntries: IItemEntryDTO[]
|
||||
) {
|
||||
const { ItemEntry } = this.tenancy.models(tenantId);
|
||||
const entriesIds = billEntries
|
||||
.filter((e: IItemEntry) => e.id)
|
||||
.map((e: IItemEntry) => e.id);
|
||||
|
||||
const storedEntries = await ItemEntry.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.
|
||||
*/
|
||||
public async validateNonPurchasableEntriesItems(
|
||||
tenantId: number,
|
||||
itemEntries: IItemEntryDTO[]
|
||||
) {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
const itemsIds = itemEntries.map((e: IItemEntryDTO) => e.itemId);
|
||||
|
||||
const purchasbleItems = await Item.query()
|
||||
.where('purchasable', true)
|
||||
.whereIn('id', itemsIds);
|
||||
|
||||
const purchasbleItemsIds = purchasbleItems.map((item: IItem) => 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.
|
||||
*/
|
||||
public async validateNonSellableEntriesItems(
|
||||
tenantId: number,
|
||||
itemEntries: IItemEntryDTO[]
|
||||
) {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
const itemsIds = itemEntries.map((e: IItemEntryDTO) => e.itemId);
|
||||
|
||||
const sellableItems = await Item.query()
|
||||
.where('sellable', true)
|
||||
.whereIn('id', itemsIds);
|
||||
|
||||
const sellableItemsIds = sellableItems.map((item: IItem) => 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 {number} tenantId
|
||||
* @param {IItemEntry} entries - Items entries.
|
||||
* @param {IItemEntry} oldEntries - Old items entries.
|
||||
*/
|
||||
public async changeItemsQuantity(
|
||||
tenantId: number,
|
||||
entries: IItemEntry[],
|
||||
oldEntries?: IItemEntry[]
|
||||
): Promise<void> {
|
||||
const { itemRepository } = this.tenancy.repositories(tenantId);
|
||||
const opers = [];
|
||||
|
||||
const diffEntries = entriesAmountDiff(
|
||||
entries,
|
||||
oldEntries,
|
||||
'quantity',
|
||||
'itemId'
|
||||
);
|
||||
diffEntries.forEach((entry: IItemEntry) => {
|
||||
const changeQuantityOper = itemRepository.changeNumber(
|
||||
{ id: entry.itemId, type: 'inventory' },
|
||||
'quantityOnHand',
|
||||
entry.quantity
|
||||
);
|
||||
opers.push(changeQuantityOper);
|
||||
});
|
||||
await Promise.all(opers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment items quantity from the given items entries.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {IItemEntry} entries - Items entries.
|
||||
*/
|
||||
public async incrementItemsEntries(
|
||||
tenantId: number,
|
||||
entries: IItemEntry[]
|
||||
): Promise<void> {
|
||||
return this.changeItemsQuantity(tenantId, entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement items quantity from the given items entries.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {IItemEntry} entries - Items entries.
|
||||
*/
|
||||
public async decrementItemsQuantity(
|
||||
tenantId: number,
|
||||
entries: IItemEntry[]
|
||||
): Promise<void> {
|
||||
return this.changeItemsQuantity(
|
||||
tenantId,
|
||||
entries.map((entry) => ({
|
||||
...entry,
|
||||
quantity: entry.quantity * -1,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the cost/sell accounts to the invoice entries.
|
||||
*/
|
||||
setItemsEntriesDefaultAccounts(tenantId: number) {
|
||||
return async (entries: IItemEntry[]) => {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
|
||||
const entriesItemsIds = entries.map((e) => e.itemId);
|
||||
const items = await Item.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 entries
|
||||
* @returns
|
||||
*/
|
||||
getTotalItemsEntries(entries: ItemEntry[]): number {
|
||||
return sumBy(entries, (e) => ItemEntry.calcAmount(e));
|
||||
}
|
||||
}
|
||||
57
packages/server/src/services/Items/constants.ts
Normal file
57
packages/server/src/services/Items/constants.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
|
||||
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'
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
]
|
||||
Reference in New Issue
Block a user