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('/', [ router.post('/', [
...this.validateItemSchema, ...this.validateItemSchema,
...this.validateNewItemSchema,
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.newItem.bind(this)), asyncMiddleware(this.newItem.bind(this)),
@@ -90,6 +91,17 @@ export default class ItemsController extends BaseController {
return router; 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. * Validate item schema.
*/ */

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,15 @@
import { defaultTo, difference } from 'lodash'; import { defaultTo, difference } from 'lodash';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import events from 'subscribers/events';
import { IItemsFilter, IItemsService, IItemDTO, IItem } from 'interfaces'; import { IItemsFilter, IItemsService, IItemDTO, IItem } from 'interfaces';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import InventoryService from 'services/Inventory/Inventory';
const ERRORS = { const ERRORS = {
NOT_FOUND: 'NOT_FOUND', NOT_FOUND: 'NOT_FOUND',
@@ -32,6 +38,12 @@ export default class ItemsService implements IItemsService {
@Inject('logger') @Inject('logger')
logger: any; logger: any;
@Inject()
inventoryService: InventoryService;
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
/** /**
* Retrieve item details or throw not found error. * Retrieve item details or throw not found error.
* @param {number} tenantId * @param {number} tenantId
@@ -248,17 +260,55 @@ export default class ItemsService implements IItemsService {
itemDTO.inventoryAccountId itemDTO.inventoryAccountId
); );
} }
const storedItem = await Item.query().insertAndFetch({ const item = await Item.query().insertAndFetch({
...itemDTO, ...itemDTO,
active: defaultTo(itemDTO.active, 1), 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.', { this.logger.info('[items] item inserted successfully.', {
tenantId, tenantId,
itemDTO, 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 service.
*/ */
items: { item: {
onCreated: 'onItemCreated', onCreated: 'onItemCreated',
onEdited: 'onItemEdited', onEdited: 'onItemEdited',
onDeleted: 'onItemDeleted', 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,
)
}
}