Compare commits
2 Commits
8423db2858
...
fa74c464f9
| Author | SHA1 | Date | |
|---|---|---|---|
| fa74c464f9 | |||
| 9a22fd6452 |
@ -2,7 +2,7 @@
|
||||
"development": {
|
||||
"server": {
|
||||
"port": 8787,
|
||||
"logLevel": "debug"
|
||||
"logLevel": "trace"
|
||||
},
|
||||
"auth": {
|
||||
"enabled": true,
|
||||
|
||||
@ -1,26 +1,7 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { generateId } from '../../utils.js';
|
||||
const { Schema } = mongoose;
|
||||
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
||||
import { stockEventModel } from './stockevent.schema.js';
|
||||
|
||||
const getStockEventTotal = async (stock, parentType) => {
|
||||
const stockId = stock?._id;
|
||||
if (!stockId) return null;
|
||||
|
||||
const parentId =
|
||||
stockId instanceof mongoose.Types.ObjectId ? stockId : new mongoose.Types.ObjectId(stockId);
|
||||
|
||||
const [result] = await stockEventModel.aggregate([
|
||||
{ $match: { parent: parentId, parentType } },
|
||||
{ $group: { _id: null, total: { $sum: '$value' }, count: { $sum: 1 } } },
|
||||
]);
|
||||
|
||||
return {
|
||||
total: result?.total ?? 0,
|
||||
count: result?.count ?? 0,
|
||||
};
|
||||
};
|
||||
import { aggregateRollups, aggregateRollupsHistory } from '../../database.js';
|
||||
|
||||
// Define the main filamentStock schema
|
||||
const filamentStockSchema = new Schema(
|
||||
@ -38,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: {
|
||||
@ -91,29 +81,6 @@ filamentStockSchema.statics.history = async function (from, to) {
|
||||
return results;
|
||||
};
|
||||
|
||||
filamentStockSchema.statics.recalculate = async function (filamentStock, user) {
|
||||
const events = await getStockEventTotal(filamentStock, this.modelName);
|
||||
if (!events?.count) return;
|
||||
|
||||
const net = events.total;
|
||||
const startingNet = filamentStock.startingWeight?.net ?? 0;
|
||||
const startingGross = filamentStock.startingWeight?.gross ?? 0;
|
||||
const gross = startingNet > 0 ? (startingGross * net) / startingNet : net;
|
||||
|
||||
await editObject({
|
||||
model: this,
|
||||
id: filamentStock._id,
|
||||
updateData: {
|
||||
currentWeight: {
|
||||
net,
|
||||
gross,
|
||||
},
|
||||
},
|
||||
user,
|
||||
recalculate: false,
|
||||
});
|
||||
};
|
||||
|
||||
// Add virtual id getter
|
||||
filamentStockSchema.virtual('id').get(function () {
|
||||
return this._id;
|
||||
|
||||
@ -1,26 +1,7 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { generateId } from '../../utils.js';
|
||||
const { Schema } = mongoose;
|
||||
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
||||
import { stockEventModel } from './stockevent.schema.js';
|
||||
|
||||
const getStockEventTotal = async (stock, parentType) => {
|
||||
const stockId = stock?._id;
|
||||
if (!stockId) return null;
|
||||
|
||||
const parentId =
|
||||
stockId instanceof mongoose.Types.ObjectId ? stockId : new mongoose.Types.ObjectId(stockId);
|
||||
|
||||
const [result] = await stockEventModel.aggregate([
|
||||
{ $match: { parent: parentId, parentType } },
|
||||
{ $group: { _id: null, total: { $sum: '$value' }, count: { $sum: 1 } } },
|
||||
]);
|
||||
|
||||
return {
|
||||
total: result?.total ?? 0,
|
||||
count: result?.count ?? 0,
|
||||
};
|
||||
};
|
||||
import { aggregateRollups, aggregateRollupsHistory } from '../../database.js';
|
||||
|
||||
// Define the main partStock schema
|
||||
const partStockSchema = new Schema(
|
||||
@ -37,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 },
|
||||
},
|
||||
@ -74,21 +61,6 @@ partStockSchema.statics.history = async function (from, to) {
|
||||
return results;
|
||||
};
|
||||
|
||||
partStockSchema.statics.recalculate = async function (partStock, user) {
|
||||
const events = await getStockEventTotal(partStock, this.modelName);
|
||||
if (!events?.count) return;
|
||||
|
||||
await editObject({
|
||||
model: this,
|
||||
id: partStock._id,
|
||||
updateData: {
|
||||
currentQuantity: events.total,
|
||||
},
|
||||
user,
|
||||
recalculate: false,
|
||||
});
|
||||
};
|
||||
|
||||
// Add virtual id getter
|
||||
partStockSchema.virtual('id').get(function () {
|
||||
return this._id;
|
||||
|
||||
@ -1,26 +1,7 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { generateId } from '../../utils.js';
|
||||
const { Schema } = mongoose;
|
||||
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
||||
import { stockEventModel } from './stockevent.schema.js';
|
||||
|
||||
const getStockEventTotal = async (stock, parentType) => {
|
||||
const stockId = stock?._id;
|
||||
if (!stockId) return null;
|
||||
|
||||
const parentId =
|
||||
stockId instanceof mongoose.Types.ObjectId ? stockId : new mongoose.Types.ObjectId(stockId);
|
||||
|
||||
const [result] = await stockEventModel.aggregate([
|
||||
{ $match: { parent: parentId, parentType } },
|
||||
{ $group: { _id: null, total: { $sum: '$value' }, count: { $sum: 1 } } },
|
||||
]);
|
||||
|
||||
return {
|
||||
total: result?.total ?? 0,
|
||||
count: result?.count ?? 0,
|
||||
};
|
||||
};
|
||||
import { aggregateRollups, aggregateRollupsHistory } from '../../database.js';
|
||||
|
||||
const partStockUsageSchema = new Schema({
|
||||
partStock: { type: Schema.Types.ObjectId, ref: 'partStock', required: false },
|
||||
@ -44,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 }
|
||||
@ -89,21 +76,6 @@ productStockSchema.statics.history = async function (from, to) {
|
||||
return results;
|
||||
};
|
||||
|
||||
productStockSchema.statics.recalculate = async function (productStock, user) {
|
||||
const events = await getStockEventTotal(productStock, this.modelName);
|
||||
if (!events?.count) return;
|
||||
|
||||
await editObject({
|
||||
model: this,
|
||||
id: productStock._id,
|
||||
updateData: {
|
||||
currentQuantity: events.total,
|
||||
},
|
||||
user,
|
||||
recalculate: false,
|
||||
});
|
||||
};
|
||||
|
||||
// Add virtual id getter
|
||||
productStockSchema.virtual('id').get(function () {
|
||||
return this._id;
|
||||
|
||||
@ -1,7 +1,199 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { generateId } from '../../utils.js';
|
||||
import { getObject, editObject } from '../../database.js';
|
||||
const { Schema } = mongoose;
|
||||
|
||||
const parentStockModelNames = {
|
||||
filamentStock: 'filamentStock',
|
||||
partStock: 'partStock',
|
||||
productStock: 'productStock',
|
||||
};
|
||||
|
||||
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) => {
|
||||
if (!parentId) return null;
|
||||
|
||||
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 } } },
|
||||
]);
|
||||
|
||||
return {
|
||||
total: result?.total ?? 0,
|
||||
count: result?.count ?? 0,
|
||||
};
|
||||
};
|
||||
|
||||
const buildParentUpdateData = (parentType, parentStock, events) => {
|
||||
const updateData = {};
|
||||
let currentAmount;
|
||||
|
||||
if (parentType === 'filamentStock') {
|
||||
const net = events.total;
|
||||
const startingNet = parentStock.startingWeight?.net ?? 0;
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const modelName = parentStockModelNames[parentType];
|
||||
if (!modelName) return;
|
||||
|
||||
const parentModel = mongoose.model(modelName);
|
||||
const parentStock = await getObject({
|
||||
model: parentModel,
|
||||
id: parentId,
|
||||
cached: true,
|
||||
});
|
||||
if (!parentStock || parentStock.error) return;
|
||||
|
||||
const events = await getStockEventTotal(parentId, parentType);
|
||||
if (!events?.count) return;
|
||||
|
||||
await editObject({
|
||||
model: parentModel,
|
||||
id: parentStock._id,
|
||||
updateData: appendParentHistoryIfChanged(
|
||||
parentType,
|
||||
parentStock,
|
||||
buildParentUpdateData(parentType, parentStock, events)
|
||||
),
|
||||
user,
|
||||
recalculate: false,
|
||||
});
|
||||
};
|
||||
|
||||
const stockEventSchema = new Schema(
|
||||
{
|
||||
_reference: { type: String, default: () => generateId()() },
|
||||
@ -27,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 }
|
||||
@ -34,6 +232,30 @@ 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);
|
||||
};
|
||||
|
||||
// Add virtual id getter
|
||||
stockEventSchema.virtual('id').get(function () {
|
||||
return this._id;
|
||||
|
||||
@ -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