import config from '../../config.js'; import { stockTransferModel } from '../../database/schemas/inventory/stocktransfer.schema.js'; import { stockLocationModel } from '../../database/schemas/inventory/stocklocation.schema.js'; import { filamentStockModel } from '../../database/schemas/inventory/filamentstock.schema.js'; import { partStockModel } from '../../database/schemas/inventory/partstock.schema.js'; import { productStockModel } from '../../database/schemas/inventory/productstock.schema.js'; import { stockEventModel } from '../../database/schemas/inventory/stockevent.schema.js'; import log4js from 'log4js'; import mongoose from 'mongoose'; import { deleteObject, deleteObjects, listObjects, getObject, editObject, editObjects, newObject, listObjectsByProperties, getModelStats, getModelHistory, checkStates, searchObjects, getPropertyValues, getObjectNeighbors, } from '../../database/database.js'; const logger = log4js.getLogger('Stock Transfers'); logger.level = config.server.logLevel; const normalizeLineInput = (l) => ({ fromStockType: l.fromStockType, fromStock: l.fromStock?._id ?? l.fromStock, quantity: Number(l.quantity), }); 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, newData, user, }); if (result?.error) { throw new Error(result.error); } return result; } async function createStockEventsForLine({ transferId, fromId, fromType, toId, toType, qty, unit, user }) { const ts = new Date(); await Promise.all([ createStockEvent( { value: -Math.abs(qty), unit, parent: fromId, parentType: fromType, owner: transferId, ownerType: 'stockTransfer', timestamp: ts, }, user ), createStockEvent( { value: Math.abs(qty), unit, parent: toId, parentType: toType, owner: transferId, ownerType: 'stockTransfer', timestamp: ts, }, user ), ]); } async function createStock(model, newData, user) { const result = await newObject({ model, newData, user, }); if (result?.error) { throw new Error(result.error); } return result; } 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'); } const tareBefore = Math.max(0, (src.currentWeight?.gross ?? 0) - (src.currentWeight?.net ?? 0)); const destWeight = { net: line.quantity, gross: line.quantity + tareBefore, }; const dest = await createStock( filamentStockModel, { state: src.state, startingWeight: destWeight, currentWeight: destWeight, filament: src.filament, filamentSku: src.filamentSku, stockLocation: toLocId, }, user ); await createStockEventsForLine({ transferId: transfer._id, fromId: src._id, fromType: 'filamentStock', toId: dest._id, toType: 'filamentStock', qty: line.quantity, unit: 'g', user, }); return { toStockType: 'filamentStock', toStock: dest._id }; } 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'); } const dest = await createStock( partStockModel, { part: src.part, partSku: src.partSku, currentQuantity: line.quantity, state: { type: 'new' }, postedAt: new Date(), stockLocation: toLocId, }, user ); await createStockEventsForLine({ transferId: transfer._id, fromId: src._id, fromType: 'partStock', toId: dest._id, toType: 'partStock', qty: line.quantity, unit: 'each', user, }); return { toStockType: 'partStock', toStock: dest._id }; } 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'); } const dest = await createStock( productStockModel, { product: src.product, productSku: src.productSku, currentQuantity: line.quantity, state: { type: 'new' }, postedAt: new Date(), partStockList: [], stockLocation: toLocId, }, user ); await createStockEventsForLine({ transferId: transfer._id, fromId: src._id, fromType: 'productStock', toId: dest._id, toType: 'productStock', qty: line.quantity, unit: 'each', user, }); return { toStockType: 'productStock', toStock: dest._id }; } 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, page = 1, limit = 25, property = '', filter = {}, search = '', sort = '', order = 'ascend' ) => { const result = await listObjects({ model: stockTransferModel, page, limit, property, filter, search, sort, order, populate: stockTransferPopulate, }); if (result?.error) { logger.error('Error listing stock transfers.'); res.status(result.code).send(result); return; } logger.debug(`List of stock transfers (Page ${page}, Limit ${limit}). Count: ${result.length}`); res.send(result); }; export const listStockTransfersByPropertiesRouteHandler = async ( req, res, properties = '', filter = {}, masterFilter = {} ) => { const result = await listObjectsByProperties({ model: stockTransferModel, properties, filter, populate: stockTransferPopulate, masterFilter, }); if (result?.error) { logger.error('Error listing stock transfers.'); res.status(result.code).send(result); return; } logger.debug(`List of stock transfers. Count: ${result.length}`); res.send(result); }; export const getStockTransferPropertyValuesRouteHandler = async ( req, res, property, filter, masterFilter ) => { const result = await getPropertyValues({ model: stockTransferModel, property, filter: { ...filter, ...masterFilter }, }); res.send(result); }; export const searchStockTransfersRouteHandler = async (req, res, search) => { const result = await searchObjects({ model: stockTransferModel, search, }); res.send(result); }; export const getStockTransferRouteHandler = async (req, res) => { const id = req.params.id; const result = await getObject({ model: stockTransferModel, id, populate: stockTransferPopulate, }); if (result?.error) { logger.warn(`Stock transfer not found with supplied id.`); return res.status(result.code).send(result); } logger.debug(`Retrieved stock transfer with ID: ${id}`); res.send(result); }; export const editStockTransferRouteHandler = async (req, res) => { const id = new mongoose.Types.ObjectId(req.params.id); const checkStatesResult = await checkStates({ model: stockTransferModel, id, states: ['draft'] }); if (checkStatesResult.error) { logger.error('Error checking stock transfer state:', checkStatesResult.error); res.status(checkStatesResult.code).send(checkStatesResult); return; } if (checkStatesResult === false) { logger.error('Stock transfer is not in draft state.'); res.status(400).send({ error: 'Stock transfer is not in draft state.', code: 400 }); return; } const updateData = { lines: (req.body.lines || []).map((l) => normalizeLineInput(l)), }; 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({ model: stockTransferModel, id, updateData, user: req.user, populate: stockTransferPopulate, }); if (result.error) { logger.error('Error editing stock transfer:', result.error); res.status(result.code).send(result); return; } logger.debug(`Edited stock transfer with ID: ${id}`); res.send(result); }; export const editMultipleStockTransfersRouteHandler = async (req, res) => { const updates = req.body.map((update) => ({ _id: update._id, })); if (!Array.isArray(updates)) { return res.status(400).send({ error: 'Body must be an array of updates.', code: 400 }); } const result = await editObjects({ model: stockTransferModel, updates, user: req.user, }); if (result.error) { logger.error('Error editing stock transfers:', result.error); res.status(result.code || 500).send(result); return; } logger.debug(`Edited ${updates.length} stock transfers`); res.send(result); }; export const newStockTransferRouteHandler = async (req, res) => { const newData = { 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({ model: stockTransferModel, newData, user: req.user, }); if (result.error) { logger.error('No stock transfer created:', result.error); return res.status(result.code).send(result); } logger.debug(`New stock transfer with ID: ${result._id}`); res.send(result); }; export const deleteStockTransferByFilterRouteHandler = async ( req, res, filter = {}, masterFilter = {} ) => { const result = await deleteObjects({ model: stockTransferModel, filter: { ...masterFilter, ...filter }, user: req.user, states: ['draft'], }); if (result.error) { logger.error('Failed to delete filtered StockTransfer:', result.error); return res.status(result.code || 500).send(result); } res.send(result); }; export const deleteStockTransferRouteHandler = async (req, res) => { const id = new mongoose.Types.ObjectId(req.params.id); const checkStatesResult = await checkStates({ model: stockTransferModel, id, states: ['draft'] }); if (checkStatesResult.error) { logger.error('Error checking stock transfer state:', checkStatesResult.error); res.status(checkStatesResult.code).send(checkStatesResult); return; } if (checkStatesResult === false) { logger.error('Stock transfer is not in draft state.'); res.status(400).send({ error: 'Stock transfer is not in draft state.', code: 400 }); return; } const result = await deleteObject({ model: stockTransferModel, id, user: req.user, }); if (result.error) { logger.error('No stock transfer deleted:', result.error); return res.status(result.code).send(result); } logger.debug(`Deleted stock transfer with ID: ${result._id}`); res.send(result); }; export const postStockTransferRouteHandler = async (req, res) => { const id = new mongoose.Types.ObjectId(req.params.id); const checkStatesResult = await checkStates({ model: stockTransferModel, id, states: ['draft'] }); if (checkStatesResult.error) { logger.error('Error checking stock transfer state:', checkStatesResult.error); res.status(checkStatesResult.code).send(checkStatesResult); return; } if (checkStatesResult === false) { logger.error('Stock transfer is not in draft state.'); res.status(400).send({ error: 'Stock transfer is not in draft state.', code: 400 }); return; } const doc = await stockTransferModel.findById(id); if (!doc) { return res.status(404).send({ error: 'Stock transfer not found.', code: 404 }); } if (!doc.lines?.length) { 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, plain, req.user); updatedLines.push({ ...plain, toStockType, toStock, }); } const postedResult = await editObject({ model: stockTransferModel, id, updateData: { state: { type: 'posted' }, postedAt: new Date(), lines: updatedLines, }, user: req.user, }); if (postedResult?.error) { throw new Error(postedResult.error); } const posted = await getObject({ model: stockTransferModel, id, populate: stockTransferPopulate, }); if (posted?.error) { throw new Error(posted.error); } logger.debug(`Posted stock transfer with ID: ${id}`); res.send(posted); } catch (err) { logger.error('Error posting stock transfer:', err); res.status(400).send({ error: err.message || 'Failed to post stock transfer', code: 400 }); } }; export const getStockTransferStatsRouteHandler = async (req, res) => { const result = await getModelStats({ model: stockTransferModel }); if (result?.error) { logger.error('Error fetching stock transfer stats:', result.error); return res.status(result.code).send(result); } logger.trace('Stock transfer stats:', result); res.send(result); }; export const getStockTransferHistoryRouteHandler = async (req, res) => { const from = req.query.from; const to = req.query.to; const result = await getModelHistory({ model: stockTransferModel, from, to }); if (result?.error) { logger.error('Error fetching stock transfer history:', result.error); return res.status(result.code).send(result); } logger.trace('Stock transfer history:', result); res.send(result); }; export const getStockTransferNeighborsRouteHandler = async ( req, res, property = '', filter = {}, search = '', sort = '', order = 'ascend', id ) => { if (!id) { return res.status(400).send({ error: 'Missing id parameter', code: 400 }); } const result = await getObjectNeighbors({ model: stockEventModel, id, filter, search, sort, order, }); if (result?.error) { logger.error('Error fetching stockTransfer neighbors.'); return res.status(result.code).send(result); } logger.debug(`Retrieved stockTransfer neighbors for ID: ${id}`); res.send(result); };