Add purchase order management functionality with new route handlers for posting, acknowledging, and canceling purchase orders. Enhanced service methods to validate states and update order items and shipments accordingly.

This commit is contained in:
Tom Butcher 2025-12-27 14:02:16 +00:00
parent eb2e920028
commit fadaafdad8
2 changed files with 293 additions and 1 deletions

View File

@ -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;

View File

@ -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);
};