Add history tracking to inventory schemas and stock event
All checks were successful
farmcontrol/farmcontrol-ws/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-ws/pipeline/head This commit looks good
- Introduced a `history` field in filamentStock, partStock, productStock, and stockEvent schemas to maintain historical records of current quantities and weights. - Enhanced stock event processing to append history entries when values change, ensuring accurate tracking of stock changes over time. - Implemented utility functions to manage history entries and enforce rate limits on updates for improved data integrity.
This commit is contained in:
parent
67f929e369
commit
2b660ec61d
@ -19,6 +19,15 @@ const filamentStockSchema = new Schema(
|
|||||||
net: { type: Number, required: true },
|
net: { type: Number, required: true },
|
||||||
gross: { 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 },
|
filament: { type: mongoose.Schema.Types.ObjectId, ref: 'filament', required: true },
|
||||||
filamentSku: { type: mongoose.Schema.Types.ObjectId, ref: 'filamentSku', required: true },
|
filamentSku: { type: mongoose.Schema.Types.ObjectId, ref: 'filamentSku', required: true },
|
||||||
stockLocation: {
|
stockLocation: {
|
||||||
|
|||||||
@ -18,6 +18,12 @@ const partStockSchema = new Schema(
|
|||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
currentQuantity: { type: Number, required: true },
|
currentQuantity: { type: Number, required: true },
|
||||||
|
history: [
|
||||||
|
{
|
||||||
|
currentQuantity: { type: Number, required: true },
|
||||||
|
timestamp: { type: Date, default: Date.now },
|
||||||
|
},
|
||||||
|
],
|
||||||
sourceType: { type: String, required: true },
|
sourceType: { type: String, required: true },
|
||||||
source: { type: Schema.Types.ObjectId, refPath: 'sourceType', required: true },
|
source: { type: Schema.Types.ObjectId, refPath: 'sourceType', required: true },
|
||||||
},
|
},
|
||||||
|
|||||||
@ -25,6 +25,12 @@ const productStockSchema = new Schema(
|
|||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
currentQuantity: { type: Number, required: true },
|
currentQuantity: { type: Number, required: true },
|
||||||
|
history: [
|
||||||
|
{
|
||||||
|
currentQuantity: { type: Number, required: true },
|
||||||
|
timestamp: { type: Date, default: Date.now },
|
||||||
|
},
|
||||||
|
],
|
||||||
partStocks: [partStockUsageSchema],
|
partStocks: [partStockUsageSchema],
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
|
|||||||
@ -12,6 +12,48 @@ const parentStockModelNames = {
|
|||||||
const initialStockStates = {
|
const initialStockStates = {
|
||||||
filamentStock: 'unconsumed',
|
filamentStock: 'unconsumed',
|
||||||
partStock: 'new',
|
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) => {
|
const getStockEventTotal = async (parentId, parentType) => {
|
||||||
@ -20,10 +62,12 @@ const getStockEventTotal = async (parentId, parentType) => {
|
|||||||
const objectId =
|
const objectId =
|
||||||
parentId instanceof mongoose.Types.ObjectId ? parentId : new mongoose.Types.ObjectId(parentId);
|
parentId instanceof mongoose.Types.ObjectId ? parentId : new mongoose.Types.ObjectId(parentId);
|
||||||
|
|
||||||
const [result] = await mongoose.model('stockEvent').aggregate([
|
const [result] = await mongoose
|
||||||
{ $match: { parent: objectId, parentType } },
|
.model('stockEvent')
|
||||||
{ $group: { _id: null, total: { $sum: '$value' }, count: { $sum: 1 } } },
|
.aggregate([
|
||||||
]);
|
{ $match: { parent: objectId, parentType } },
|
||||||
|
{ $group: { _id: null, total: { $sum: '$value' }, count: { $sum: 1 } } },
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total: result?.total ?? 0,
|
total: result?.total ?? 0,
|
||||||
@ -33,6 +77,7 @@ const getStockEventTotal = async (parentId, parentType) => {
|
|||||||
|
|
||||||
const buildParentUpdateData = (parentType, parentStock, events) => {
|
const buildParentUpdateData = (parentType, parentStock, events) => {
|
||||||
const updateData = {};
|
const updateData = {};
|
||||||
|
let currentAmount;
|
||||||
|
|
||||||
if (parentType === 'filamentStock') {
|
if (parentType === 'filamentStock') {
|
||||||
const net = events.total;
|
const net = events.total;
|
||||||
@ -40,20 +85,85 @@ const buildParentUpdateData = (parentType, parentStock, events) => {
|
|||||||
const startingGross = parentStock.startingWeight?.gross ?? 0;
|
const startingGross = parentStock.startingWeight?.gross ?? 0;
|
||||||
const gross = startingNet > 0 ? (startingGross * net) / startingNet : net;
|
const gross = startingNet > 0 ? (startingGross * net) / startingNet : net;
|
||||||
updateData.currentWeight = { net, gross };
|
updateData.currentWeight = { net, gross };
|
||||||
|
currentAmount = net;
|
||||||
} else {
|
} else {
|
||||||
updateData.currentQuantity = events.total;
|
updateData.currentQuantity = events.total;
|
||||||
|
currentAmount = events.total;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parentStock.state?.type !== 'draft') {
|
const state = buildParentState(
|
||||||
const initialState = initialStockStates[parentType];
|
parentType,
|
||||||
if (initialState && parentStock.state?.type === initialState) {
|
parentStock,
|
||||||
updateData.state = { ...parentStock.state, type: 'used' };
|
currentAmount,
|
||||||
}
|
getStartingAmount(parentType, parentStock)
|
||||||
|
);
|
||||||
|
if (state) {
|
||||||
|
updateData.state = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
return updateData;
|
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) => {
|
const recalculateParentStock = async (parentType, parentId, user) => {
|
||||||
if (!parentType || !parentId) return;
|
if (!parentType || !parentId) return;
|
||||||
|
|
||||||
@ -74,7 +184,11 @@ const recalculateParentStock = async (parentType, parentId, user) => {
|
|||||||
await editObject({
|
await editObject({
|
||||||
model: parentModel,
|
model: parentModel,
|
||||||
id: parentStock._id,
|
id: parentStock._id,
|
||||||
updateData: buildParentUpdateData(parentType, parentStock, events),
|
updateData: appendParentHistoryIfChanged(
|
||||||
|
parentType,
|
||||||
|
parentStock,
|
||||||
|
buildParentUpdateData(parentType, parentStock, events)
|
||||||
|
),
|
||||||
user,
|
user,
|
||||||
recalculate: false,
|
recalculate: false,
|
||||||
});
|
});
|
||||||
@ -105,6 +219,12 @@ const stockEventSchema = new Schema(
|
|||||||
required: true,
|
required: true,
|
||||||
enum: ['user', 'subJob', 'stockAudit', 'stockTransfer'],
|
enum: ['user', 'subJob', 'stockAudit', 'stockTransfer'],
|
||||||
},
|
},
|
||||||
|
history: [
|
||||||
|
{
|
||||||
|
value: { type: Number, required: true },
|
||||||
|
timestamp: { type: Date, default: Date.now },
|
||||||
|
},
|
||||||
|
],
|
||||||
timestamp: { type: Date, default: Date.now },
|
timestamp: { type: Date, default: Date.now },
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
@ -113,6 +233,24 @@ const stockEventSchema = new Schema(
|
|||||||
stockEventSchema.index({ parentType: 'text', ownerType: 'text', unit: 'text' });
|
stockEventSchema.index({ parentType: 'text', ownerType: 'text', unit: 'text' });
|
||||||
|
|
||||||
stockEventSchema.statics.recalculate = async function (stockEvent, user) {
|
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 parentType = stockEvent.parentType;
|
||||||
const parentId = stockEvent.parent?._id || stockEvent.parent;
|
const parentId = stockEvent.parent?._id || stockEvent.parent;
|
||||||
await recalculateParentStock(parentType, parentId, user);
|
await recalculateParentStock(parentType, parentId, user);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user