diff --git a/src/routes/inventory/purchaseorders.js b/src/routes/inventory/purchaseorders.js index 4b0160d..0977cc8 100644 --- a/src/routes/inventory/purchaseorders.js +++ b/src/routes/inventory/purchaseorders.js @@ -13,6 +13,9 @@ import { listPurchaseOrdersByPropertiesRouteHandler, getPurchaseOrderStatsRouteHandler, getPurchaseOrderHistoryRouteHandler, + postPurchaseOrderRouteHandler, + acknowledgePurchaseOrderRouteHandler, + cancelPurchaseOrderRouteHandler, } from '../../services/inventory/purchaseorders.js'; // list of purchase orders @@ -29,7 +32,7 @@ router.get('/properties', isAuthenticated, (req, res) => { const filter = getFilter(req.query, allowedFilters, false); var masterFilter = {}; if (req.query.masterFilter) { - masterFilter = JSON.parse(req.query.masterFilter); + masterFilter = getFilter(JSON.parse(req.query.masterFilter), allowedFilters, true); } listPurchaseOrdersByPropertiesRouteHandler(req, res, properties, filter, masterFilter); }); @@ -65,4 +68,16 @@ router.delete('/:id', isAuthenticated, async (req, res) => { deletePurchaseOrderRouteHandler(req, res); }); +router.post('/:id/post', isAuthenticated, async (req, res) => { + postPurchaseOrderRouteHandler(req, res); +}); + +router.post('/:id/acknowledge', isAuthenticated, async (req, res) => { + acknowledgePurchaseOrderRouteHandler(req, res); +}); + +router.post('/:id/cancel', isAuthenticated, async (req, res) => { + cancelPurchaseOrderRouteHandler(req, res); +}); + export default router; diff --git a/src/services/inventory/purchaseorders.js b/src/services/inventory/purchaseorders.js index 437f27f..2432f9a 100644 --- a/src/services/inventory/purchaseorders.js +++ b/src/services/inventory/purchaseorders.js @@ -7,11 +7,15 @@ import { listObjects, getObject, editObject, + editObjects, newObject, listObjectsByProperties, getModelStats, getModelHistory, + checkStates, } from '../../database/database.js'; +import { orderItemModel } from '../../database/schemas/inventory/orderitem.schema.js'; +import { shipmentModel } from '../../database/schemas/inventory/shipment.schema.js'; const logger = log4js.getLogger('Purchase Orders'); logger.level = config.server.logLevel; @@ -95,6 +99,20 @@ export const editPurchaseOrderRouteHandler = async (req, res) => { logger.trace(`Purchase Order with ID: ${id}`); + const checkStatesResult = await checkStates({ model: purchaseOrderModel, id, states: ['draft'] }); + + if (checkStatesResult.error) { + logger.error('Error checking purchase order states:', checkStatesResult.error); + res.status(checkStatesResult.code).send(checkStatesResult); + return; + } + + if (checkStatesResult === false) { + logger.error('Purchase order is not in draft state.'); + res.status(400).send({ error: 'Purchase order is not in draft state.', code: 400 }); + return; + } + const updateData = { updatedAt: new Date(), vendor: req.body.vendor, @@ -118,10 +136,40 @@ export const editPurchaseOrderRouteHandler = async (req, res) => { res.send(result); }; +export const editMultiplePurchaseOrdersRouteHandler = async (req, res) => { + const updates = req.body.map((update) => ({ + _id: update._id, + vendor: update.vendor, + })); + + if (!Array.isArray(updates)) { + return res.status(400).send({ error: 'Body must be an array of updates.', code: 400 }); + } + + const result = await editObjects({ + model: purchaseOrderModel, + updates, + user: req.user, + }); + + if (result.error) { + logger.error('Error editing purchase orders:', result.error); + res.status(result.code || 500).send(result); + return; + } + + logger.debug(`Edited ${updates.length} purchase orders`); + + res.send(result); +}; + export const newPurchaseOrderRouteHandler = async (req, res) => { const newData = { updatedAt: new Date(), vendor: req.body.vendor, + totalAmount: 0, + totalAmountWithTax: 0, + totalTaxAmount: 0, }; const result = await newObject({ model: purchaseOrderModel, @@ -180,3 +228,232 @@ export const getPurchaseOrderHistoryRouteHandler = async (req, res) => { logger.trace('Purchase order history:', result); res.send(result); }; + +export const postPurchaseOrderRouteHandler = async (req, res) => { + const id = new mongoose.Types.ObjectId(req.params.id); + + logger.trace(`Purchase Order with ID: ${id}`); + + const checkStatesResult = await checkStates({ model: purchaseOrderModel, id, states: ['draft'] }); + + if (checkStatesResult.error) { + logger.error('Error checking purchase order states:', checkStatesResult.error); + res.status(checkStatesResult.code).send(checkStatesResult); + return; + } + + if (checkStatesResult === false) { + logger.error('Purchase order is not in draft state.'); + res.status(400).send({ error: 'Purchase order is not in draft state.', code: 400 }); + return; + } + + const orderItemsResult = await listObjects({ + model: orderItemModel, + filter: { order: id, orderType: 'purchaseOrder' }, + pagination: false, + }); + + const shipmentsResult = await listObjects({ + model: shipmentModel, + filter: { order: id, orderType: 'purchaseOrder' }, + pagination: false, + }); + + for (const orderItem of orderItemsResult) { + if (orderItem.state.type != 'draft') { + logger.warn(`Order item ${orderItem._id} is not in draft state.`); + return res + .status(400) + .send({ error: `Order item ${orderItem._reference} not in draft state.`, code: 400 }); + } + if (!orderItem?.shipment || orderItem?.shipment == null) { + logger.warn(`Order item ${orderItem._id} does not have a shipment.`); + return res + .status(400) + .send({ error: `Order item ${orderItem._reference} does not have a shipment.`, code: 400 }); + } + } + + for (const shipment of shipmentsResult) { + if (shipment.state.type != 'draft') { + logger.warn(`Shipment ${shipment._id} is not in draft state.`); + return res + .status(400) + .send({ error: `Shipment ${shipment._reference} not in draft state.`, code: 400 }); + } + } + + for (const orderItem of orderItemsResult) { + await editObject({ + model: orderItemModel, + id: orderItem._id, + updateData: { + state: { type: 'ordered' }, + orderedAt: new Date(), + }, + user: req.user, + }); + } + + for (const shipment of shipmentsResult) { + await editObject({ + model: shipmentModel, + id: shipment._id, + updateData: { + state: { type: 'planned' }, + }, + user: req.user, + }); + } + + const updateData = { + updatedAt: new Date(), + state: { type: 'sent' }, + postedAt: new Date(), + }; + const result = await editObject({ + model: purchaseOrderModel, + id, + updateData, + user: req.user, + }); + + if (result.error) { + logger.error('Error posting purchase order:', result.error); + res.status(result.code).send(result); + return; + } + + logger.debug(`Posted purchase order with ID: ${id}`); + res.send(result); +}; + +export const acknowledgePurchaseOrderRouteHandler = async (req, res) => { + const id = new mongoose.Types.ObjectId(req.params.id); + + logger.trace(`Purchase Order with ID: ${id}`); + + const checkStatesResult = await checkStates({ model: purchaseOrderModel, id, states: ['sent'] }); + + if (checkStatesResult.error) { + logger.error('Error checking purchase order states:', checkStatesResult.error); + res.status(checkStatesResult.code).send(checkStatesResult); + return; + } + + if (checkStatesResult === false) { + logger.error('Purchase order is not in sent state.'); + res.status(400).send({ error: 'Purchase order is not in sent state.', code: 400 }); + return; + } + + const updateData = { + updatedAt: new Date(), + state: { type: 'acknowledged' }, + acknowledgedAt: new Date(), + }; + const result = await editObject({ + model: purchaseOrderModel, + id, + updateData, + user: req.user, + }); + + if (result.error) { + logger.error('Error acknowledging purchase order:', result.error); + res.status(result.code).send(result); + return; + } + + logger.debug(`Acknowledged purchase order with ID: ${id}`); + res.send(result); +}; + +export const cancelPurchaseOrderRouteHandler = async (req, res) => { + const id = new mongoose.Types.ObjectId(req.params.id); + + logger.trace(`Purchase Order with ID: ${id}`); + + const checkStatesResult = await checkStates({ + model: purchaseOrderModel, + id, + states: ['sent', 'acknowledged', 'partiallyShipped', 'shipped', 'partiallyReceived'], + }); + + if (checkStatesResult.error) { + logger.error('Error checking purchase order states:', checkStatesResult.error); + res.status(checkStatesResult.code).send(checkStatesResult); + return; + } + + if (checkStatesResult === false) { + logger.error('Purchase order is not in a cancellable state.'); + res.status(400).send({ + error: 'Purchase order is not in a cancellable state (must be draft, sent, or acknowledged).', + code: 400, + }); + return; + } + + const orderItemsResult = await listObjects({ + model: orderItemModel, + filter: { order: id, orderType: 'purchaseOrder' }, + pagination: false, + }); + + const shipmentsResult = await listObjects({ + model: shipmentModel, + filter: { order: id, orderType: 'purchaseOrder' }, + pagination: false, + }); + + const allowedOrderItemStates = ['ordered', 'shipped']; + const allowedShipmentStates = ['shipped', 'planned']; + + for (const orderItem of orderItemsResult) { + if (allowedOrderItemStates.includes(orderItem.state.type)) { + await editObject({ + model: orderItemModel, + id: orderItem._id, + updateData: { + state: { type: 'cancelled' }, + }, + user: req.user, + }); + } + } + + for (const shipment of shipmentsResult) { + if (allowedShipmentStates.includes(shipment.state.type)) { + await editObject({ + model: shipmentModel, + id: shipment._id, + updateData: { + state: { type: 'cancelled' }, + }, + user: req.user, + }); + } + } + const updateData = { + updatedAt: new Date(), + state: { type: 'cancelled' }, + cancelledAt: new Date(), + }; + const result = await editObject({ + model: purchaseOrderModel, + id, + updateData, + user: req.user, + }); + + if (result.error) { + logger.error('Error cancelling purchase order:', result.error); + res.status(result.code).send(result); + return; + } + + logger.debug(`Cancelled purchase order with ID: ${id}`); + res.send(result); +};