feat: add opening quantity, cost and date to items.

This commit is contained in:
a.bouhuolia
2020-12-23 21:31:17 +02:00
parent b07bb2df53
commit 47b40f6940
9 changed files with 146 additions and 18 deletions

View File

@@ -25,6 +25,7 @@ export default class ItemsController extends BaseController {
router.post('/', [
...this.validateItemSchema,
...this.validateNewItemSchema,
],
this.validationResult,
asyncMiddleware(this.newItem.bind(this)),
@@ -90,6 +91,17 @@ export default class ItemsController extends BaseController {
return router;
}
/**
* New item validation schema.
*/
get validateNewItemSchema(): ValidationChain[] {
return [
check('opening_quantity').default(0).isInt({ min: 0 }).toInt(),
check('opening_cost').optional({ nullable: true }).isFloat({ min: 0 }).toFloat(),
check('opening_date').optional({ nullable: true }).isISO8601(),
];
}
/**
* Validate item schema.
*/

View File

@@ -17,6 +17,11 @@ exports.up = function (knex) {
table.text('sell_description').nullable();
table.text('purchase_description').nullable();
table.integer('quantity_on_hand');
table.integer('opening_quantity');
table.decimal('opening_cost', 13, 3);
table.date('opening_date');
table.text('note').nullable();
table.boolean('active');
table.integer('category_id').unsigned().index().references('id').inTable('items_categories');

View File

@@ -8,7 +8,7 @@ export interface IInventoryTransaction {
quantity: number,
rate: number,
transactionType: string,
transactionId: string,
transactionId: number,
lotNumber: string,
entryId: number
};

View File

@@ -21,6 +21,11 @@ export interface IItem{
purchaseDescription: string,
quantityOnHand: number,
openingQuantity: number,
openingCost: number,
openingDate: Date,
note: string,
active: boolean,
@@ -52,6 +57,11 @@ export interface IItemDTO {
purchaseDescription: string,
quantityOnHand: number,
openingQuantity?: number,
openingCost?: number,
openingDate?: Date,
note: string,
active: boolean,

View File

@@ -13,4 +13,5 @@ import 'subscribers/paymentMades';
import 'subscribers/paymentReceives';
import 'subscribers/saleEstimates';
import 'subscribers/saleReceipts';
import 'subscribers/inventory';
import 'subscribers/inventory';
import 'subscribers/items';

View File

@@ -127,20 +127,38 @@ export default class InventoryService {
inventoryEntries: IInventoryTransaction[],
deleteOld: boolean,
): Promise<void> {
inventoryEntries.forEach(async (entry: IInventoryTransaction) => {
await this.recordInventoryTransaction(
tenantId,
entry,
deleteOld,
);
});
}
/**
*
* @param {number} tenantId
* @param {IInventoryTransaction} inventoryEntry
* @param {boolean} deleteOld
*/
async recordInventoryTransaction(
tenantId: number,
inventoryEntry: IInventoryTransaction,
deleteOld: boolean = false,
) {
const { InventoryTransaction, Item } = this.tenancy.models(tenantId);
inventoryEntries.forEach(async (entry: any) => {
if (deleteOld) {
await this.deleteInventoryTransactions(
tenantId,
entry.transactionId,
entry.transactionType,
);
}
await InventoryTransaction.query().insert({
...entry,
lotNumber: entry.lotNumber,
});
if (deleteOld) {
await this.deleteInventoryTransactions(
tenantId,
inventoryEntry.transactionId,
inventoryEntry.transactionType,
);
}
await InventoryTransaction.query().insert({
...inventoryEntry,
lotNumber: inventoryEntry.lotNumber,
});
}

View File

@@ -1,9 +1,15 @@
import { defaultTo, difference } from 'lodash';
import { Service, Inject } from 'typedi';
import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import events from 'subscribers/events';
import { IItemsFilter, IItemsService, IItemDTO, IItem } from 'interfaces';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import TenancyService from 'services/Tenancy/TenancyService';
import { ServiceError } from 'exceptions';
import InventoryService from 'services/Inventory/Inventory';
const ERRORS = {
NOT_FOUND: 'NOT_FOUND',
@@ -32,6 +38,12 @@ export default class ItemsService implements IItemsService {
@Inject('logger')
logger: any;
@Inject()
inventoryService: InventoryService;
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
/**
* Retrieve item details or throw not found error.
* @param {number} tenantId
@@ -248,17 +260,55 @@ export default class ItemsService implements IItemsService {
itemDTO.inventoryAccountId
);
}
const storedItem = await Item.query().insertAndFetch({
const item = await Item.query().insertAndFetch({
...itemDTO,
active: defaultTo(itemDTO.active, 1),
quantityOnHand: itemDTO.type === 'inventory' ? 0 : null,
quantityOnHand:
itemDTO.type === 'inventory'
? defaultTo(itemDTO.openingQuantity, 0)
: null,
});
this.logger.info('[items] item inserted successfully.', {
tenantId,
itemDTO,
});
// Triggers `onItemCreated` event.
await this.eventDispatcher.dispatch(events.item.onCreated, {
tenantId,
item,
itemId: item.id,
});
return item;
}
return storedItem;
/**
* Records the opening items inventory transaction.
* @param {number} tenantId
* @param itemId
* @param openingQuantity
* @param openingCost
* @param openingDate
*/
public async recordOpeningItemsInventoryTransaction(
tenantId: number,
itemId: number,
openingQuantity: number,
openingCost: number,
openingDate: Date
): Promise<void> {
// Gets the next inventory lot number.
const lotNumber = this.inventoryService.getNextLotNumber(tenantId);
await this.inventoryService.recordInventoryTransaction(tenantId, {
date: openingDate,
quantity: openingQuantity,
rate: openingCost,
direction: 'IN',
transactionType: 'OpeningItem',
itemId,
lotNumber,
});
await this.inventoryService.incrementNextLotNumber(tenantId);
}
/**

View File

@@ -168,7 +168,7 @@ export default {
/**
* Items service.
*/
items: {
item: {
onCreated: 'onItemCreated',
onEdited: 'onItemEdited',
onDeleted: 'onItemDeleted',

View File

@@ -0,0 +1,32 @@
import { Container } from 'typedi';
import { EventSubscriber, On } from 'event-dispatch';
import events from 'subscribers/events';
import ItemsService from 'services/Items/ItemsService';
@EventSubscriber()
export default class ItemsSubscriber{
itemsService: ItemsService;
constructor() {
this.itemsService = Container.get(ItemsService);
};
/**
* Handle writing opening item inventory transaction.
*/
@On(events.item.onCreated)
handleWriteOpeningInventoryTransaction({ tenantId, item }) {
// Can't continue if the opeing cost, quantity or opening date was empty.
if (!item.openingCost || !item.openingQuantity || !item.openingDate) {
return;
}
// Records the opeing items inventory transaction once the item created.
this.itemsService.recordOpeningItemsInventoryTransaction(
tenantId,
item.id,
item.openingQuantity,
item.openingCost,
item.openingDate,
)
}
}