All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good
555 lines
14 KiB
JavaScript
555 lines
14 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,
|
|
searchObjects
|
|
} 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,
|
|
});
|
|
|
|
const stockModelByType = {
|
|
filamentStock: filamentStockModel,
|
|
partStock: partStockModel,
|
|
productStock: productStockModel,
|
|
};
|
|
|
|
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 recalculateStock(stockType, stock, user) {
|
|
const model = stockModelByType[stockType];
|
|
if (!model?.recalculate) return;
|
|
|
|
await model.recalculate(stock, user);
|
|
}
|
|
|
|
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(transferId, line, user) {
|
|
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 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,
|
|
fromId: src._id,
|
|
fromType: 'filamentStock',
|
|
toId: dest._id,
|
|
toType: 'filamentStock',
|
|
qty: line.quantity,
|
|
unit: 'g',
|
|
user,
|
|
});
|
|
|
|
await Promise.all([
|
|
recalculateStock('filamentStock', src, user),
|
|
recalculateStock('filamentStock', dest, 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');
|
|
const currentQuantity = src.currentQuantity;
|
|
if (line.quantity > currentQuantity) {
|
|
throw new Error('Part transfer quantity exceeds current quantity');
|
|
}
|
|
|
|
const dest = await createStock(
|
|
partStockModel,
|
|
{
|
|
partSku: src.partSku,
|
|
currentQuantity: line.quantity,
|
|
state: { type: 'new' },
|
|
sourceType: 'stockTransfer',
|
|
source: transferId,
|
|
stockLocation: toLocId,
|
|
},
|
|
user
|
|
);
|
|
|
|
await createStockEventsForLine({
|
|
transferId,
|
|
fromId: src._id,
|
|
fromType: 'partStock',
|
|
toId: dest._id,
|
|
toType: 'partStock',
|
|
qty: line.quantity,
|
|
unit: 'each',
|
|
user,
|
|
});
|
|
|
|
await Promise.all([
|
|
recalculateStock('partStock', src, user),
|
|
recalculateStock('partStock', dest, 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');
|
|
if (line.quantity > src.currentQuantity) {
|
|
throw new Error('Product transfer quantity exceeds current quantity');
|
|
}
|
|
|
|
const dest = await createStock(
|
|
productStockModel,
|
|
{
|
|
productSku: src.productSku,
|
|
currentQuantity: line.quantity,
|
|
state: { type: 'posted' },
|
|
postedAt: new Date(),
|
|
partStocks: [],
|
|
stockLocation: toLocId,
|
|
},
|
|
user
|
|
);
|
|
|
|
await createStockEventsForLine({
|
|
transferId,
|
|
fromId: src._id,
|
|
fromType: 'productStock',
|
|
toId: dest._id,
|
|
toType: 'productStock',
|
|
qty: line.quantity,
|
|
unit: 'each',
|
|
user,
|
|
});
|
|
|
|
await Promise.all([
|
|
recalculateStock('productStock', src, user),
|
|
recalculateStock('productStock', dest, user),
|
|
]);
|
|
|
|
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 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: [
|
|
{ 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)),
|
|
};
|
|
if (req.body.name !== undefined) {
|
|
updateData.name = req.body.name;
|
|
}
|
|
|
|
const result = await editObject({
|
|
model: stockTransferModel,
|
|
id,
|
|
updateData,
|
|
user: req.user,
|
|
populate: [
|
|
{ path: 'lines.fromStock' },
|
|
{ path: 'lines.toStockLocation' },
|
|
{ path: 'lines.toStock' },
|
|
],
|
|
});
|
|
|
|
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 = {
|
|
name: req.body.name,
|
|
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, 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: [
|
|
{ path: 'lines.fromStock' },
|
|
{ path: 'lines.toStockLocation' },
|
|
{ path: 'lines.toStock' },
|
|
],
|
|
});
|
|
|
|
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);
|
|
};
|