From d88da6939dc9fcf34d09258e4536e05636ef26db Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sun, 7 Dec 2025 02:36:55 +0000 Subject: [PATCH] Added order items, shipments etc --- src/routes/inventory/orderitems.js | 50 +++++ src/routes/inventory/shipments.js | 64 ++++++ src/schemas/inventory/orderitem.schema.js | 32 +++ src/schemas/inventory/partstock.schema.js | 3 +- src/schemas/inventory/purchaseorder.schema.js | 2 +- src/schemas/inventory/shipment.schema.js | 50 +++++ src/schemas/management/host.schema.js | 33 +-- src/services/inventory/orderitems.js | 209 ++++++++++++++++++ src/services/inventory/shipments.js | 177 +++++++++++++++ 9 files changed, 603 insertions(+), 17 deletions(-) create mode 100644 src/routes/inventory/orderitems.js create mode 100644 src/routes/inventory/shipments.js create mode 100644 src/schemas/inventory/orderitem.schema.js create mode 100644 src/schemas/inventory/shipment.schema.js create mode 100644 src/services/inventory/orderitems.js create mode 100644 src/services/inventory/shipments.js diff --git a/src/routes/inventory/orderitems.js b/src/routes/inventory/orderitems.js new file mode 100644 index 0000000..b4a8679 --- /dev/null +++ b/src/routes/inventory/orderitems.js @@ -0,0 +1,50 @@ +import express from 'express'; +import { isAuthenticated } from '../../keycloak.js'; +import { getFilter, convertPropertiesString } from '../../utils.js'; + +const router = express.Router(); +import { + listOrderItemsRouteHandler, + getOrderItemRouteHandler, + editOrderItemRouteHandler, + newOrderItemRouteHandler, + deleteOrderItemRouteHandler, + listOrderItemsByPropertiesRouteHandler, +} from '../../services/inventory/orderitems.js'; + +// list of order items +router.get('/', isAuthenticated, (req, res) => { + const { page, limit, property, search, sort, order } = req.query; + const allowedFilters = ['itemType', 'item', 'item._id', 'order', 'order._id', 'orderType']; + const filter = getFilter(req.query, allowedFilters); + listOrderItemsRouteHandler(req, res, page, limit, property, filter, search, sort, order); +}); + +router.get('/properties', isAuthenticated, (req, res) => { + let properties = convertPropertiesString(req.query.properties); + const allowedFilters = ['itemType', 'item', 'item._id', 'order', 'order._id', 'orderType']; + const filter = getFilter(req.query, allowedFilters, false); + var masterFilter = {}; + if (req.query.masterFilter) { + masterFilter = JSON.parse(req.query.masterFilter); + } + listOrderItemsByPropertiesRouteHandler(req, res, properties, filter, masterFilter); +}); + +router.post('/', isAuthenticated, (req, res) => { + newOrderItemRouteHandler(req, res); +}); + +router.get('/:id', isAuthenticated, (req, res) => { + getOrderItemRouteHandler(req, res); +}); + +router.put('/:id', isAuthenticated, async (req, res) => { + editOrderItemRouteHandler(req, res); +}); + +router.delete('/:id', isAuthenticated, async (req, res) => { + deleteOrderItemRouteHandler(req, res); +}); + +export default router; diff --git a/src/routes/inventory/shipments.js b/src/routes/inventory/shipments.js new file mode 100644 index 0000000..2023298 --- /dev/null +++ b/src/routes/inventory/shipments.js @@ -0,0 +1,64 @@ +import express from 'express'; +import { isAuthenticated } from '../../keycloak.js'; +import { getFilter, convertPropertiesString } from '../../utils.js'; + +const router = express.Router(); +import { + listShipmentsRouteHandler, + getShipmentRouteHandler, + editShipmentRouteHandler, + newShipmentRouteHandler, + deleteShipmentRouteHandler, + listShipmentsByPropertiesRouteHandler, +} from '../../services/inventory/shipments.js'; + +// list of shipments +router.get('/', isAuthenticated, (req, res) => { + const { page, limit, property, search, sort, order } = req.query; + const allowedFilters = [ + 'vendor', + 'purchaseOrder', + 'state', + 'courierService', + 'vendor._id', + 'purchaseOrder._id', + ]; + const filter = getFilter(req.query, allowedFilters); + listShipmentsRouteHandler(req, res, page, limit, property, filter, search, sort, order); +}); + +router.get('/properties', isAuthenticated, (req, res) => { + let properties = convertPropertiesString(req.query.properties); + const allowedFilters = [ + 'vendor', + 'purchaseOrder', + 'state.type', + 'courierService', + 'vendor._id', + 'purchaseOrder._id', + ]; + const filter = getFilter(req.query, allowedFilters, false); + var masterFilter = {}; + if (req.query.masterFilter) { + masterFilter = JSON.parse(req.query.masterFilter); + } + listShipmentsByPropertiesRouteHandler(req, res, properties, filter, masterFilter); +}); + +router.post('/', isAuthenticated, (req, res) => { + newShipmentRouteHandler(req, res); +}); + +router.get('/:id', isAuthenticated, (req, res) => { + getShipmentRouteHandler(req, res); +}); + +router.put('/:id', isAuthenticated, async (req, res) => { + editShipmentRouteHandler(req, res); +}); + +router.delete('/:id', isAuthenticated, async (req, res) => { + deleteShipmentRouteHandler(req, res); +}); + +export default router; diff --git a/src/schemas/inventory/orderitem.schema.js b/src/schemas/inventory/orderitem.schema.js new file mode 100644 index 0000000..42a68f8 --- /dev/null +++ b/src/schemas/inventory/orderitem.schema.js @@ -0,0 +1,32 @@ +import mongoose from 'mongoose'; +import { generateId } from '../../utils.js'; +const { Schema } = mongoose; + +const orderItemSchema = new Schema( + { + _reference: { type: String, default: () => generateId()() }, + orderType: { type: String, required: true }, + order: { type: Schema.Types.ObjectId, refPath: 'orderType', required: true }, + itemType: { type: String, required: true }, + item: { type: Schema.Types.ObjectId, refPath: 'itemType', required: true }, + syncAmount: { type: String, required: true, default: null }, + itemAmount: { type: Number, required: true }, + quantity: { type: Number, required: true }, + totalAmount: { type: Number, required: true }, + taxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false }, + totalAmountWithTax: { type: Number, required: true }, + timestamp: { type: Date, default: Date.now }, + }, + { timestamps: true } +); + +// Add virtual id getter +orderItemSchema.virtual('id').get(function () { + return this._id; +}); + +// Configure JSON serialization to include virtuals +orderItemSchema.set('toJSON', { virtuals: true }); + +// Create and export the model +export const orderItemModel = mongoose.model('orderItem', orderItemSchema); diff --git a/src/schemas/inventory/partstock.schema.js b/src/schemas/inventory/partstock.schema.js index 3b8ebc6..584c7a9 100644 --- a/src/schemas/inventory/partstock.schema.js +++ b/src/schemas/inventory/partstock.schema.js @@ -11,8 +11,9 @@ const partStockSchema = new Schema( progress: { type: Number, required: false }, }, part: { type: mongoose.Schema.Types.ObjectId, ref: 'part', required: true }, - startingQuantity: { type: Number, required: true }, currentQuantity: { type: Number, required: true }, + sourceType: { type: String, required: true }, + source: { type: Schema.Types.ObjectId, refPath: 'sourceType', required: true }, }, { timestamps: true } ); diff --git a/src/schemas/inventory/purchaseorder.schema.js b/src/schemas/inventory/purchaseorder.schema.js index d88fe3e..4d08762 100644 --- a/src/schemas/inventory/purchaseorder.schema.js +++ b/src/schemas/inventory/purchaseorder.schema.js @@ -4,7 +4,7 @@ const { Schema } = mongoose; const itemSchema = new Schema({ itemType: { type: String, required: true }, - item: { type: Schema.Types.ObjectId, refPath: 'itemType', required: true }, + item: { type: Schema.Types.ObjectId, refPath: 'items.itemType', required: true }, quantity: { type: Number, required: true }, itemCost: { type: Number, required: true }, totalCost: { type: Number, required: true }, diff --git a/src/schemas/inventory/shipment.schema.js b/src/schemas/inventory/shipment.schema.js new file mode 100644 index 0000000..67331ae --- /dev/null +++ b/src/schemas/inventory/shipment.schema.js @@ -0,0 +1,50 @@ +import mongoose from 'mongoose'; +import { generateId } from '../../utils.js'; +const { Schema } = mongoose; + +const shipmentItemSchema = new Schema({ + itemType: { type: String, required: true }, + item: { type: Schema.Types.ObjectId, refPath: 'itemType', required: true }, + quantity: { type: Number, required: true }, + itemCost: { type: Number, required: true }, + totalCost: { type: Number, required: true }, + totalCostWithTax: { type: Number, required: true }, + taxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false }, +}); + +const shipmentSchema = new Schema( + { + _reference: { type: String, default: () => generateId()() }, + purchaseOrder: { type: Schema.Types.ObjectId, ref: 'purchaseOrder', required: true }, + vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true }, + courierService: { type: Schema.Types.ObjectId, ref: 'courierService', required: false }, + trackingNumber: { type: String, required: false }, + items: [shipmentItemSchema], + cost: { net: { type: Number, required: true }, gross: { type: Number, required: true } }, + shippedDate: { type: Date, required: false }, + expectedDeliveryDate: { type: Date, required: false }, + actualDeliveryDate: { type: Date, required: false }, + state: { + type: { + type: String, + required: true, + default: 'pending', + enum: ['pending', 'shipped', 'in_transit', 'delivered', 'cancelled'], + }, + }, + notes: { type: String }, + timestamp: { type: Date, default: Date.now }, + }, + { timestamps: true } +); + +// Add virtual id getter +shipmentSchema.virtual('id').get(function () { + return this._id; +}); + +// Configure JSON serialization to include virtuals +shipmentSchema.set('toJSON', { virtuals: true }); + +// Create and export the model +export const shipmentModel = mongoose.model('shipment', shipmentSchema); diff --git a/src/schemas/management/host.schema.js b/src/schemas/management/host.schema.js index fa7a684..4e1098f 100644 --- a/src/schemas/management/host.schema.js +++ b/src/schemas/management/host.schema.js @@ -41,22 +41,25 @@ const deviceInfoSchema = new mongoose.Schema( { _id: false } ); -const hostSchema = new mongoose.Schema({ - _reference: { type: String, default: () => generateId()() }, - name: { required: true, type: String }, - tags: [{ required: false, type: String }], - online: { required: true, type: Boolean, default: false }, - state: { - type: { type: String, required: true, default: 'offline' }, - message: { type: String, required: false }, - percent: { type: Number, required: false }, +const hostSchema = new mongoose.Schema( + { + _reference: { type: String, default: () => generateId()() }, + name: { required: true, type: String }, + tags: [{ required: false, type: String }], + online: { required: true, type: Boolean, default: false }, + state: { + type: { type: String, required: true, default: 'offline' }, + message: { type: String, required: false }, + percent: { type: Number, required: false }, + }, + active: { required: true, type: Boolean, default: true }, + connectedAt: { required: false, type: Date }, + authCode: { type: { required: false, type: String } }, + deviceInfo: { deviceInfoSchema }, + files: [{ type: mongoose.Schema.Types.ObjectId, ref: 'file' }], }, - active: { required: true, type: Boolean, default: true }, - connectedAt: { required: false, type: Date }, - authCode: { type: { required: false, type: String } }, - deviceInfo: { deviceInfoSchema }, - files: [{ type: mongoose.Schema.Types.ObjectId, ref: 'file' }], -}); + { timestamps: true } +); hostSchema.virtual('id').get(function () { return this._id; diff --git a/src/services/inventory/orderitems.js b/src/services/inventory/orderitems.js new file mode 100644 index 0000000..3e7afeb --- /dev/null +++ b/src/services/inventory/orderitems.js @@ -0,0 +1,209 @@ +import dotenv from 'dotenv'; +import { orderItemModel } from '../../schemas/inventory/orderitem.schema.js'; +import log4js from 'log4js'; +import mongoose from 'mongoose'; +import { + deleteObject, + listObjects, + getObject, + editObject, + newObject, + listObjectsByProperties, +} from '../../database/database.js'; +dotenv.config(); + +const logger = log4js.getLogger('Order Items'); +logger.level = process.env.LOG_LEVEL; + +export const listOrderItemsRouteHandler = async ( + req, + res, + page = 1, + limit = 25, + property = '', + filter = {}, + search = '', + sort = '', + order = 'ascend' +) => { + const result = await listObjects({ + model: orderItemModel, + page, + limit, + property, + filter, + search, + sort, + order, + populate: [ + { + path: 'order', + }, + { + path: 'taxRate', + strictPopulate: false, + }, + { + path: 'item', + populate: { path: 'costTaxRate' }, + }, + { + path: 'item', + populate: { path: 'priceTaxRate' }, + }, + ], + }); + + if (result?.error) { + logger.error('Error listing order items.'); + res.status(result.code).send(result); + return; + } + + logger.debug(`List of order items (Page ${page}, Limit ${limit}). Count: ${result.length}`); + res.send(result); +}; + +export const listOrderItemsByPropertiesRouteHandler = async ( + req, + res, + properties = '', + filter = {}, + masterFilter = {} +) => { + const result = await listObjectsByProperties({ + model: orderItemModel, + properties, + filter, + populate: [], + masterFilter, + }); + + if (result?.error) { + logger.error('Error listing order items.'); + res.status(result.code).send(result); + return; + } + + logger.debug(`List of order items. Count: ${result.length}`); + res.send(result); +}; + +export const getOrderItemRouteHandler = async (req, res) => { + const id = req.params.id; + const result = await getObject({ + model: orderItemModel, + id, + populate: [ + { + path: 'order', + }, + { + path: 'taxRate', + strictPopulate: false, + }, + { + path: 'item', + populate: { path: 'costTaxRate' }, + }, + { + path: 'item', + populate: { path: 'priceTaxRate' }, + }, + ], + }); + if (result?.error) { + logger.warn(`Order Item not found with supplied id.`); + return res.status(result.code).send(result); + } + logger.debug(`Retreived order item with ID: ${id}`); + res.send(result); +}; + +export const editOrderItemRouteHandler = async (req, res) => { + // Get ID from params + const id = new mongoose.Types.ObjectId(req.params.id); + + logger.trace(`Order Item with ID: ${id}`); + + const updateData = { + updatedAt: new Date(), + purchaseOrder: req.body.purchaseOrder, + itemType: req.body.itemType, + item: req.body.item, + syncAmount: req.body.syncAmount, + itemAmount: req.body.itemAmount, + quantity: req.body.quantity, + totalAmount: req.body.totalAmount, + taxRate: req.body.taxRate, + totalAmountWithTax: req.body.totalAmountWithTax, + }; + // Create audit log before updating + const result = await editObject({ + model: orderItemModel, + id, + updateData, + user: req.user, + }); + + if (result.error) { + logger.error('Error editing order item:', result.error); + res.status(result).send(result); + return; + } + + logger.debug(`Edited order item with ID: ${id}`); + + res.send(result); +}; + +export const newOrderItemRouteHandler = async (req, res) => { + const newData = { + updatedAt: new Date(), + purchaseOrder: req.body.purchaseOrder, + itemType: req.body.itemType, + item: req.body.item, + orderType: req.body.orderType, + order: req.body.order, + syncAmount: req.body.syncAmount, + itemAmount: req.body.itemAmount, + quantity: req.body.quantity, + totalAmount: req.body.totalAmount, + taxRate: req.body.taxRate, + totalAmountWithTax: req.body.totalAmountWithTax, + }; + const result = await newObject({ + model: orderItemModel, + newData, + user: req.user, + }); + if (result.error) { + logger.error('No order item created:', result.error); + return res.status(result.code).send(result); + } + + logger.debug(`New order item with ID: ${result._id}`); + + res.send(result); +}; + +export const deleteOrderItemRouteHandler = async (req, res) => { + // Get ID from params + const id = new mongoose.Types.ObjectId(req.params.id); + + logger.trace(`Order Item with ID: ${id}`); + + const result = await deleteObject({ + model: orderItemModel, + id, + user: req.user, + }); + if (result.error) { + logger.error('No order item deleted:', result.error); + return res.status(result.code).send(result); + } + + logger.debug(`Deleted order item with ID: ${result._id}`); + + res.send(result); +}; diff --git a/src/services/inventory/shipments.js b/src/services/inventory/shipments.js new file mode 100644 index 0000000..6c8923a --- /dev/null +++ b/src/services/inventory/shipments.js @@ -0,0 +1,177 @@ +import dotenv from 'dotenv'; +import { shipmentModel } from '../../schemas/inventory/shipment.schema.js'; +import log4js from 'log4js'; +import mongoose from 'mongoose'; +import { + deleteObject, + listObjects, + getObject, + editObject, + newObject, + listObjectsByProperties, +} from '../../database/database.js'; +dotenv.config(); + +const logger = log4js.getLogger('Shipments'); +logger.level = process.env.LOG_LEVEL; + +export const listShipmentsRouteHandler = async ( + req, + res, + page = 1, + limit = 25, + property = '', + filter = {}, + search = '', + sort = '', + order = 'ascend' +) => { + const result = await listObjects({ + model: shipmentModel, + page, + limit, + property, + filter, + search, + sort, + order, + populate: ['purchaseOrder', 'vendor', 'courierService'], + }); + + if (result?.error) { + logger.error('Error listing shipments.'); + res.status(result.code).send(result); + return; + } + + logger.debug(`List of shipments (Page ${page}, Limit ${limit}). Count: ${result.length}`); + res.send(result); +}; + +export const listShipmentsByPropertiesRouteHandler = async ( + req, + res, + properties = '', + filter = {}, + masterFilter = {} +) => { + const result = await listObjectsByProperties({ + model: shipmentModel, + properties, + filter, + populate: ['purchaseOrder', 'vendor', 'courierService'], + masterFilter, + }); + + if (result?.error) { + logger.error('Error listing shipments.'); + res.status(result.code).send(result); + return; + } + + logger.debug(`List of shipments. Count: ${result.length}`); + res.send(result); +}; + +export const getShipmentRouteHandler = async (req, res) => { + const id = req.params.id; + const result = await getObject({ + model: shipmentModel, + id, + populate: ['purchaseOrder', 'vendor', 'courierService', 'items.item', 'items.taxRate'], + }); + if (result?.error) { + logger.warn(`Shipment not found with supplied id.`); + return res.status(result.code).send(result); + } + logger.debug(`Retreived shipment with ID: ${id}`); + res.send(result); +}; + +export const editShipmentRouteHandler = async (req, res) => { + // Get ID from params + const id = new mongoose.Types.ObjectId(req.params.id); + + logger.trace(`Shipment with ID: ${id}`); + + const updateData = { + updatedAt: new Date(), + purchaseOrder: req.body.purchaseOrder, + vendor: req.body.vendor, + courierService: req.body.courierService, + trackingNumber: req.body.trackingNumber, + shippedDate: req.body.shippedDate, + expectedDeliveryDate: req.body.expectedDeliveryDate, + actualDeliveryDate: req.body.actualDeliveryDate, + state: req.body.state, + notes: req.body.notes, + }; + // Create audit log before updating + const result = await editObject({ + model: shipmentModel, + id, + updateData, + user: req.user, + }); + + if (result.error) { + logger.error('Error editing shipment:', result.error); + res.status(result).send(result); + return; + } + + logger.debug(`Edited shipment with ID: ${id}`); + + res.send(result); +}; + +export const newShipmentRouteHandler = async (req, res) => { + const newData = { + updatedAt: new Date(), + purchaseOrder: req.body.purchaseOrder, + vendor: req.body.vendor, + courierService: req.body.courierService, + trackingNumber: req.body.trackingNumber, + items: req.body.items, + cost: req.body.cost, + shippedDate: req.body.shippedDate, + expectedDeliveryDate: req.body.expectedDeliveryDate, + actualDeliveryDate: req.body.actualDeliveryDate, + state: req.body.state, + notes: req.body.notes, + }; + const result = await newObject({ + model: shipmentModel, + newData, + user: req.user, + }); + if (result.error) { + logger.error('No shipment created:', result.error); + return res.status(result.code).send(result); + } + + logger.debug(`New shipment with ID: ${result._id}`); + + res.send(result); +}; + +export const deleteShipmentRouteHandler = async (req, res) => { + // Get ID from params + const id = new mongoose.Types.ObjectId(req.params.id); + + logger.trace(`Shipment with ID: ${id}`); + + const result = await deleteObject({ + model: shipmentModel, + id, + user: req.user, + }); + if (result.error) { + logger.error('No shipment deleted:', result.error); + return res.status(result.code).send(result); + } + + logger.debug(`Deleted shipment with ID: ${result._id}`); + + res.send(result); +};