Implemented product stocks and minor improvements.
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
cf01c3aa38
commit
7c44f36590
@ -23,6 +23,7 @@ import {
|
|||||||
import { getAllModels } from '../services/misc/model.js';
|
import { getAllModels } from '../services/misc/model.js';
|
||||||
import { redisServer } from './redis.js';
|
import { redisServer } from './redis.js';
|
||||||
import { auditLogModel } from './schemas/management/auditlog.schema.js';
|
import { auditLogModel } from './schemas/management/auditlog.schema.js';
|
||||||
|
import { convertObjectIdStringsInFilter } from './utils.js';
|
||||||
|
|
||||||
const logger = log4js.getLogger('Database');
|
const logger = log4js.getLogger('Database');
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
@ -570,8 +571,12 @@ export const listObjectsByProperties = async ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (masterFilter != {}) {
|
logger.debug('Master filter:', masterFilter);
|
||||||
pipeline.push({ $match: { ...masterFilter } });
|
|
||||||
|
if (Object.keys(masterFilter).length > 0) {
|
||||||
|
const convertedFilter = convertObjectIdStringsInFilter(masterFilter);
|
||||||
|
logger.debug('Converted filter:', convertedFilter);
|
||||||
|
pipeline.push({ $match: convertedFilter });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (propertiesPresent) {
|
if (propertiesPresent) {
|
||||||
@ -593,10 +598,13 @@ export const listObjectsByProperties = async ({
|
|||||||
} else {
|
} else {
|
||||||
// If no properties specified, just return all objects without grouping
|
// If no properties specified, just return all objects without grouping
|
||||||
// Ensure pipeline is not empty by adding a $match stage if needed
|
// Ensure pipeline is not empty by adding a $match stage if needed
|
||||||
if (pipeline.length === 0 && masterFilter == {}) {
|
if (pipeline.length === 0 && Object.keys(masterFilter).length === 0) {
|
||||||
|
console.log('Adding empty match stage');
|
||||||
pipeline.push({ $match: {} });
|
pipeline.push({ $match: {} });
|
||||||
}
|
}
|
||||||
|
console.log('Running pipeline:', pipeline);
|
||||||
const results = await model.aggregate(pipeline);
|
const results = await model.aggregate(pipeline);
|
||||||
|
console.log('Results:', results);
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
64
src/database/schemas/inventory/productstock.schema.js
Normal file
64
src/database/schemas/inventory/productstock.schema.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import { generateId } from '../../utils.js';
|
||||||
|
const { Schema } = mongoose;
|
||||||
|
import { aggregateRollups, aggregateRollupsHistory } from '../../database.js';
|
||||||
|
|
||||||
|
const partStockUsageSchema = new Schema({
|
||||||
|
partStock: { type: Schema.Types.ObjectId, ref: 'partStock', required: false },
|
||||||
|
part: { type: Schema.Types.ObjectId, ref: 'part', required: true },
|
||||||
|
quantity: { type: Number, required: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define the main productStock schema - tracks assembled products consisting of part stocks
|
||||||
|
const productStockSchema = new Schema(
|
||||||
|
{
|
||||||
|
_reference: { type: String, default: () => generateId()() },
|
||||||
|
state: {
|
||||||
|
type: { type: String, required: true },
|
||||||
|
progress: { type: Number, required: false },
|
||||||
|
},
|
||||||
|
product: { type: mongoose.Schema.Types.ObjectId, ref: 'product', required: true },
|
||||||
|
currentQuantity: { type: Number, required: true },
|
||||||
|
partStocks: [partStockUsageSchema],
|
||||||
|
},
|
||||||
|
{ timestamps: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const rollupConfigs = [
|
||||||
|
{
|
||||||
|
name: 'totalCurrentQuantity',
|
||||||
|
filter: {},
|
||||||
|
rollups: [{ name: 'totalCurrentQuantity', property: 'currentQuantity', operation: 'sum' }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
productStockSchema.statics.stats = async function () {
|
||||||
|
const results = await aggregateRollups({
|
||||||
|
model: this,
|
||||||
|
rollupConfigs: rollupConfigs,
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
productStockSchema.statics.history = async function (from, to) {
|
||||||
|
const results = await aggregateRollupsHistory({
|
||||||
|
model: this,
|
||||||
|
startDate: from,
|
||||||
|
endDate: to,
|
||||||
|
rollupConfigs: rollupConfigs,
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add virtual id getter
|
||||||
|
productStockSchema.virtual('id').get(function () {
|
||||||
|
return this._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure JSON serialization to include virtuals
|
||||||
|
productStockSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
|
// Create and export the model
|
||||||
|
export const productStockModel = mongoose.model('productStock', productStockSchema);
|
||||||
@ -12,6 +12,7 @@ import { orderItemModel } from './inventory/orderitem.schema.js';
|
|||||||
import { stockEventModel } from './inventory/stockevent.schema.js';
|
import { stockEventModel } from './inventory/stockevent.schema.js';
|
||||||
import { stockAuditModel } from './inventory/stockaudit.schema.js';
|
import { stockAuditModel } from './inventory/stockaudit.schema.js';
|
||||||
import { partStockModel } from './inventory/partstock.schema.js';
|
import { partStockModel } from './inventory/partstock.schema.js';
|
||||||
|
import { productStockModel } from './inventory/productstock.schema.js';
|
||||||
import { auditLogModel } from './management/auditlog.schema.js';
|
import { auditLogModel } from './management/auditlog.schema.js';
|
||||||
import { userModel } from './management/user.schema.js';
|
import { userModel } from './management/user.schema.js';
|
||||||
import { appPasswordModel } from './management/apppassword.schema.js';
|
import { appPasswordModel } from './management/apppassword.schema.js';
|
||||||
@ -115,12 +116,12 @@ export const models = {
|
|||||||
label: 'Part Stock',
|
label: 'Part Stock',
|
||||||
},
|
},
|
||||||
PDS: {
|
PDS: {
|
||||||
model: null,
|
model: productStockModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'productStock',
|
type: 'productStock',
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
label: 'Product Stock',
|
label: 'Product Stock',
|
||||||
}, // No productStockModel found
|
},
|
||||||
ADL: {
|
ADL: {
|
||||||
model: auditLogModel,
|
model: auditLogModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
|
|||||||
@ -1,7 +1,50 @@
|
|||||||
import { customAlphabet } from 'nanoid';
|
import { customAlphabet } from 'nanoid';
|
||||||
|
import mongoose from 'mongoose';
|
||||||
|
|
||||||
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
export const generateId = () => {
|
export const generateId = () => {
|
||||||
// 10 characters
|
// 10 characters
|
||||||
return customAlphabet(ALPHABET, 12);
|
return customAlphabet(ALPHABET, 12);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Check if a value is a string that looks like a MongoDB ObjectId (24 hex chars). */
|
||||||
|
export function isObjectIdString(value) {
|
||||||
|
return typeof value === 'string' && /^[a-f\d]{24}$/i.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convert a value to ObjectId if it's a valid ObjectId string; otherwise return as-is. */
|
||||||
|
export function toObjectIdIfValid(value) {
|
||||||
|
if (isObjectIdString(value)) {
|
||||||
|
return new mongoose.Types.ObjectId(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Recursively convert ObjectId strings to ObjectId in a filter object for MongoDB $match. */
|
||||||
|
export function convertObjectIdStringsInFilter(filter) {
|
||||||
|
if (!filter || typeof filter !== 'object') return filter;
|
||||||
|
|
||||||
|
const result = {};
|
||||||
|
for (const [key, value] of Object.entries(filter)) {
|
||||||
|
if (key.startsWith('$')) {
|
||||||
|
if ((key === '$in' || key === '$nin') && Array.isArray(value)) {
|
||||||
|
result[key] = value.map((v) => (isObjectIdString(v) ? new mongoose.Types.ObjectId(v) : v));
|
||||||
|
} else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
result[key] = convertObjectIdStringsInFilter(value);
|
||||||
|
} else {
|
||||||
|
result[key] = toObjectIdIfValid(value);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
value &&
|
||||||
|
typeof value === 'object' &&
|
||||||
|
!Array.isArray(value) &&
|
||||||
|
!(value instanceof mongoose.Types.ObjectId) &&
|
||||||
|
!(value instanceof Date)
|
||||||
|
) {
|
||||||
|
result[key] = convertObjectIdStringsInFilter(value);
|
||||||
|
} else {
|
||||||
|
result[key] = toObjectIdIfValid(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import {
|
|||||||
vendorRoutes,
|
vendorRoutes,
|
||||||
materialRoutes,
|
materialRoutes,
|
||||||
partStockRoutes,
|
partStockRoutes,
|
||||||
|
productStockRoutes,
|
||||||
filamentStockRoutes,
|
filamentStockRoutes,
|
||||||
purchaseOrderRoutes,
|
purchaseOrderRoutes,
|
||||||
orderItemRoutes,
|
orderItemRoutes,
|
||||||
@ -135,6 +136,7 @@ app.use('/products', productRoutes);
|
|||||||
app.use('/vendors', vendorRoutes);
|
app.use('/vendors', vendorRoutes);
|
||||||
app.use('/materials', materialRoutes);
|
app.use('/materials', materialRoutes);
|
||||||
app.use('/partstocks', partStockRoutes);
|
app.use('/partstocks', partStockRoutes);
|
||||||
|
app.use('/productstocks', productStockRoutes);
|
||||||
app.use('/filamentstocks', filamentStockRoutes);
|
app.use('/filamentstocks', filamentStockRoutes);
|
||||||
app.use('/purchaseorders', purchaseOrderRoutes);
|
app.use('/purchaseorders', purchaseOrderRoutes);
|
||||||
app.use('/orderitems', orderItemRoutes);
|
app.use('/orderitems', orderItemRoutes);
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import productRoutes from './management/products.js';
|
|||||||
import vendorRoutes from './management/vendors.js';
|
import vendorRoutes from './management/vendors.js';
|
||||||
import materialRoutes from './management/materials.js';
|
import materialRoutes from './management/materials.js';
|
||||||
import partStockRoutes from './inventory/partstocks.js';
|
import partStockRoutes from './inventory/partstocks.js';
|
||||||
|
import productStockRoutes from './inventory/productstocks.js';
|
||||||
import filamentStockRoutes from './inventory/filamentstocks.js';
|
import filamentStockRoutes from './inventory/filamentstocks.js';
|
||||||
import purchaseOrderRoutes from './inventory/purchaseorders.js';
|
import purchaseOrderRoutes from './inventory/purchaseorders.js';
|
||||||
import orderItemRoutes from './inventory/orderitems.js';
|
import orderItemRoutes from './inventory/orderitems.js';
|
||||||
@ -58,6 +59,7 @@ export {
|
|||||||
vendorRoutes,
|
vendorRoutes,
|
||||||
materialRoutes,
|
materialRoutes,
|
||||||
partStockRoutes,
|
partStockRoutes,
|
||||||
|
productStockRoutes,
|
||||||
filamentStockRoutes,
|
filamentStockRoutes,
|
||||||
purchaseOrderRoutes,
|
purchaseOrderRoutes,
|
||||||
orderItemRoutes,
|
orderItemRoutes,
|
||||||
|
|||||||
64
src/routes/inventory/productstocks.js
Normal file
64
src/routes/inventory/productstocks.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { isAuthenticated } from '../../keycloak.js';
|
||||||
|
import { getFilter, convertPropertiesString } from '../../utils.js';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
import {
|
||||||
|
listProductStocksRouteHandler,
|
||||||
|
getProductStockRouteHandler,
|
||||||
|
editProductStockRouteHandler,
|
||||||
|
editMultipleProductStocksRouteHandler,
|
||||||
|
newProductStockRouteHandler,
|
||||||
|
deleteProductStockRouteHandler,
|
||||||
|
listProductStocksByPropertiesRouteHandler,
|
||||||
|
getProductStockStatsRouteHandler,
|
||||||
|
getProductStockHistoryRouteHandler,
|
||||||
|
} from '../../services/inventory/productstocks.js';
|
||||||
|
|
||||||
|
router.get('/', isAuthenticated, (req, res) => {
|
||||||
|
const { page, limit, property, search, sort, order } = req.query;
|
||||||
|
const allowedFilters = ['product', 'state', 'currentQuantity', 'product._id'];
|
||||||
|
const filter = getFilter(req.query, allowedFilters);
|
||||||
|
listProductStocksRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/properties', isAuthenticated, (req, res) => {
|
||||||
|
let properties = convertPropertiesString(req.query.properties);
|
||||||
|
const allowedFilters = ['product', 'state.type'];
|
||||||
|
const filter = getFilter(req.query, allowedFilters, false);
|
||||||
|
var masterFilter = {};
|
||||||
|
if (req.query.masterFilter) {
|
||||||
|
masterFilter = JSON.parse(req.query.masterFilter);
|
||||||
|
}
|
||||||
|
listProductStocksByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/', isAuthenticated, (req, res) => {
|
||||||
|
newProductStockRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/stats', isAuthenticated, (req, res) => {
|
||||||
|
getProductStockStatsRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/history', isAuthenticated, (req, res) => {
|
||||||
|
getProductStockHistoryRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:id', isAuthenticated, (req, res) => {
|
||||||
|
getProductStockRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/', isAuthenticated, async (req, res) => {
|
||||||
|
editMultipleProductStocksRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/:id', isAuthenticated, async (req, res) => {
|
||||||
|
editProductStockRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/:id', isAuthenticated, async (req, res) => {
|
||||||
|
deleteProductStockRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
221
src/services/inventory/productstocks.js
Normal file
221
src/services/inventory/productstocks.js
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
import config from '../../config.js';
|
||||||
|
import { productStockModel } from '../../database/schemas/inventory/productstock.schema.js';
|
||||||
|
import log4js from 'log4js';
|
||||||
|
import mongoose from 'mongoose';
|
||||||
|
import {
|
||||||
|
deleteObject,
|
||||||
|
listObjects,
|
||||||
|
getObject,
|
||||||
|
editObject,
|
||||||
|
editObjects,
|
||||||
|
newObject,
|
||||||
|
listObjectsByProperties,
|
||||||
|
getModelStats,
|
||||||
|
getModelHistory,
|
||||||
|
} from '../../database/database.js';
|
||||||
|
import { productModel } from '../../database/schemas/management/product.schema.js';
|
||||||
|
const logger = log4js.getLogger('Product Stocks');
|
||||||
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
|
export const listProductStocksRouteHandler = async (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
page = 1,
|
||||||
|
limit = 25,
|
||||||
|
property = '',
|
||||||
|
filter = {},
|
||||||
|
search = '',
|
||||||
|
sort = '',
|
||||||
|
order = 'ascend'
|
||||||
|
) => {
|
||||||
|
const result = await listObjects({
|
||||||
|
model: productStockModel,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
property,
|
||||||
|
filter,
|
||||||
|
search,
|
||||||
|
sort,
|
||||||
|
order,
|
||||||
|
populate: [{ path: 'product' }, { path: 'partStocks.partStock' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error listing product stocks.');
|
||||||
|
res.status(result.code).send(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`List of product stocks (Page ${page}, Limit ${limit}). Count: ${result.length}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listProductStocksByPropertiesRouteHandler = async (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
properties = '',
|
||||||
|
filter = {},
|
||||||
|
masterFilter = {}
|
||||||
|
) => {
|
||||||
|
const result = await listObjectsByProperties({
|
||||||
|
model: productStockModel,
|
||||||
|
properties,
|
||||||
|
filter,
|
||||||
|
populate: ['product', 'partStocks.partStock'],
|
||||||
|
masterFilter,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error listing product stocks.');
|
||||||
|
res.status(result.code).send(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`List of product stocks. Count: ${result.length}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getProductStockRouteHandler = async (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
const result = await getObject({
|
||||||
|
model: productStockModel,
|
||||||
|
id,
|
||||||
|
populate: [{ path: 'partStocks.part' }, { path: 'partStocks.partStock' }, { path: 'product' }],
|
||||||
|
});
|
||||||
|
if (result?.error) {
|
||||||
|
logger.warn(`Product Stock not found with supplied id.`);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
logger.debug(`Retrieved product stock with ID: ${id}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const editProductStockRouteHandler = async (req, res) => {
|
||||||
|
const id = new mongoose.Types.ObjectId(req.params.id);
|
||||||
|
|
||||||
|
logger.trace(`Product Stock with ID: ${id}`);
|
||||||
|
|
||||||
|
const updateData = {
|
||||||
|
partStocks: req.body?.partStocks?.map((partStock) => ({
|
||||||
|
quantity: partStock.quantity,
|
||||||
|
partStock: partStock.partStock,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await editObject({
|
||||||
|
model: productStockModel,
|
||||||
|
id,
|
||||||
|
updateData,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
logger.error('Error editing product stock:', result.error);
|
||||||
|
res.status(result).send(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Edited product stock with ID: ${id}`);
|
||||||
|
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const editMultipleProductStocksRouteHandler = 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: productStockModel,
|
||||||
|
updates,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
logger.error('Error editing product stocks:', result.error);
|
||||||
|
res.status(result.code || 500).send(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Edited ${updates.length} product stocks`);
|
||||||
|
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const newProductStockRouteHandler = async (req, res) => {
|
||||||
|
const productId = new mongoose.Types.ObjectId(req.body.product?._id);
|
||||||
|
const product = await getObject({
|
||||||
|
model: productModel,
|
||||||
|
id: productId,
|
||||||
|
});
|
||||||
|
const newData = {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
currentQuantity: req.body.currentQuantity,
|
||||||
|
product: req.body.product,
|
||||||
|
state: req.body.state,
|
||||||
|
partStocks: product.parts.map((part) => ({
|
||||||
|
part: part.part,
|
||||||
|
quantity: part.quantity,
|
||||||
|
partStock: undefined,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
const result = await newObject({
|
||||||
|
model: productStockModel,
|
||||||
|
newData,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
if (result.error) {
|
||||||
|
logger.error('No product stock created:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`New product stock with ID: ${result._id}`);
|
||||||
|
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteProductStockRouteHandler = async (req, res) => {
|
||||||
|
const id = new mongoose.Types.ObjectId(req.params.id);
|
||||||
|
|
||||||
|
logger.trace(`Product Stock with ID: ${id}`);
|
||||||
|
|
||||||
|
const result = await deleteObject({
|
||||||
|
model: productStockModel,
|
||||||
|
id,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
if (result.error) {
|
||||||
|
logger.error('No product stock deleted:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Deleted product stock with ID: ${result._id}`);
|
||||||
|
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getProductStockStatsRouteHandler = async (req, res) => {
|
||||||
|
const result = await getModelStats({ model: productStockModel });
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error fetching product stock stats:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
logger.trace('Product stock stats:', result);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getProductStockHistoryRouteHandler = async (req, res) => {
|
||||||
|
const from = req.query.from;
|
||||||
|
const to = req.query.to;
|
||||||
|
const result = await getModelHistory({ model: productStockModel, from, to });
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error fetching product stock history:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
logger.trace('Product stock history:', result);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
@ -1,161 +0,0 @@
|
|||||||
import config from '../../config.js';
|
|
||||||
import { jobModel } from '../../database/schemas/production/job.schema.js';
|
|
||||||
import { subJobModel } from '../../database/schemas/production/subjob.schema.js';
|
|
||||||
import log4js from 'log4js';
|
|
||||||
import { printerModel } from '../../database/schemas/production/printer.schema.js';
|
|
||||||
import { filamentModel } from '../../database/schemas/management/filament.schema.js';
|
|
||||||
import { gcodeFileModel } from '../../database/schemas/production/gcodefile.schema.js';
|
|
||||||
import { partModel } from '../../database/schemas/management/part.schema.js';
|
|
||||||
import { productModel } from '../../database/schemas/management/product.schema.js';
|
|
||||||
import { vendorModel } from '../../database/schemas/management/vendor.schema.js';
|
|
||||||
import { filamentStockModel } from '../../database/schemas/inventory/filamentstock.schema.js';
|
|
||||||
import { stockEventModel } from '../../database/schemas/inventory/stockevent.schema.js';
|
|
||||||
import { stockAuditModel } from '../../database/schemas/inventory/stockaudit.schema.js';
|
|
||||||
import { partStockModel } from '../../database/schemas/inventory/partstock.schema.js';
|
|
||||||
import { auditLogModel } from '../../database/schemas/management/auditlog.schema.js';
|
|
||||||
import { userModel } from '../../database/schemas/management/user.schema.js';
|
|
||||||
import { noteTypeModel } from '../../database/schemas/management/notetype.schema.js';
|
|
||||||
import { noteModel } from '../../database/schemas/misc/note.schema.js';
|
|
||||||
import mongoose from 'mongoose';
|
|
||||||
|
|
||||||
const logger = log4js.getLogger('Jobs');
|
|
||||||
logger.level = config.server.logLevel;
|
|
||||||
|
|
||||||
// Map prefixes to models and id fields
|
|
||||||
const PREFIX_MODEL_MAP = {
|
|
||||||
PRN: { model: printerModel, idField: '_id', type: 'printer' },
|
|
||||||
FIL: { model: filamentModel, idField: '_id', type: 'filament' },
|
|
||||||
SPL: { model: null, idField: '_id', type: 'spool' }, // No spool model found
|
|
||||||
GCF: { model: gcodeFileModel, idField: '_id', type: 'gcodefile' },
|
|
||||||
JOB: { model: jobModel, idField: '_id', type: 'job' },
|
|
||||||
PRT: { model: partModel, idField: '_id', type: 'part' },
|
|
||||||
PRD: { model: productModel, idField: '_id', type: 'product' },
|
|
||||||
VEN: { model: vendorModel, idField: '_id', type: 'vendor' },
|
|
||||||
SJB: { model: subJobModel, idField: '_id', type: 'subjob' },
|
|
||||||
FLS: { model: filamentStockModel, idField: '_id', type: 'filamentstock' },
|
|
||||||
SEV: { model: stockEventModel, idField: '_id', type: 'stockevent' },
|
|
||||||
SAU: { model: stockAuditModel, idField: '_id', type: 'stockaudit' },
|
|
||||||
PTS: { model: partStockModel, idField: '_id', type: 'partstock' },
|
|
||||||
PDS: { model: null, idField: '_id', type: 'productstock' }, // No productStockModel found
|
|
||||||
ADL: { model: auditLogModel, idField: '_id', type: 'auditlog' },
|
|
||||||
USR: { model: userModel, idField: '_id', type: 'user' },
|
|
||||||
NTY: { model: noteTypeModel, idField: '_id', type: 'notetype' },
|
|
||||||
NTE: { model: noteModel, idField: '_id', type: 'note' },
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to build search filter from query parameters
|
|
||||||
const buildSearchFilter = (params) => {
|
|
||||||
const filter = {};
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(params)) {
|
|
||||||
// Skip pagination and limit parameters as they're not search filters
|
|
||||||
if (key === 'limit' || key === 'page') continue;
|
|
||||||
|
|
||||||
// Handle different field types
|
|
||||||
if (key === 'name') {
|
|
||||||
filter.name = { $regex: value, $options: 'i' }; // Case-insensitive search
|
|
||||||
} else if (key === 'id' || key === '_id') {
|
|
||||||
if (mongoose.Types.ObjectId.isValid(value)) {
|
|
||||||
filter._id = value;
|
|
||||||
}
|
|
||||||
} else if (key === 'tags') {
|
|
||||||
filter.tags = { $in: [new RegExp(value, 'i')] };
|
|
||||||
} else if (key === 'state') {
|
|
||||||
filter['state.type'] = value;
|
|
||||||
} else if (key.includes('.')) {
|
|
||||||
// Handle nested fields like 'state.type', 'address.city', etc.
|
|
||||||
filter[key] = { $regex: value, $options: 'i' };
|
|
||||||
} else {
|
|
||||||
// For all other fields, do a case-insensitive search
|
|
||||||
filter[key] = { $regex: value, $options: 'i' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filter;
|
|
||||||
};
|
|
||||||
|
|
||||||
const trimSpotlightObject = (object) => {
|
|
||||||
return {
|
|
||||||
_id: object._id,
|
|
||||||
name: object.name || undefined,
|
|
||||||
state: object.state && object?.state.type ? { type: object.state.type } : undefined,
|
|
||||||
tags: object.tags || undefined,
|
|
||||||
email: object.email || undefined,
|
|
||||||
color: object.color || undefined,
|
|
||||||
updatedAt: object.updatedAt || undefined,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSpotlightRouteHandler = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const query = req.params.query;
|
|
||||||
const queryParams = req.query;
|
|
||||||
if (query.length < 3) {
|
|
||||||
res.status(200).send([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const prefix = query.substring(0, 3);
|
|
||||||
const delimiter = query.substring(3, 4);
|
|
||||||
const suffix = query.substring(4);
|
|
||||||
|
|
||||||
if (delimiter == ':') {
|
|
||||||
const prefixEntry = PREFIX_MODEL_MAP[prefix];
|
|
||||||
if (!prefixEntry || !prefixEntry.model) {
|
|
||||||
res.status(400).send({ error: 'Invalid or unsupported prefix' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { model, idField } = prefixEntry;
|
|
||||||
|
|
||||||
// Validate ObjectId if the idField is '_id'
|
|
||||||
if (idField === '_id' && !mongoose.Types.ObjectId.isValid(suffix)) {
|
|
||||||
res.status(404).send({ error: `${prefix} not found` });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the object by the correct field
|
|
||||||
const queryObj = {};
|
|
||||||
queryObj[idField] = suffix.toLowerCase();
|
|
||||||
let doc = await model.findOne(queryObj).lean();
|
|
||||||
if (!doc) {
|
|
||||||
res.status(404).send({ error: `${prefix} not found` });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Build the response with only the required fields
|
|
||||||
const response = trimSpotlightObject(doc);
|
|
||||||
res.status(200).send(response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(queryParams).length > 0) {
|
|
||||||
const prefixEntry = PREFIX_MODEL_MAP[prefix];
|
|
||||||
if (!prefixEntry || !prefixEntry.model) {
|
|
||||||
res.status(400).send({ error: 'Invalid or unsupported prefix' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { model } = prefixEntry;
|
|
||||||
|
|
||||||
// Use req.query for search parameters
|
|
||||||
|
|
||||||
if (Object.keys(queryParams).length === 0) {
|
|
||||||
res.status(400).send({ error: 'No search parameters provided' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build search filter
|
|
||||||
const searchFilter = buildSearchFilter(queryParams);
|
|
||||||
|
|
||||||
// Perform search with limit
|
|
||||||
const limit = parseInt(req.query.limit) || 10;
|
|
||||||
const docs = await model.find(searchFilter).limit(limit).sort({ updatedAt: -1 }).lean();
|
|
||||||
|
|
||||||
// Format response
|
|
||||||
const response = docs.map((doc) => trimSpotlightObject(doc));
|
|
||||||
|
|
||||||
res.status(200).send(response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error in spotlight lookup:', error);
|
|
||||||
res.status(500).send({ error: error });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -64,6 +64,7 @@ function getModelFilterFields(objectType) {
|
|||||||
subJob: ['job'],
|
subJob: ['job'],
|
||||||
filamentStock: ['filament'],
|
filamentStock: ['filament'],
|
||||||
partStock: ['part'],
|
partStock: ['part'],
|
||||||
|
productStock: ['product'],
|
||||||
purchaseOrder: ['vendor'],
|
purchaseOrder: ['vendor'],
|
||||||
orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'],
|
orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'],
|
||||||
shipment: ['order._id', 'orderType', 'courierService._id'],
|
shipment: ['order._id', 'orderType', 'courierService._id'],
|
||||||
|
|||||||
@ -70,6 +70,7 @@ function getModelFilterFields(objectType) {
|
|||||||
subJob: ['job'],
|
subJob: ['job'],
|
||||||
filamentStock: ['filament'],
|
filamentStock: ['filament'],
|
||||||
partStock: ['part'],
|
partStock: ['part'],
|
||||||
|
productStock: ['product'],
|
||||||
purchaseOrder: ['vendor'],
|
purchaseOrder: ['vendor'],
|
||||||
orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'],
|
orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'],
|
||||||
shipment: ['order._id', 'orderType', 'courierService._id'],
|
shipment: ['order._id', 'orderType', 'courierService._id'],
|
||||||
|
|||||||
@ -344,6 +344,7 @@ function getModelFilterFields(objectType) {
|
|||||||
subJob: ['job'],
|
subJob: ['job'],
|
||||||
filamentStock: ['filament'],
|
filamentStock: ['filament'],
|
||||||
partStock: ['part'],
|
partStock: ['part'],
|
||||||
|
productStock: ['product'],
|
||||||
purchaseOrder: ['vendor'],
|
purchaseOrder: ['vendor'],
|
||||||
orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'],
|
orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'],
|
||||||
shipment: ['order._id', 'orderType', 'courierService._id'],
|
shipment: ['order._id', 'orderType', 'courierService._id'],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user