Added history tracking to inventory schemas (filament, part, product stocks) and stock events, enabling better state management and historical data retention. Updated related service functions to incorporate new history logic for stock events and recalculations.
Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
This commit is contained in:
parent
9a22fd6452
commit
fa74c464f9
@ -19,6 +19,15 @@ const filamentStockSchema = new Schema(
|
||||
net: { type: Number, required: true },
|
||||
gross: { type: Number, required: true },
|
||||
},
|
||||
history: [
|
||||
{
|
||||
currentWeight: {
|
||||
net: { type: Number, required: true },
|
||||
gross: { type: Number, required: true },
|
||||
},
|
||||
timestamp: { type: Date, default: Date.now },
|
||||
},
|
||||
],
|
||||
filament: { type: mongoose.Schema.Types.ObjectId, ref: 'filament', required: true },
|
||||
filamentSku: { type: mongoose.Schema.Types.ObjectId, ref: 'filamentSku', required: true },
|
||||
stockLocation: {
|
||||
|
||||
@ -18,6 +18,12 @@ const partStockSchema = new Schema(
|
||||
required: false,
|
||||
},
|
||||
currentQuantity: { type: Number, required: true },
|
||||
history: [
|
||||
{
|
||||
currentQuantity: { type: Number, required: true },
|
||||
timestamp: { type: Date, default: Date.now },
|
||||
},
|
||||
],
|
||||
sourceType: { type: String, required: true },
|
||||
source: { type: Schema.Types.ObjectId, refPath: 'sourceType', required: true },
|
||||
},
|
||||
|
||||
@ -25,6 +25,12 @@ const productStockSchema = new Schema(
|
||||
required: false,
|
||||
},
|
||||
currentQuantity: { type: Number, required: true },
|
||||
history: [
|
||||
{
|
||||
currentQuantity: { type: Number, required: true },
|
||||
timestamp: { type: Date, default: Date.now },
|
||||
},
|
||||
],
|
||||
partStocks: [partStockUsageSchema],
|
||||
},
|
||||
{ timestamps: true }
|
||||
|
||||
@ -12,6 +12,48 @@ const parentStockModelNames = {
|
||||
const initialStockStates = {
|
||||
filamentStock: 'unconsumed',
|
||||
partStock: 'new',
|
||||
productStock: 'posted',
|
||||
};
|
||||
|
||||
const getStartingAmount = (parentType, parentStock) => {
|
||||
if (parentType === 'filamentStock') {
|
||||
return parentStock.startingWeight?.net ?? 0;
|
||||
}
|
||||
|
||||
return parentStock.startingQuantity ?? 0;
|
||||
};
|
||||
|
||||
const buildParentState = (parentType, parentStock, currentAmount, startingAmount) => {
|
||||
if (parentStock.state?.type === 'draft') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const fullState = initialStockStates[parentType];
|
||||
if (!fullState) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (currentAmount <= 0) {
|
||||
return { ...parentStock.state, type: 'consumed', progress: 0 };
|
||||
}
|
||||
|
||||
if (startingAmount <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const progress = currentAmount / startingAmount;
|
||||
|
||||
console.log('progress', progress);
|
||||
|
||||
if (currentAmount === startingAmount) {
|
||||
return { ...parentStock.state, type: fullState, progress: 1 };
|
||||
}
|
||||
|
||||
if (currentAmount < startingAmount) {
|
||||
return { ...parentStock.state, type: 'used', progress };
|
||||
}
|
||||
|
||||
return { ...parentStock.state, type: fullState, progress: 1 };
|
||||
};
|
||||
|
||||
const getStockEventTotal = async (parentId, parentType) => {
|
||||
@ -20,10 +62,12 @@ const getStockEventTotal = async (parentId, parentType) => {
|
||||
const objectId =
|
||||
parentId instanceof mongoose.Types.ObjectId ? parentId : new mongoose.Types.ObjectId(parentId);
|
||||
|
||||
const [result] = await mongoose.model('stockEvent').aggregate([
|
||||
{ $match: { parent: objectId, parentType } },
|
||||
{ $group: { _id: null, total: { $sum: '$value' }, count: { $sum: 1 } } },
|
||||
]);
|
||||
const [result] = await mongoose
|
||||
.model('stockEvent')
|
||||
.aggregate([
|
||||
{ $match: { parent: objectId, parentType } },
|
||||
{ $group: { _id: null, total: { $sum: '$value' }, count: { $sum: 1 } } },
|
||||
]);
|
||||
|
||||
return {
|
||||
total: result?.total ?? 0,
|
||||
@ -33,6 +77,7 @@ const getStockEventTotal = async (parentId, parentType) => {
|
||||
|
||||
const buildParentUpdateData = (parentType, parentStock, events) => {
|
||||
const updateData = {};
|
||||
let currentAmount;
|
||||
|
||||
if (parentType === 'filamentStock') {
|
||||
const net = events.total;
|
||||
@ -40,20 +85,85 @@ const buildParentUpdateData = (parentType, parentStock, events) => {
|
||||
const startingGross = parentStock.startingWeight?.gross ?? 0;
|
||||
const gross = startingNet > 0 ? (startingGross * net) / startingNet : net;
|
||||
updateData.currentWeight = { net, gross };
|
||||
currentAmount = net;
|
||||
} else {
|
||||
updateData.currentQuantity = events.total;
|
||||
currentAmount = events.total;
|
||||
}
|
||||
|
||||
if (parentStock.state?.type !== 'draft') {
|
||||
const initialState = initialStockStates[parentType];
|
||||
if (initialState && parentStock.state?.type === initialState) {
|
||||
updateData.state = { ...parentStock.state, type: 'used' };
|
||||
}
|
||||
const state = buildParentState(
|
||||
parentType,
|
||||
parentStock,
|
||||
currentAmount,
|
||||
getStartingAmount(parentType, parentStock)
|
||||
);
|
||||
if (state) {
|
||||
updateData.state = state;
|
||||
}
|
||||
|
||||
return updateData;
|
||||
};
|
||||
|
||||
const HISTORY_RATE_LIMIT_MS = 3000;
|
||||
|
||||
const isWithinHistoryRateLimit = (lastEntry, timestamp = new Date()) => {
|
||||
if (!lastEntry?.timestamp) return false;
|
||||
const elapsed = new Date(timestamp).getTime() - new Date(lastEntry.timestamp).getTime();
|
||||
return elapsed < HISTORY_RATE_LIMIT_MS;
|
||||
};
|
||||
|
||||
const getLastParentHistoryValue = (parentType, history = []) => {
|
||||
const lastEntry = history.at(-1);
|
||||
if (!lastEntry) return undefined;
|
||||
|
||||
return parentType === 'filamentStock' ? lastEntry.currentWeight : lastEntry.currentQuantity;
|
||||
};
|
||||
|
||||
const parentValuesEqual = (parentType, a, b) => {
|
||||
if (a === b) return true;
|
||||
if (a == null || b == null) return false;
|
||||
|
||||
if (parentType === 'filamentStock') {
|
||||
return a.net === b.net && a.gross === b.gross;
|
||||
}
|
||||
|
||||
return a === b;
|
||||
};
|
||||
|
||||
const buildParentHistoryEntry = (parentType, currentValue, timestamp) => {
|
||||
if (parentType === 'filamentStock') {
|
||||
return { currentWeight: currentValue, timestamp };
|
||||
}
|
||||
|
||||
return { currentQuantity: currentValue, timestamp };
|
||||
};
|
||||
|
||||
const appendParentHistoryIfChanged = (
|
||||
parentType,
|
||||
parentStock,
|
||||
updateData,
|
||||
timestamp = new Date()
|
||||
) => {
|
||||
const history = parentStock.history || [];
|
||||
const lastEntry = history.at(-1);
|
||||
const currentValue =
|
||||
parentType === 'filamentStock' ? updateData.currentWeight : updateData.currentQuantity;
|
||||
const lastHistoryValue = getLastParentHistoryValue(parentType, history);
|
||||
|
||||
if (parentValuesEqual(parentType, currentValue, lastHistoryValue)) {
|
||||
return updateData;
|
||||
}
|
||||
|
||||
if (isWithinHistoryRateLimit(lastEntry, timestamp)) {
|
||||
return updateData;
|
||||
}
|
||||
|
||||
return {
|
||||
...updateData,
|
||||
history: [...history, buildParentHistoryEntry(parentType, currentValue, timestamp)],
|
||||
};
|
||||
};
|
||||
|
||||
const recalculateParentStock = async (parentType, parentId, user) => {
|
||||
if (!parentType || !parentId) return;
|
||||
|
||||
@ -74,7 +184,11 @@ const recalculateParentStock = async (parentType, parentId, user) => {
|
||||
await editObject({
|
||||
model: parentModel,
|
||||
id: parentStock._id,
|
||||
updateData: buildParentUpdateData(parentType, parentStock, events),
|
||||
updateData: appendParentHistoryIfChanged(
|
||||
parentType,
|
||||
parentStock,
|
||||
buildParentUpdateData(parentType, parentStock, events)
|
||||
),
|
||||
user,
|
||||
recalculate: false,
|
||||
});
|
||||
@ -105,6 +219,12 @@ const stockEventSchema = new Schema(
|
||||
required: true,
|
||||
enum: ['user', 'subJob', 'stockAudit', 'stockTransfer'],
|
||||
},
|
||||
history: [
|
||||
{
|
||||
value: { type: Number, required: true },
|
||||
timestamp: { type: Date, default: Date.now },
|
||||
},
|
||||
],
|
||||
timestamp: { type: Date, default: Date.now },
|
||||
},
|
||||
{ timestamps: true }
|
||||
@ -113,6 +233,24 @@ const stockEventSchema = new Schema(
|
||||
stockEventSchema.index({ parentType: 'text', ownerType: 'text', unit: 'text' });
|
||||
|
||||
stockEventSchema.statics.recalculate = async function (stockEvent, user) {
|
||||
const history = stockEvent.history || [];
|
||||
const lastEntry = history.at(-1);
|
||||
const lastHistoryValue = lastEntry?.value;
|
||||
const currentValue = stockEvent.value;
|
||||
const timestamp = stockEvent.timestamp || new Date();
|
||||
|
||||
if (currentValue !== lastHistoryValue && !isWithinHistoryRateLimit(lastEntry, timestamp)) {
|
||||
await editObject({
|
||||
model: this,
|
||||
id: stockEvent._id,
|
||||
updateData: {
|
||||
history: [...history, { value: currentValue, timestamp }],
|
||||
},
|
||||
user,
|
||||
recalculate: false,
|
||||
});
|
||||
}
|
||||
|
||||
const parentType = stockEvent.parentType;
|
||||
const parentId = stockEvent.parent?._id || stockEvent.parent;
|
||||
await recalculateParentStock(parentType, parentId, user);
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
searchObjects,
|
||||
getPropertyValues,
|
||||
} from '../../database/database.js';
|
||||
import { stockEventModel } from '../../database/schemas/inventory/stockevent.schema.js';
|
||||
const logger = log4js.getLogger('Filament Stocks');
|
||||
logger.level = config.server.logLevel;
|
||||
|
||||
@ -195,6 +196,28 @@ export const newFilamentStockRouteHandler = async (req, res) => {
|
||||
|
||||
logger.debug(`New filament stock with ID: ${result._id}`);
|
||||
|
||||
const netStockEventData = {
|
||||
updatedAt: new Date(),
|
||||
value: req.body.startingWeight.net,
|
||||
owner: req.user,
|
||||
ownerType: 'user',
|
||||
parent: result._id,
|
||||
parentType: 'filamentStock',
|
||||
unit: 'g',
|
||||
};
|
||||
|
||||
const stockEventResult = await newObject({
|
||||
model: stockEventModel,
|
||||
newData: netStockEventData,
|
||||
user: req.user,
|
||||
});
|
||||
if (stockEventResult.error) {
|
||||
logger.error('No stock event created:', stockEventResult.error);
|
||||
return res.status(stockEventResult.code).send(stockEventResult);
|
||||
}
|
||||
|
||||
logger.debug(`New stock event with ID: ${stockEventResult._id}`);
|
||||
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
|
||||
@ -32,12 +32,6 @@ const normalizeLineInput = (l) => ({
|
||||
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,
|
||||
@ -52,13 +46,6 @@ async function createStockEvent(newData, user) {
|
||||
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([
|
||||
@ -151,11 +138,6 @@ async function executePostedLine(transferId, line, user) {
|
||||
user,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
recalculateStock('filamentStock', src, user),
|
||||
recalculateStock('filamentStock', dest, user),
|
||||
]);
|
||||
|
||||
return { toStockType: 'filamentStock', toStock: dest._id };
|
||||
}
|
||||
|
||||
@ -191,11 +173,6 @@ async function executePostedLine(transferId, line, user) {
|
||||
user,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
recalculateStock('partStock', src, user),
|
||||
recalculateStock('partStock', dest, user),
|
||||
]);
|
||||
|
||||
return { toStockType: 'partStock', toStock: dest._id };
|
||||
}
|
||||
|
||||
@ -230,11 +207,6 @@ async function executePostedLine(transferId, line, user) {
|
||||
user,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
recalculateStock('productStock', src, user),
|
||||
recalculateStock('productStock', dest, user),
|
||||
]);
|
||||
|
||||
return { toStockType: 'productStock', toStock: dest._id };
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user