Compare commits

...

2 Commits

Author SHA1 Message Date
3bd1549c85 Refactor stock transfer schema and related services to improve location handling
Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
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.
2026-09-01 16:04:29 +01:00
6623bf9bfd Update stock audit level schema to require tags and improve code readability
This commit modifies the `stockAuditLevel` schema by making the `tags` field required, ensuring that all stock audit levels have associated tags for better categorization. Additionally, it enhances the readability of the `normalizeAuditLevelLine` function by adjusting the formatting of the item and itemSku assignments, improving overall code clarity.
2026-09-01 15:51:25 +01:00
5 changed files with 75 additions and 51 deletions

View File

@ -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([

View File

@ -13,8 +13,8 @@ const toId = (value) => {
export function normalizeAuditLevelLine(line) {
const allItems = Boolean(line?.allItems);
const allSkus = allItems ? true : Boolean(line?.allSkus);
const item = allItems ? null : line?.item?._id ?? line?.item ?? null;
const itemSku = allItems || allSkus ? null : line?.itemSku?._id ?? line?.itemSku ?? null;
const item = allItems ? null : (line?.item?._id ?? line?.item ?? null);
const itemSku = allItems || allSkus ? null : (line?.itemSku?._id ?? line?.itemSku ?? null);
return {
_id: line?._id,
@ -62,7 +62,7 @@ const stockAuditLevelSchema = new Schema(
{
_reference: { type: String, default: () => generateId()() },
name: { type: String, required: true },
tags: [{ type: String }],
tags: [{ type: String, required: true }],
auditLines: { type: [stockAuditLevelLineSchema], default: [] },
},
{ timestamps: true }

View File

@ -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,

View File

@ -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) {

View File

@ -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'],