Merge pull request #15 from kfnawaz/feat/inventory-policies

Feat: Inventory Policies
This commit is contained in:
Sathishkumar Krishnan
2022-01-07 09:05:59 +05:30
committed by GitHub
9 changed files with 441 additions and 36 deletions

View File

@@ -68,6 +68,8 @@ const ItemTransactionTypes = [
"RESERVE"
];
const ReportItemForTypes = ["LOCATION", "ISSUE", "INCIDENT"];
module.exports = {
UserActions,
InventoryScopes,
@@ -85,4 +87,5 @@ module.exports = {
AUTHENTICATION_FAILURE_ERROR_MESSAGE,
AUTHORIZATION_FAILURE_ERROR_MESSAGE,
ItemTransactionTypes,
ReportItemForTypes,
};

View File

@@ -163,14 +163,34 @@ const createSublevels = async (subLevels, level, parent = undefined, depth = 0)
return sub_levels_list;
};
const createInventory = async ({ name, type }) => {
const createInventory = async ({ name, type, policies }) => {
if (!(name && type)) {
return;
}
const preferredLocations = [];
if (policies.preferredLocations && Array.isArray(policies.preferredLocations)) {
for (const preferredLocation of policies.preferredLocations) {
preferredLocations.push({ id: preferredLocation.id, type: preferredLocation.type });
}
}
const verifiedPolicies = {
orderTracking: policies.orderTracking || {},
alerting: {
lowestStockLevel: policies.alerting && policies.alerting.lowestStockLevel ? policies.alerting.lowestStockLevel : false,
highestStockLevel: policies.alerting && policies.alerting.highestStockLevel ? policies.alerting.highestStockLevel : false,
alertStockLevel: policies.alerting && policies.alerting.alertStockLevel ? policies.alerting.alertStockLevel : false,
reOrderLevel: policies.alerting && policies.alerting.reOrderLevel ? policies.alerting.reOrderLevel : false,
},
replenishment: policies.replenishment || {},
preferredLocations: preferredLocations,
};
return await Inventory.create({
name,
type,
policies: verifiedPolicies,
});
};

View File

@@ -29,17 +29,36 @@ module.exports = {
* Create a Inventory
*/
createInventory: async (req, res, next) => {
const { name, type } = req.body;
const { name, type, policies } = req.body;
if (!(name && type)) {
res.status(400).send("Missing params param");
return;
}
const preferredLocations = [];
if (policies.preferredLocations && Array.isArray(policies.preferredLocations)) {
for (const preferredLocation of policies.preferredLocations) {
preferredLocations.push({ id: preferredLocation.id, type: preferredLocation.type });
}
}
const verifiedPolicies = {
orderTracking: policies.orderTracking || {},
alerting: {
lowestStockLevel: policies.alerting && policies.alerting.lowestStockLevel ? policies.alerting.lowestStockLevel : false,
highestStockLevel: policies.alerting && policies.alerting.highestStockLevel ? policies.alerting.highestStockLevel : false,
alertStockLevel: policies.alerting && policies.alerting.alertStockLevel ? policies.alerting.alertStockLevel : false,
reOrderLevel: policies.alerting && policies.alerting.reOrderLevel ? policies.alerting.reOrderLevel : false,
},
replenishment: policies.replenishment || {},
preferredLocations: preferredLocations,
};
try {
const inventoryData = new Inventory({
name,
type,
policies: verifiedPolicies,
});
await inventoryData.save();
@@ -64,7 +83,7 @@ module.exports = {
return;
}
const { name, type } = req.body;
const { name, type, policies } = req.body;
if (!(name || type)) {
res.status(400).send("Missing data in body");
@@ -81,6 +100,37 @@ module.exports = {
if (name) inventoryData.name = name;
if (type) inventoryData.type = type;
if (policies) {
const preferredLocations = [];
if (policies.preferredLocations && Array.isArray(policies.preferredLocations)) {
for (const preferredLocation of policies.preferredLocations) {
preferredLocations.push({ id: preferredLocation.id, type: preferredLocation.type });
}
}
inventoryData.policies = {
orderTracking: policies.orderTracking || inventoryData.policies.orderTracking,
alerting: {
lowestStockLevel:
policies.alerting && policies.alerting.lowestStockLevel
? policies.alerting.lowestStockLevel
: inventoryData.policies.alerting.lowestStockLevel,
highestStockLevel:
policies.alerting && policies.alerting.highestStockLevel
? policies.alerting.highestStockLevel
: inventoryData.policies.alerting.highestStockLevel,
alertStockLevel:
policies.alerting && policies.alerting.alertStockLevel
? policies.alerting.alertStockLevel
: inventoryData.policies.alerting.alertStockLevel,
reOrderLevel:
policies.alerting && policies.alerting.reOrderLevel ? policies.alerting.reOrderLevel : inventoryData.policies.alerting.reOrderLevel,
},
replenishment: policies.replenishment || inventoryData.policies.replenishment,
preferredLocations: preferredLocations,
};
}
await inventoryData.save();
res.send({ success: true, data: inventoryData });
} catch (error) {

View File

@@ -2,7 +2,19 @@ const mongoose = require("mongoose");
const Item = require("../models/Item");
const WidgetFamily = require("../models/WidgetFamily");
const Inventory = require("../models/Inventory");
const { InventoryTypes } = require("../config/constants");
const {
PickItemTransaction,
PutItemTransaction,
ReserveItemTransaction,
CheckInItemTransaction,
CheckOutItemTransaction,
ReportItemTransaction,
AdjustItemTransaction,
} = require("../models/ItemTransaction");
const ItemAssociation = require("../models/ItemAssociation");
const Sublevel = require("../models/Sublevel");
const { InventoryTypes, ReportItemForTypes } = require("../config/constants");
module.exports = {
/**
@@ -194,4 +206,260 @@ module.exports = {
next(error);
}
},
putItem: async (req, res, next) => {
try {
const { id } = req.params;
if (!id || mongoose.isValidObjectId(id)) {
res.status(400).send("Missing/Invalid id param");
return;
}
const item = await Item.findById(id);
if (!item) {
res.status(404).send("item not found");
return;
}
const { putQuantity, subLevel } = req.body;
if (!(putQuantity && putQuantity > 0) || !(subLevel && mongoose.isValidObjectId(subLevel))) {
res.status(400).send("Invalid value for putQuantity/subLevel");
return;
}
const subLevelObj = await Sublevel.findById(subLevel);
const itemAssociation = await ItemAssociation.findOne({ item_id: item._id, sub_level_id: subLevelObj._id });
itemAssociation.totalQuantity = itemAssociation.totalQuantity + putQuantity;
itemAssociation.availableQuantity = itemAssociation.availableQuantity + putQuantity;
await itemAssociation.save();
await PutItemTransaction.create({
type: "PUT",
performedOn: item,
performedBy: res.locals.user,
putQuantity: putQuantity,
subLevel: subLevelObj,
});
} catch (error) {
next(error);
}
},
pickItem: async (req, res, next) => {
try {
const { id } = req.params;
if (!id || mongoose.isValidObjectId(id)) {
res.status(400).send("Missing/Invalid id param");
return;
}
const item = await Item.findById(id);
if (!item) {
res.status(404).send("item not found");
return;
}
const { pickupQuantity, subLevel } = req.body;
if (!(pickupQuantity && pickupQuantity > 0) || !(subLevel && mongoose.isValidObjectId(subLevel))) {
res.status(400).send("Invalid value for pickupQuantity/subLevel");
return;
}
const subLevelObj = await Sublevel.findById(subLevel);
const itemAssociation = await ItemAssociation.findOne({ item_id: item._id, sub_level_id: subLevelObj._id });
itemAssociation.totalQuantity = itemAssociation.totalQuantity - pickupQuantity;
itemAssociation.availableQuantity = itemAssociation.availableQuantity - pickupQuantity;
await itemAssociation.save();
await PickItemTransaction.create({
type: "PICK",
performedOn: item,
performedBy: res.locals.user,
pickupQuantity: pickupQuantity,
subLevel: subLevelObj,
});
} catch (error) {
next(error);
}
},
reserveItem: async (req, res, next) => {
try {
const { id } = req.params;
if (!id || mongoose.isValidObjectId(id)) {
res.status(400).send("Missing/Invalid id param");
return;
}
const item = await Item.findById(id);
if (!item) {
res.status(404).send("item not found");
return;
}
const { reserveQuantity, job, pickupDate } = req.body;
if (!(reserveQuantity && reserveQuantity > 0) || !(job && mongoose.isValidObjectId(job)) || !(pickupDate && Date.parse(pickupDate))) {
res.status(400).send("Invalid value for reserveQuantity/job/pickupDate");
return;
}
const itemAssociation = await ItemAssociation.findOne({ item_id: item._id, availableQuantity: { $gte: reserveQuantity } });
itemAssociation.reservedQuantity = itemAssociation.reservedQuantity + reserveQuantity;
itemAssociation.availableQuantity = itemAssociation.availableQuantity - reserveQuantity;
await itemAssociation.save();
await ReserveItemTransaction.create({
type: "RESERVE",
performedOn: item,
performedBy: res.locals.user,
reserveQuantity: reserveQuantity,
job: job,
pickupDate: Date.parse(pickupDate),
});
} catch (error) {
next(error);
}
},
checkInItem: async (req, res, next) => {
try {
const { id } = req.params;
if (!id || mongoose.isValidObjectId(id)) {
res.status(400).send("Missing/Invalid id param");
return;
}
const item = await Item.findById(id);
if (!item) {
res.status(404).send("item not found");
return;
}
const { checkInMeterReading, hasIssue, issueDescription } = req.body;
if (!(checkInMeterReading && checkInMeterReading > 0)) {
res.status(400).send("Invalid value for checkInMeterReading");
return;
}
await CheckInItemTransaction.create({
type: "CHECK-IN",
performedOn: item,
performedBy: res.locals.user,
checkInMeterReading: checkInMeterReading,
hasIssue: hasIssue,
issueDescription: hasIssue ? issueDescription : "",
});
} catch (error) {
next(error);
}
},
checkOutItem: async (req, res, next) => {
try {
const { id } = req.params;
if (!id || mongoose.isValidObjectId(id)) {
res.status(400).send("Missing/Invalid id param");
return;
}
const item = await Item.findById(id);
if (!item) {
res.status(404).send("item not found");
return;
}
const { checkOutMeterReading, job, usageReason } = req.body;
if (!(checkOutMeterReading && checkOutMeterReading > 0) || !(job && mongoose.isValidObjectId(job))) {
res.status(400).send("Invalid value for checkOutMeterReading/job");
return;
}
await CheckOutItemTransaction.create({
type: "CHECK-OUT",
performedOn: item,
performedBy: res.locals.user,
checkOutMeterReading: checkOutMeterReading,
job: job,
usageReason: usageReason ? usageReason : "",
});
} catch (error) {
next(error);
}
},
reportItem: async (req, res, next) => {
try {
const { id } = req.params;
if (!id || mongoose.isValidObjectId(id)) {
res.status(400).send("Missing/Invalid id param");
return;
}
const item = await Item.findById(id);
if (!item) {
res.status(404).send("item not found");
return;
}
const { reportingFor, details } = req.body;
if (!(reportingFor && ReportItemForTypes.includes(reportingFor))) {
res.status(400).send("Invalid value for checkOutMeterReading/job");
return;
}
await ReportItemTransaction.create({
type: "REPORT",
performedOn: item,
performedBy: res.locals.user,
reportingFor: reportingFor,
details: details ? details : "",
});
} catch (error) {
next(error);
}
},
adjustItem: async (req, res, next) => {
try {
const { id } = req.params;
if (!id || mongoose.isValidObjectId(id)) {
res.status(400).send("Missing/Invalid id param");
return;
}
const item = await Item.findById(id);
if (!item) {
res.status(404).send("item not found");
return;
}
const { recountedQuantity, damagedQuantity, subLevel } = req.body;
if (!(recountedQuantity && recountedQuantity > 0) || !(subLevel && mongoose.isValidObjectId(subLevel))) {
res.status(400).send("Invalid value for pickupQuantity/subLevel");
return;
}
const subLevelObj = await Sublevel.findById(subLevel);
const itemAssociation = await ItemAssociation.findOne({ item_id: item._id, sub_level_id: subLevelObj._id });
const lastRecordedQuantity = itemAssociation.totalQuantity;
const varianceRecordedInQuantity = itemAssociation.totalQuantity - recountedQuantity;
const totalAdjustment = varianceRecordedInQuantity + damagedQuantity;
itemAssociation.totalQuantity = itemAssociation.totalQuantity - totalAdjustment;
itemAssociation.availableQuantity = itemAssociation.availableQuantity - totalAdjustment;
await itemAssociation.save();
await AdjustItemTransaction.create({
type: "ADJUST",
performedOn: item,
performedBy: res.locals.user,
lastRecordedQuantity,
recountedQuantity,
varianceRecordedInQuantity,
damagedQuantity,
totalAdjustment,
newAdjustedQuantity: itemAssociation.totalQuantity,
});
} catch (error) {
next(error);
}
},
};

View File

@@ -1,6 +1,6 @@
const router = require("express").Router();
const controller = require("./item.controller");
const { AuthenticateMiddleware, ItemTransactionCheck } = require("./utils/authorize");
/**
* @route /item/
*/
@@ -21,4 +21,39 @@ router.get("/filter", controller.getItemsByFilter);
*/
router.get("/:id", controller.getItemByID);
/**
* @route /item/:id/pick
*/
router.post("/:id/pick", AuthenticateMiddleware, ItemTransactionCheck, controller.pickItem);
/**
* @route /item/:id/put
*/
router.post("/:id/put", AuthenticateMiddleware, ItemTransactionCheck, controller.putItem);
/**
* @route /item/:id/reserve
*/
router.post("/:id/reserve", AuthenticateMiddleware, ItemTransactionCheck, controller.reserveItem);
/**
* @route /item/:id/check-in
*/
router.post("/:id/check-in", AuthenticateMiddleware, ItemTransactionCheck, controller.checkInItem);
/**
* @route /item/:id/check-out
*/
router.post("/:id/check-out", AuthenticateMiddleware, ItemTransactionCheck, controller.checkOutItem);
/**
* @route /item/:id/report
*/
router.post("/:id/report", AuthenticateMiddleware, ItemTransactionCheck, controller.reportItem);
/**
* @route /item/:id/adjust
*/
router.post("/:id/adjust", AuthenticateMiddleware, ItemTransactionCheck, controller.adjustItem);
module.exports = router;

View File

@@ -1,38 +1,22 @@
const jwt = require("jsonwebtoken");
const User = require("../../models/User");
const UserRole = require("../../models/UserRole");
const {
SUPER_ADMIN_ROLE,
AUTHORIZATION_FAILURE_ERROR_MESSAGE,
} = require("../../config/constants");
const { SUPER_ADMIN_ROLE, AUTHORIZATION_FAILURE_ERROR_MESSAGE } = require("../../config/constants");
const { JWT_SECRET } = require("../../config/env");
const constants = require("../../config/constants");
const authenticate = async (token) => {
const decodedToken = jwt.verify(token, JWT_SECRET);
if (decodedToken) {
return await User.findById(decodedToken.id)
.populate({ path: "roles", populate: "permissions" })
.populate("permissions");
return await User.findById(decodedToken.id).populate({ path: "roles", populate: "permissions" }).populate("permissions");
}
};
const authorize = async (
user,
requiredRoles = [],
requiredPermissions = []
) => {
const authorize = async (user, requiredRoles = [], requiredPermissions = []) => {
const userRoles = user.roles.map((_) => _._id);
const userPermissions = [
...user.permissions.map((_) => _._id),
...userRoles.map((_) => _.permissions).flat(),
];
const userPermissions = [...user.permissions.map((_) => _._id), ...userRoles.map((_) => _.permissions).flat()];
return (
user != undefined &&
requiredRoles.every((_) => userRoles.includes(_)) &&
requiredPermissions.every((_) => userPermissions.includes(_))
);
return user != undefined && requiredRoles.every((_) => userRoles.includes(_)) && requiredPermissions.every((_) => userPermissions.includes(_));
};
module.exports = {
@@ -41,11 +25,13 @@ module.exports = {
if (authorize(res.locals.user, [SuperAdmin.id])) {
next();
} else {
res
.status(403)
.send({ success: false, error: AUTHORIZATION_FAILURE_ERROR_MESSAGE });
res.status(403).send({ success: false, error: AUTHORIZATION_FAILURE_ERROR_MESSAGE });
}
},
ItemTransactionCheck: async (req, res, next) => {
// WIP
next();
},
AuthenticateMiddleware: async (req, res, next) => {
try {
const token = req.headers.authorization || "";

View File

@@ -1,5 +1,5 @@
const mongoose = require("mongoose");
const { InventoryTypes } = require("./../config/constants");
const { InventoryTypes, WarehouseScopes } = require("./../config/constants");
const schema = new mongoose.Schema(
{
@@ -15,15 +15,42 @@ const schema = new mongoose.Schema(
enum: InventoryTypes,
},
policies: {
tracking: {
orderTracking: {
type: Object, // Create a different model and reference it here once more details available
},
alerting: {
type: Object, // Create a different model and reference it here once more details available
lowestStockLevel: {
type: Boolean,
required: true,
},
highestStockLevel: {
type: Boolean,
required: true,
},
alertStockLevel: {
type: Boolean,
required: true,
},
reOrderLevel: {
type: Boolean,
required: true,
},
},
replenishment: {
type: Object, // Create a different model and reference it here once more details available
},
preferredLocations: [
{
id: {
type: mongoose.Schema.Types.ObjectId,
refPath: "type",
},
type: {
type: String,
enum: WarehouseScopes,
},
},
],
},
},
{

View File

@@ -10,7 +10,15 @@ const schema = new mongoose.Schema(
type: mongoose.Schema.Types.ObjectId,
ref: "Sublevel",
},
quantity: {
totalQuantity: {
type: Number,
default: 0,
},
reservedQuantity: {
type: Number,
default: 0,
},
availableQuantity: {
type: Number,
default: 0,
},

View File

@@ -1,5 +1,5 @@
const mongoose = require("mongoose");
const { ItemTransactionTypes } = require("../config/constants");
const { ItemTransactionTypes, ReportItemForTypes } = require("../config/constants");
const schema = new mongoose.Schema(
{
@@ -33,6 +33,10 @@ const PutItemTransaction = ItemTransaction.discriminator(
type: Number,
required: true,
},
subLevel: {
type: mongoose.Schema.Types.ObjectId,
ref: "Sublevel"
},
})
);
@@ -43,6 +47,10 @@ const PickItemTransaction = ItemTransaction.discriminator(
type: Number,
required: true,
},
subLevel: {
type: mongoose.Schema.Types.ObjectId,
ref: "Sublevel",
},
})
);
@@ -103,10 +111,10 @@ const CheckOutItemTransaction = ItemTransaction.discriminator(
const ReportItemTransaction = ItemTransaction.discriminator(
"Report",
new mongoose.Schema({
for: {
reportingFor: {
type: String,
required: true,
enum: ["LOCATION", "ISSUE", "INCIDENT"]
enum: ReportItemForTypes,
},
details: {
type: String,