From 3bd1549c85ec43b8dbbc3710ecb388eac8e1d757 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Tue, 1 Sep 2026 16:04:29 +0100 Subject: [PATCH] Refactor stock transfer schema and related services to improve location handling This commit modifies the `stockTransfer` schema by removing the `toStockLocation` field and introducing `fromLocation` and `toLocation` fields, ensuring better clarity in stock transfer operations. The routes and services are updated to reflect these changes, including adjustments in the `executePostedLine` function to validate stock locations. Additionally, the allowed filters and sorters in the stock transfer routes are refined, enhancing the overall functionality and maintainability of the inventory management system. --- .../schemas/inventory/stocktransfer.schema.js | 18 ++-- src/routes/inventory/stocktransfers.js | 3 +- src/services/inventory/stocktransfers.js | 97 +++++++++++-------- src/services/misc/export.js | 2 +- 4 files changed, 72 insertions(+), 48 deletions(-) diff --git a/src/database/schemas/inventory/stocktransfer.schema.js b/src/database/schemas/inventory/stocktransfer.schema.js index 2ea7bb0..3a1eb84 100644 --- a/src/database/schemas/inventory/stocktransfer.schema.js +++ b/src/database/schemas/inventory/stocktransfer.schema.js @@ -15,11 +15,6 @@ const stockTransferLineSchema = new Schema( required: true, }, quantity: { type: Number, required: true }, - toStockLocation: { - type: Schema.Types.ObjectId, - ref: 'stockLocation', - required: true, - }, toStockType: { type: String, required: false, @@ -37,18 +32,27 @@ const stockTransferLineSchema = new Schema( const stockTransferSchema = new Schema( { _reference: { type: String, default: () => generateId()() }, - name: { type: String, required: true }, state: { type: { type: String, required: true, default: 'draft' }, progress: { type: Number, required: false }, }, postedAt: { type: Date, required: false }, + fromLocation: { + type: Schema.Types.ObjectId, + ref: 'stockLocation', + required: true, + }, + toLocation: { + type: Schema.Types.ObjectId, + ref: 'stockLocation', + required: true, + }, lines: { type: [stockTransferLineSchema], default: [] }, }, { timestamps: true } ); -stockTransferSchema.index({ name: 'text' }); +stockTransferSchema.index({ 'state.type': 'text' }); stockTransferSchema.statics.stats = async function () { const [draft, posted] = await Promise.all([ diff --git a/src/routes/inventory/stocktransfers.js b/src/routes/inventory/stocktransfers.js index 4da7647..0857245 100644 --- a/src/routes/inventory/stocktransfers.js +++ b/src/routes/inventory/stocktransfers.js @@ -9,12 +9,11 @@ const listAllowedFilters = [ 'state', 'state.type', 'postedAt', - 'name', 'createdAt', 'updatedAt', '_reference', ]; -const listAllowedSorters = ['name', 'createdAt', 'postedAt', 'state', 'updatedAt']; +const listAllowedSorters = ['createdAt', 'postedAt', 'state', 'updatedAt']; const propertiesAllowedFilters = ['state.type']; import { listStockTransfersRouteHandler, diff --git a/src/services/inventory/stocktransfers.js b/src/services/inventory/stocktransfers.js index 97fdf7e..aec2962 100644 --- a/src/services/inventory/stocktransfers.js +++ b/src/services/inventory/stocktransfers.js @@ -30,9 +30,14 @@ const normalizeLineInput = (l) => ({ fromStockType: l.fromStockType, fromStock: l.fromStock?._id ?? l.fromStock, quantity: Number(l.quantity), - toStockLocation: l.toStockLocation?._id ?? l.toStockLocation, }); +const toId = (value) => { + if (value == null) return null; + if (typeof value === 'object' && value._id) return String(value._id); + return String(value); +}; + async function createStockEvent(newData, user) { const result = await newObject({ model: stockEventModel, @@ -91,20 +96,36 @@ async function createStock(model, newData, user) { return result; } -async function executePostedLine(transferId, line, user) { - const toLocId = line.toStockLocation; - const loc = await stockLocationModel.findById(toLocId).lean(); - if (!loc) { - throw new Error(`Unknown stock location: ${toLocId}`); +async function executePostedLine(transfer, line, user) { + const fromLocId = transfer.fromLocation; + const toLocId = transfer.toLocation; + + const [fromLoc, toLoc] = await Promise.all([ + stockLocationModel.findById(fromLocId).lean(), + stockLocationModel.findById(toLocId).lean(), + ]); + + if (!fromLoc) { + throw new Error(`Unknown from location: ${fromLocId}`); + } + if (!toLoc) { + throw new Error(`Unknown to location: ${toLocId}`); } if (!(line.quantity > 0)) { throw new Error('Line quantity must be positive'); } + const assertStockAtFromLocation = (stock) => { + if (toId(stock?.stockLocation) !== toId(fromLocId)) { + throw new Error('From stock must be at the transfer from location'); + } + }; + if (line.fromStockType === 'filamentStock') { const src = await filamentStockModel.findById(line.fromStock); if (!src) throw new Error('From filament stock not found'); + assertStockAtFromLocation(src); const netAvail = src.currentWeight?.net ?? 0; if (line.quantity > netAvail) { throw new Error('Filament transfer quantity exceeds available net weight'); @@ -129,7 +150,7 @@ async function executePostedLine(transferId, line, user) { ); await createStockEventsForLine({ - transferId, + transferId: transfer._id, fromId: src._id, fromType: 'filamentStock', toId: dest._id, @@ -145,6 +166,7 @@ async function executePostedLine(transferId, line, user) { if (line.fromStockType === 'partStock') { const src = await partStockModel.findById(line.fromStock); if (!src) throw new Error('From part stock not found'); + assertStockAtFromLocation(src); const currentQuantity = src.currentQuantity; if (line.quantity > currentQuantity) { throw new Error('Part transfer quantity exceeds current quantity'); @@ -164,7 +186,7 @@ async function executePostedLine(transferId, line, user) { ); await createStockEventsForLine({ - transferId, + transferId: transfer._id, fromId: src._id, fromType: 'partStock', toId: dest._id, @@ -180,6 +202,7 @@ async function executePostedLine(transferId, line, user) { if (line.fromStockType === 'productStock') { const src = await productStockModel.findById(line.fromStock); if (!src) throw new Error('From product stock not found'); + assertStockAtFromLocation(src); if (line.quantity > src.currentQuantity) { throw new Error('Product transfer quantity exceeds current quantity'); } @@ -199,7 +222,7 @@ async function executePostedLine(transferId, line, user) { ); await createStockEventsForLine({ - transferId, + transferId: transfer._id, fromId: src._id, fromType: 'productStock', toId: dest._id, @@ -215,6 +238,13 @@ async function executePostedLine(transferId, line, user) { throw new Error(`Unsupported from stock type: ${line.fromStockType}`); } +const stockTransferPopulate = [ + { path: 'fromLocation' }, + { path: 'toLocation' }, + { path: 'lines.fromStock' }, + { path: 'lines.toStock' }, +]; + export const listStockTransfersRouteHandler = async ( req, res, @@ -235,11 +265,7 @@ export const listStockTransfersRouteHandler = async ( search, sort, order, - populate: [ - { path: 'lines.fromStock' }, - { path: 'lines.toStockLocation' }, - { path: 'lines.toStock' }, - ], + populate: stockTransferPopulate, }); if (result?.error) { @@ -263,11 +289,7 @@ export const listStockTransfersByPropertiesRouteHandler = async ( model: stockTransferModel, properties, filter, - populate: [ - { path: 'lines.fromStock' }, - { path: 'lines.toStockLocation' }, - { path: 'lines.toStock' }, - ], + populate: stockTransferPopulate, masterFilter, }); @@ -309,11 +331,7 @@ export const getStockTransferRouteHandler = async (req, res) => { const result = await getObject({ model: stockTransferModel, id, - populate: [ - { path: 'lines.fromStock' }, - { path: 'lines.toStockLocation' }, - { path: 'lines.toStock' }, - ], + populate: stockTransferPopulate, }); if (result?.error) { logger.warn(`Stock transfer not found with supplied id.`); @@ -343,8 +361,11 @@ export const editStockTransferRouteHandler = async (req, res) => { const updateData = { lines: (req.body.lines || []).map((l) => normalizeLineInput(l)), }; - if (req.body.name !== undefined) { - updateData.name = req.body.name; + if (req.body.fromLocation !== undefined) { + updateData.fromLocation = req.body.fromLocation?._id ?? req.body.fromLocation; + } + if (req.body.toLocation !== undefined) { + updateData.toLocation = req.body.toLocation?._id ?? req.body.toLocation; } const result = await editObject({ @@ -352,11 +373,7 @@ export const editStockTransferRouteHandler = async (req, res) => { id, updateData, user: req.user, - populate: [ - { path: 'lines.fromStock' }, - { path: 'lines.toStockLocation' }, - { path: 'lines.toStock' }, - ], + populate: stockTransferPopulate, }); if (result.error) { @@ -396,8 +413,9 @@ export const editMultipleStockTransfersRouteHandler = async (req, res) => { export const newStockTransferRouteHandler = async (req, res) => { const newData = { - name: req.body.name, state: req.body.state ?? { type: 'draft' }, + fromLocation: req.body.fromLocation?._id ?? req.body.fromLocation, + toLocation: req.body.toLocation?._id ?? req.body.toLocation, lines: (req.body.lines || []).map((l) => normalizeLineInput(l)), }; const result = await newObject({ @@ -471,12 +489,19 @@ export const postStockTransferRouteHandler = async (req, res) => { return res.status(400).send({ error: 'Stock transfer has no lines.', code: 400 }); } + if (!doc.fromLocation || !doc.toLocation) { + return res.status(400).send({ + error: 'Stock transfer must have from and to locations.', + code: 400, + }); + } + const updatedLines = []; try { for (const line of doc.lines) { const plain = line.toObject(); - const { toStockType, toStock } = await executePostedLine(doc._id, plain, req.user); + const { toStockType, toStock } = await executePostedLine(doc, plain, req.user); updatedLines.push({ ...plain, toStockType, @@ -502,11 +527,7 @@ export const postStockTransferRouteHandler = async (req, res) => { const posted = await getObject({ model: stockTransferModel, id, - populate: [ - { path: 'lines.fromStock' }, - { path: 'lines.toStockLocation' }, - { path: 'lines.toStock' }, - ], + populate: stockTransferPopulate, }); if (posted?.error) { diff --git a/src/services/misc/export.js b/src/services/misc/export.js index 60cb2d5..77184d2 100644 --- a/src/services/misc/export.js +++ b/src/services/misc/export.js @@ -35,7 +35,7 @@ export const EXPORT_FILTER_BY_TYPE = { shipment: ['order._id', 'orderType', 'courierService._id'], stockEvent: ['parent._id', 'parentType', 'owner._id', 'ownerType'], stockLocation: ['name', 'address'], - stockTransfer: ['name', 'state.type', 'postedAt'], + stockTransfer: ['state.type', 'postedAt'], stockAudit: ['auditLevel._id', 'stockLocation._id', 'state.type', 'postedAt'], stockAuditLevel: ['name', 'tags'], documentJob: ['documentTemplate', 'documentPrinter', 'object._id', 'objectType'],