463 lines
13 KiB
JavaScript
463 lines
13 KiB
JavaScript
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);
|
|
};
|