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, listObjects, getObject, editObject, editObjects, newObject, listObjectsByProperties, getModelStats, getModelHistory, checkStates, } 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), toStockLocation: l.toStockLocation?._id ?? l.toStockLocation, }); async function createStockEventsForLine({ transferId, fromId, fromType, toId, toType, qty, unit }) { const ts = new Date(); await stockEventModel.insertMany([ { value: -Math.abs(qty), unit, parent: fromId, parentType: fromType, owner: transferId, ownerType: 'stockTransfer', timestamp: ts, }, { value: Math.abs(qty), unit, parent: toId, parentType: toType, owner: transferId, ownerType: 'stockTransfer', timestamp: ts, }, ]); } async function executePostedLine(transferId, line) { const toLocId = line.toStockLocation; const loc = await stockLocationModel.findById(toLocId).lean(); if (!loc) { throw new Error(`Unknown stock location: ${toLocId}`); } if (!(line.quantity > 0)) { throw new Error('Line quantity must be positive'); } if (line.fromStockType === 'filamentStock') { const src = await filamentStockModel.findById(line.fromStock); if (!src) throw new Error('From filament stock not found'); 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 newNet = netAvail - line.quantity; const ratio = netAvail > 0 ? newNet / netAvail : 0; const newGross = (src.currentWeight?.gross ?? 0) * ratio; await filamentStockModel.findByIdAndUpdate(src._id, { $set: { 'currentWeight.net': newNet, 'currentWeight.gross': newGross }, }); const destWeight = { net: line.quantity, gross: line.quantity + tareBefore, }; const dest = await filamentStockModel.create({ state: src.state, startingWeight: destWeight, currentWeight: destWeight, filament: src.filament, filamentSku: src.filamentSku, stockLocation: toLocId, }); await createStockEventsForLine({ transferId, fromId: src._id, fromType: 'filamentStock', toId: dest._id, toType: 'filamentStock', qty: line.quantity, unit: 'g', }); return { toStockType: 'filamentStock', toStock: dest._id }; } if (line.fromStockType === 'partStock') { const src = await partStockModel.findById(line.fromStock); console.log(src); if (!src) throw new Error('From part stock not found'); const currentQuantity = src.state.type === 'new' ? src.startingQuantity : src.currentQuantity; if (line.quantity > currentQuantity) { throw new Error('Part transfer quantity exceeds current quantity'); } await partStockModel.findByIdAndUpdate(src._id, { $inc: { currentQuantity: -line.quantity }, }); const dest = await partStockModel.create({ partSku: src.partSku, currentQuantity: line.quantity, state: { type: 'new' }, sourceType: 'stockTransfer', source: transferId, stockLocation: toLocId, }); await createStockEventsForLine({ transferId, fromId: src._id, fromType: 'partStock', toId: dest._id, toType: 'partStock', qty: line.quantity, unit: 'each', }); 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'); if (line.quantity > src.currentQuantity) { throw new Error('Product transfer quantity exceeds current quantity'); } await productStockModel.findByIdAndUpdate(src._id, { $inc: { currentQuantity: -line.quantity }, }); const dest = await productStockModel.create({ productSku: src.productSku, currentQuantity: line.quantity, state: { type: 'posted' }, postedAt: new Date(), partStocks: [], stockLocation: toLocId, }); await createStockEventsForLine({ transferId, fromId: src._id, fromType: 'productStock', toId: dest._id, toType: 'productStock', qty: line.quantity, unit: 'each', }); return { toStockType: 'productStock', toStock: dest._id }; } throw new Error(`Unsupported from stock type: ${line.fromStockType}`); } 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: [ { path: 'lines.fromStock' }, { path: 'lines.toStockLocation' }, { path: 'lines.toStock' }, ], }); 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: [ { path: 'lines.fromStock' }, { path: 'lines.toStockLocation' }, { path: 'lines.toStock' }, ], 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 getStockTransferRouteHandler = async (req, res) => { const id = req.params.id; const result = await getObject({ model: stockTransferModel, id, populate: [ { path: 'lines.fromStock' }, { path: 'lines.toStockLocation' }, { path: 'lines.toStock' }, ], }); 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)), }; const result = await editObject({ model: stockTransferModel, id, updateData, user: req.user, }); 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' }, 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 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 }); } const updatedLines = []; try { for (const line of doc.lines) { const plain = line.toObject(); const { toStockType, toStock } = await executePostedLine(doc._id, plain); updatedLines.push({ ...plain, toStockType, toStock, }); } const posted = await stockTransferModel .findByIdAndUpdate( id, { $set: { state: { type: 'posted' }, postedAt: new Date(), lines: updatedLines, }, }, { new: true } ) .populate([ { path: 'lines.fromStock' }, { path: 'lines.toStockLocation' }, { path: 'lines.toStock' }, ]) .lean(); 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); };