mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 12:50:38 +00:00
feat: add opening quantity, cost and date to items.
This commit is contained in:
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface IInventoryTransaction {
|
||||
quantity: number,
|
||||
rate: number,
|
||||
transactionType: string,
|
||||
transactionId: string,
|
||||
transactionId: number,
|
||||
lotNumber: string,
|
||||
entryId: number
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -168,7 +168,7 @@ export default {
|
||||
/**
|
||||
* Items service.
|
||||
*/
|
||||
items: {
|
||||
item: {
|
||||
onCreated: 'onItemCreated',
|
||||
onEdited: 'onItemEdited',
|
||||
onDeleted: 'onItemDeleted',
|
||||
|
||||
32
server/src/subscribers/items.ts
Normal file
32
server/src/subscribers/items.ts
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user