mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 14:50:32 +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('/', [
|
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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -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,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ export default {
|
|||||||
/**
|
/**
|
||||||
* Items service.
|
* Items service.
|
||||||
*/
|
*/
|
||||||
items: {
|
item: {
|
||||||
onCreated: 'onItemCreated',
|
onCreated: 'onItemCreated',
|
||||||
onEdited: 'onItemEdited',
|
onEdited: 'onItemEdited',
|
||||||
onDeleted: 'onItemDeleted',
|
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