Add stock audit level management functionality and integrate with existing inventory system
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 introduces a new `stockAuditLevel` schema and corresponding routes for managing stock audit levels, enhancing the inventory management capabilities. It includes CRUD operations for stock audit levels, allowing for the creation, retrieval, updating, and deletion of audit levels. The integration with existing stock audit functionalities is established, enabling the association of audit levels with stock audits. Additionally, utility functions for handling stock audit levels are implemented, improving the overall structure and maintainability of the inventory system.
This commit is contained in:
parent
ec5783acd8
commit
bce40ce30d
@ -2,6 +2,13 @@ import mongoose from 'mongoose';
|
|||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
import { aggregateRollups, aggregateRollupsHistory } from '../../database.js';
|
import { aggregateRollups, aggregateRollupsHistory } from '../../database.js';
|
||||||
|
import { updateDraftStockAuditCurrents } from './stockaudit.schema.js';
|
||||||
|
|
||||||
|
const toId = (value) => {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (typeof value === 'object' && value._id) return String(value._id);
|
||||||
|
return String(value);
|
||||||
|
};
|
||||||
|
|
||||||
// Define the main filamentStock schema
|
// Define the main filamentStock schema
|
||||||
const filamentStockSchema = new Schema(
|
const filamentStockSchema = new Schema(
|
||||||
@ -96,6 +103,19 @@ filamentStockSchema.statics.history = async function (from, to) {
|
|||||||
return results;
|
return results;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
filamentStockSchema.statics.recalculate = async function (filamentStock, user) {
|
||||||
|
const itemSkuId = toId(filamentStock?.filamentSku);
|
||||||
|
const stockLocationId = toId(filamentStock?.stockLocation);
|
||||||
|
if (!itemSkuId || !stockLocationId) return;
|
||||||
|
|
||||||
|
await updateDraftStockAuditCurrents({
|
||||||
|
itemType: 'filament',
|
||||||
|
itemSkuId,
|
||||||
|
stockLocationId,
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Add virtual id getter
|
// Add virtual id getter
|
||||||
filamentStockSchema.virtual('id').get(function () {
|
filamentStockSchema.virtual('id').get(function () {
|
||||||
return this._id;
|
return this._id;
|
||||||
|
|||||||
@ -1,6 +1,13 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
||||||
|
import { updateDraftStockAuditCurrents } from './stockaudit.schema.js';
|
||||||
|
|
||||||
|
const toId = (value) => {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (typeof value === 'object' && value._id) return String(value._id);
|
||||||
|
return String(value);
|
||||||
|
};
|
||||||
|
|
||||||
// Define the main partStock schema
|
// Define the main partStock schema
|
||||||
const partStockSchema = new mongoose.Schema(
|
const partStockSchema = new mongoose.Schema(
|
||||||
@ -89,6 +96,18 @@ partStockSchema.statics.history = async function (from, to) {
|
|||||||
|
|
||||||
partStockSchema.statics.recalculate = async function (partStock, user) {
|
partStockSchema.statics.recalculate = async function (partStock, user) {
|
||||||
if (!partStock?._id) return;
|
if (!partStock?._id) return;
|
||||||
|
|
||||||
|
const itemSkuId = toId(partStock.partSku);
|
||||||
|
const stockLocationId = toId(partStock.stockLocation);
|
||||||
|
if (itemSkuId && stockLocationId) {
|
||||||
|
await updateDraftStockAuditCurrents({
|
||||||
|
itemType: 'part',
|
||||||
|
itemSkuId,
|
||||||
|
stockLocationId,
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (partStock.state?.type === 'draft' || partStock.state?.type === 'consumed') return;
|
if (partStock.state?.type === 'draft' || partStock.state?.type === 'consumed') return;
|
||||||
if ((Number(partStock.currentQuantity) || 0) > 0) return;
|
if ((Number(partStock.currentQuantity) || 0) > 0) return;
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import mongoose from 'mongoose';
|
|||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
||||||
|
import { updateDraftStockAuditCurrents } from './stockaudit.schema.js';
|
||||||
|
|
||||||
const partStockListItemSchema = new Schema({
|
const partStockListItemSchema = new Schema({
|
||||||
part: { type: Schema.Types.ObjectId, ref: 'part', required: true },
|
part: { type: Schema.Types.ObjectId, ref: 'part', required: true },
|
||||||
@ -122,6 +123,17 @@ productStockSchema.statics.history = async function (from, to) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
productStockSchema.statics.recalculate = async function (productStock, user) {
|
productStockSchema.statics.recalculate = async function (productStock, user) {
|
||||||
|
const productSkuId = toId(productStock?.productSku);
|
||||||
|
const stockLocationId = toId(productStock?.stockLocation);
|
||||||
|
if (productSkuId && stockLocationId) {
|
||||||
|
await updateDraftStockAuditCurrents({
|
||||||
|
itemType: 'product',
|
||||||
|
itemSkuId: productSkuId,
|
||||||
|
stockLocationId,
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
productStock?._id &&
|
productStock?._id &&
|
||||||
productStock.state?.type !== 'draft' &&
|
productStock.state?.type !== 'draft' &&
|
||||||
@ -139,8 +151,6 @@ productStockSchema.statics.recalculate = async function (productStock, user) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const productSkuId = toId(productStock?.productSku);
|
|
||||||
const stockLocationId = toId(productStock?.stockLocation);
|
|
||||||
if (!productSkuId || !stockLocationId) {
|
if (!productSkuId || !stockLocationId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,42 +1,311 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
|
import { editObject, getObject } from '../../database.js';
|
||||||
|
import { stockAuditLevelModel, normalizeAuditLevelLine } from '../management/stockauditlevel.schema.js';
|
||||||
|
import { filamentModel } from '../management/filament.schema.js';
|
||||||
|
import { filamentSkuModel } from '../management/filamentsku.schema.js';
|
||||||
|
import { partModel } from '../management/part.schema.js';
|
||||||
|
import { partSkuModel } from '../management/partsku.schema.js';
|
||||||
|
import { productModel } from '../management/product.schema.js';
|
||||||
|
import { productSkuModel } from '../management/productsku.schema.js';
|
||||||
|
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
const stockAuditItemSchema = new Schema({
|
const itemModelsByType = {
|
||||||
type: { type: String, enum: ['filament', 'part'], required: true },
|
filament: filamentModel,
|
||||||
stock: { type: Schema.Types.ObjectId, required: true },
|
part: partModel,
|
||||||
expectedQuantity: { type: Number, required: true },
|
product: productModel,
|
||||||
actualQuantity: { type: Number, required: true },
|
};
|
||||||
notes: { type: String },
|
|
||||||
});
|
const skuModelsByType = {
|
||||||
|
filament: filamentSkuModel,
|
||||||
|
part: partSkuModel,
|
||||||
|
product: productSkuModel,
|
||||||
|
};
|
||||||
|
|
||||||
|
const parentFieldByType = {
|
||||||
|
filament: 'filament',
|
||||||
|
part: 'part',
|
||||||
|
product: 'product',
|
||||||
|
};
|
||||||
|
|
||||||
|
const toId = (value) => {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (typeof value === 'object' && value._id != null) return String(value._id);
|
||||||
|
return String(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stockAuditLineSchema = new Schema(
|
||||||
|
{
|
||||||
|
itemType: {
|
||||||
|
type: String,
|
||||||
|
enum: ['filament', 'part', 'product'],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
item: { type: Schema.Types.ObjectId, refPath: 'auditLines.itemType', required: true },
|
||||||
|
itemSku: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: function () {
|
||||||
|
return ['filament', 'part', 'product'].includes(this.itemType)
|
||||||
|
? this.itemType + 'Sku'
|
||||||
|
: null;
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
current: { type: Number, required: true, default: 0 },
|
||||||
|
actual: { type: Number, required: true, default: 0 },
|
||||||
|
new: { type: Number, required: true, default: 0 },
|
||||||
|
},
|
||||||
|
{ _id: true }
|
||||||
|
);
|
||||||
|
|
||||||
const stockAuditSchema = new Schema(
|
const stockAuditSchema = new Schema(
|
||||||
{
|
{
|
||||||
_reference: { type: String, default: () => generateId()() },
|
_reference: { type: String, default: () => generateId()() },
|
||||||
type: { type: String, required: true },
|
state: {
|
||||||
status: {
|
type: { type: String, required: true, default: 'draft' },
|
||||||
type: String,
|
progress: { type: Number, required: false },
|
||||||
enum: ['pending', 'in_progress', 'completed', 'cancelled'],
|
},
|
||||||
default: 'pending',
|
auditLevel: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'stockAuditLevel',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
notes: { type: String },
|
stockLocation: {
|
||||||
items: [stockAuditItemSchema],
|
type: Schema.Types.ObjectId,
|
||||||
createdBy: { type: Schema.Types.ObjectId, ref: 'user', required: true },
|
ref: 'stockLocation',
|
||||||
completedAt: { type: Date },
|
required: true,
|
||||||
|
},
|
||||||
|
postedAt: { type: Date, required: false },
|
||||||
|
auditLines: { type: [stockAuditLineSchema], default: [] },
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
stockAuditSchema.index({ type: 'text', status: 'text', notes: 'text' });
|
stockAuditSchema.index({ 'state.type': 'text' });
|
||||||
|
|
||||||
|
stockAuditSchema.statics.stats = async function () {
|
||||||
|
const [draft, complete] = await Promise.all([
|
||||||
|
this.countDocuments({ 'state.type': 'draft' }),
|
||||||
|
this.countDocuments({ 'state.type': 'complete' }),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
draft: { count: draft },
|
||||||
|
complete: { count: complete },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
stockAuditSchema.statics.history = async function () {
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
async function fetchItemsForLevelLine(levelLine) {
|
||||||
|
const itemType = levelLine.itemType;
|
||||||
|
const itemModel = itemModelsByType[itemType];
|
||||||
|
if (!itemModel) return [];
|
||||||
|
|
||||||
|
if (levelLine.allItems) {
|
||||||
|
return itemModel.find().select('_id').lean();
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemId = toId(levelLine.item);
|
||||||
|
if (!itemId) return [];
|
||||||
|
return [{ _id: itemId }];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSkusForLevelLine(levelLine, itemId) {
|
||||||
|
const itemType = levelLine.itemType;
|
||||||
|
const skuModel = skuModelsByType[itemType];
|
||||||
|
const parentField = parentFieldByType[itemType];
|
||||||
|
if (!skuModel || !parentField) return [];
|
||||||
|
|
||||||
|
if (levelLine.allSkus) {
|
||||||
|
return skuModel.find({ [parentField]: itemId }).select('_id').lean();
|
||||||
|
}
|
||||||
|
|
||||||
|
const skuId = toId(levelLine.itemSku);
|
||||||
|
if (!skuId) return [];
|
||||||
|
return [{ _id: skuId }];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCurrentQuantityAtLocation(itemType, itemSkuId, stockLocationId) {
|
||||||
|
const locationId = toId(stockLocationId);
|
||||||
|
const skuId = toId(itemSkuId);
|
||||||
|
if (!locationId || !skuId) return 0;
|
||||||
|
|
||||||
|
if (itemType === 'filament') {
|
||||||
|
const stocks = await mongoose
|
||||||
|
.model('filamentStock')
|
||||||
|
.find({ filamentSku: skuId, stockLocation: locationId })
|
||||||
|
.select('currentWeight.net')
|
||||||
|
.lean();
|
||||||
|
return stocks.reduce((sum, stock) => sum + (Number(stock.currentWeight?.net) || 0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemType === 'part') {
|
||||||
|
const stocks = await mongoose
|
||||||
|
.model('partStock')
|
||||||
|
.find({ partSku: skuId, stockLocation: locationId })
|
||||||
|
.select('currentQuantity')
|
||||||
|
.lean();
|
||||||
|
return stocks.reduce((sum, stock) => sum + (Number(stock.currentQuantity) || 0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemType === 'product') {
|
||||||
|
const stocks = await mongoose
|
||||||
|
.model('productStock')
|
||||||
|
.find({ productSku: skuId, stockLocation: locationId })
|
||||||
|
.select('currentQuantity')
|
||||||
|
.lean();
|
||||||
|
return stocks.reduce((sum, stock) => sum + (Number(stock.currentQuantity) || 0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAuditLineQuantities(current, actual) {
|
||||||
|
const currentVal = Number(current) || 0;
|
||||||
|
const actualVal = Number(actual) || 0;
|
||||||
|
return {
|
||||||
|
current: currentVal,
|
||||||
|
actual: actualVal,
|
||||||
|
new: actualVal,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExistingActual(line) {
|
||||||
|
if (line?.actual != null) return Number(line.actual);
|
||||||
|
if (line?.actualQuantity != null) return Number(line.actualQuantity);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAuditLineKey(itemType, itemId, itemSkuId) {
|
||||||
|
return `${itemType}:${toId(itemId)}:${toId(itemSkuId)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expandLevelLinesToAuditLines(levelLines, stockLocationId, existingLines = []) {
|
||||||
|
const existingByKey = new Map();
|
||||||
|
for (const line of existingLines) {
|
||||||
|
existingByKey.set(
|
||||||
|
buildAuditLineKey(line.itemType, line.item, line.itemSku),
|
||||||
|
line
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auditLines = [];
|
||||||
|
|
||||||
|
for (const levelLine of levelLines || []) {
|
||||||
|
const items = await fetchItemsForLevelLine(levelLine);
|
||||||
|
for (const item of items) {
|
||||||
|
const itemId = toId(item._id);
|
||||||
|
const skus = await fetchSkusForLevelLine(levelLine, itemId);
|
||||||
|
for (const sku of skus) {
|
||||||
|
const itemSkuId = toId(sku._id);
|
||||||
|
const current = await getCurrentQuantityAtLocation(
|
||||||
|
levelLine.itemType,
|
||||||
|
itemSkuId,
|
||||||
|
stockLocationId
|
||||||
|
);
|
||||||
|
const key = buildAuditLineKey(levelLine.itemType, itemId, itemSkuId);
|
||||||
|
const existing = existingByKey.get(key);
|
||||||
|
const existingActual = getExistingActual(existing);
|
||||||
|
const actual = existingActual != null ? existingActual : current;
|
||||||
|
|
||||||
|
auditLines.push({
|
||||||
|
itemType: levelLine.itemType,
|
||||||
|
item: itemId,
|
||||||
|
itemSku: itemSkuId,
|
||||||
|
...buildAuditLineQuantities(current, actual),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return auditLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
stockAuditSchema.statics.recalculate = async function (stockAudit, user) {
|
||||||
|
if (stockAudit?.state?.type !== 'draft') return;
|
||||||
|
|
||||||
|
const auditLevelId = toId(stockAudit.auditLevel?._id ?? stockAudit.auditLevel);
|
||||||
|
const stockLocationId = toId(stockAudit.stockLocation?._id ?? stockAudit.stockLocation);
|
||||||
|
if (!auditLevelId || !stockLocationId) return;
|
||||||
|
|
||||||
|
const auditLevel = await getObject({
|
||||||
|
model: stockAuditLevelModel,
|
||||||
|
id: auditLevelId,
|
||||||
|
populate: [
|
||||||
|
{ path: 'auditLines.item' },
|
||||||
|
{ path: 'auditLines.itemSku' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!auditLevel || auditLevel.error) return;
|
||||||
|
|
||||||
|
const auditLines = await expandLevelLinesToAuditLines(
|
||||||
|
(auditLevel.auditLines || []).map((line) => normalizeAuditLevelLine(line)),
|
||||||
|
stockLocationId,
|
||||||
|
stockAudit.auditLines
|
||||||
|
);
|
||||||
|
|
||||||
|
await editObject({
|
||||||
|
model: this,
|
||||||
|
id: stockAudit._id,
|
||||||
|
updateData: { auditLines },
|
||||||
|
user,
|
||||||
|
recalculate: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Add virtual id getter
|
|
||||||
stockAuditSchema.virtual('id').get(function () {
|
stockAuditSchema.virtual('id').get(function () {
|
||||||
return this._id;
|
return this._id;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Configure JSON serialization to include virtuals
|
|
||||||
stockAuditSchema.set('toJSON', { virtuals: true });
|
stockAuditSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
// Create and export the model
|
|
||||||
export const stockAuditModel = mongoose.model('stockAudit', stockAuditSchema);
|
export const stockAuditModel = mongoose.model('stockAudit', stockAuditSchema);
|
||||||
|
|
||||||
|
export async function updateDraftStockAuditCurrents({
|
||||||
|
itemType,
|
||||||
|
itemSkuId,
|
||||||
|
stockLocationId,
|
||||||
|
user,
|
||||||
|
}) {
|
||||||
|
const skuId = toId(itemSkuId);
|
||||||
|
const locationId = toId(stockLocationId);
|
||||||
|
if (!itemType || !skuId || !locationId) return;
|
||||||
|
|
||||||
|
const current = await getCurrentQuantityAtLocation(itemType, skuId, locationId);
|
||||||
|
|
||||||
|
const draftAudits = await stockAuditModel
|
||||||
|
.find({
|
||||||
|
'state.type': 'draft',
|
||||||
|
stockLocation: locationId,
|
||||||
|
})
|
||||||
|
.lean();
|
||||||
|
|
||||||
|
for (const audit of draftAudits) {
|
||||||
|
let changed = false;
|
||||||
|
const auditLines = (audit.auditLines || []).map((line) => {
|
||||||
|
if (line.itemType !== itemType || toId(line.itemSku) !== skuId) {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
const lineCurrent = Number(line.current) || 0;
|
||||||
|
if (lineCurrent === current) {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
return { ...line, current };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!changed) continue;
|
||||||
|
|
||||||
|
await editObject({
|
||||||
|
model: stockAuditModel,
|
||||||
|
id: audit._id,
|
||||||
|
updateData: { auditLines },
|
||||||
|
user,
|
||||||
|
recalculate: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
108
src/database/schemas/management/stockauditlevel.schema.js
Normal file
108
src/database/schemas/management/stockauditlevel.schema.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import { generateId } from '../../utils.js';
|
||||||
|
import { editObject } from '../../database.js';
|
||||||
|
|
||||||
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
|
const toId = (value) => {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (typeof value === 'object' && value._id != null) return String(value._id);
|
||||||
|
return String(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;
|
||||||
|
|
||||||
|
return {
|
||||||
|
_id: line?._id,
|
||||||
|
itemType: line?.itemType,
|
||||||
|
allItems,
|
||||||
|
allSkus,
|
||||||
|
item,
|
||||||
|
itemSku,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function auditLevelLineChanged(before, after) {
|
||||||
|
return (
|
||||||
|
Boolean(before?.allItems) !== Boolean(after.allItems) ||
|
||||||
|
Boolean(before?.allSkus) !== Boolean(after.allSkus) ||
|
||||||
|
toId(before?.item) !== toId(after.item) ||
|
||||||
|
toId(before?.itemSku) !== toId(after.itemSku)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stockAuditLevelLineSchema = new Schema(
|
||||||
|
{
|
||||||
|
itemType: {
|
||||||
|
type: String,
|
||||||
|
enum: ['filament', 'part', 'product'],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
allItems: { type: Boolean, default: false },
|
||||||
|
item: { type: Schema.Types.ObjectId, refPath: 'auditLines.itemType', required: false },
|
||||||
|
allSkus: { type: Boolean, default: false },
|
||||||
|
itemSku: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: function () {
|
||||||
|
return ['filament', 'part', 'product'].includes(this.itemType)
|
||||||
|
? this.itemType + 'Sku'
|
||||||
|
: null;
|
||||||
|
},
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ _id: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const stockAuditLevelSchema = new Schema(
|
||||||
|
{
|
||||||
|
_reference: { type: String, default: () => generateId()() },
|
||||||
|
name: { type: String, required: true },
|
||||||
|
tags: [{ type: String }],
|
||||||
|
auditLines: { type: [stockAuditLevelLineSchema], default: [] },
|
||||||
|
},
|
||||||
|
{ timestamps: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
stockAuditLevelSchema.index({ name: 'text', tags: 'text' });
|
||||||
|
|
||||||
|
stockAuditLevelSchema.statics.stats = async function () {
|
||||||
|
const count = await this.countDocuments();
|
||||||
|
return { total: { count } };
|
||||||
|
};
|
||||||
|
|
||||||
|
stockAuditLevelSchema.statics.history = async function () {
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
stockAuditLevelSchema.statics.recalculate = async function (stockAuditLevel, user) {
|
||||||
|
if (!stockAuditLevel?._id) return;
|
||||||
|
|
||||||
|
const auditLines = stockAuditLevel.auditLines || [];
|
||||||
|
const normalizedLines = auditLines.map((line) => normalizeAuditLevelLine(line));
|
||||||
|
const changed = auditLines.some((line, index) =>
|
||||||
|
auditLevelLineChanged(line, normalizedLines[index])
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!changed) return;
|
||||||
|
|
||||||
|
await editObject({
|
||||||
|
model: this,
|
||||||
|
id: stockAuditLevel._id,
|
||||||
|
updateData: { auditLines: normalizedLines },
|
||||||
|
user,
|
||||||
|
recalculate: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
stockAuditLevelSchema.virtual('id').get(function () {
|
||||||
|
return this._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
stockAuditLevelSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
|
export const stockAuditLevelModel = mongoose.model('stockAuditLevel', stockAuditLevelSchema);
|
||||||
@ -18,6 +18,7 @@ import { purchaseOrderModel } from './inventory/purchaseorder.schema.js';
|
|||||||
import { orderItemModel } from './inventory/orderitem.schema.js';
|
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 { stockAuditLevelModel } from './management/stockauditlevel.schema.js';
|
||||||
import { partStockModel } from './inventory/partstock.schema.js';
|
import { partStockModel } from './inventory/partstock.schema.js';
|
||||||
import { productStockModel } from './inventory/productstock.schema.js';
|
import { productStockModel } from './inventory/productstock.schema.js';
|
||||||
import { stockLocationModel } from './inventory/stocklocation.schema.js';
|
import { stockLocationModel } from './inventory/stocklocation.schema.js';
|
||||||
@ -88,6 +89,7 @@ export const models = {
|
|||||||
FLS: modelEntry(() => filamentStockModel, 'filamentStock', 'Filament Stock'),
|
FLS: modelEntry(() => filamentStockModel, 'filamentStock', 'Filament Stock'),
|
||||||
SEV: modelEntry(() => stockEventModel, 'stockEvent', 'Stock Event'),
|
SEV: modelEntry(() => stockEventModel, 'stockEvent', 'Stock Event'),
|
||||||
SAU: modelEntry(() => stockAuditModel, 'stockAudit', 'Stock Audit'),
|
SAU: modelEntry(() => stockAuditModel, 'stockAudit', 'Stock Audit'),
|
||||||
|
SAL: modelEntry(() => stockAuditLevelModel, 'stockAuditLevel', 'Stock Audit Level'),
|
||||||
PTS: modelEntry(() => partStockModel, 'partStock', 'Part Stock'),
|
PTS: modelEntry(() => partStockModel, 'partStock', 'Part Stock'),
|
||||||
PDS: modelEntry(() => productStockModel, 'productStock', 'Product Stock'),
|
PDS: modelEntry(() => productStockModel, 'productStock', 'Product Stock'),
|
||||||
SLN: modelEntry(() => stockLocationModel, 'stockLocation', 'Stock Location'),
|
SLN: modelEntry(() => stockLocationModel, 'stockLocation', 'Stock Location'),
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import {
|
|||||||
orderItemRoutes,
|
orderItemRoutes,
|
||||||
shipmentRoutes,
|
shipmentRoutes,
|
||||||
stockAuditRoutes,
|
stockAuditRoutes,
|
||||||
|
stockAuditLevelRoutes,
|
||||||
stockLocationRoutes,
|
stockLocationRoutes,
|
||||||
stockTransferRoutes,
|
stockTransferRoutes,
|
||||||
stockEventRoutes,
|
stockEventRoutes,
|
||||||
@ -204,6 +205,7 @@ app.use('/orderitems', orderItemRoutes);
|
|||||||
app.use('/shipments', shipmentRoutes);
|
app.use('/shipments', shipmentRoutes);
|
||||||
app.use('/stockevents', stockEventRoutes);
|
app.use('/stockevents', stockEventRoutes);
|
||||||
app.use('/stockaudits', stockAuditRoutes);
|
app.use('/stockaudits', stockAuditRoutes);
|
||||||
|
app.use('/stockauditlevels', stockAuditLevelRoutes);
|
||||||
app.use('/stocklocations', stockLocationRoutes);
|
app.use('/stocklocations', stockLocationRoutes);
|
||||||
app.use('/stocktransfers', stockTransferRoutes);
|
app.use('/stocktransfers', stockTransferRoutes);
|
||||||
app.use('/auditlogs', auditLogRoutes);
|
app.use('/auditlogs', auditLogRoutes);
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import orderItemRoutes from './inventory/orderitems.js';
|
|||||||
import shipmentRoutes from './inventory/shipments.js';
|
import shipmentRoutes from './inventory/shipments.js';
|
||||||
import stockEventRoutes from './inventory/stockevents.js';
|
import stockEventRoutes from './inventory/stockevents.js';
|
||||||
import stockAuditRoutes from './inventory/stockaudits.js';
|
import stockAuditRoutes from './inventory/stockaudits.js';
|
||||||
|
import stockAuditLevelRoutes from './management/stockauditlevels.js';
|
||||||
import stockLocationRoutes from './inventory/stocklocations.js';
|
import stockLocationRoutes from './inventory/stocklocations.js';
|
||||||
import stockTransferRoutes from './inventory/stocktransfers.js';
|
import stockTransferRoutes from './inventory/stocktransfers.js';
|
||||||
import auditLogRoutes from './management/auditlogs.js';
|
import auditLogRoutes from './management/auditlogs.js';
|
||||||
@ -95,6 +96,7 @@ export {
|
|||||||
shipmentRoutes,
|
shipmentRoutes,
|
||||||
stockEventRoutes,
|
stockEventRoutes,
|
||||||
stockAuditRoutes,
|
stockAuditRoutes,
|
||||||
|
stockAuditLevelRoutes,
|
||||||
stockLocationRoutes,
|
stockLocationRoutes,
|
||||||
stockTransferRoutes,
|
stockTransferRoutes,
|
||||||
auditLogRoutes,
|
auditLogRoutes,
|
||||||
|
|||||||
@ -1,9 +1,23 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { isAuthenticated } from '../../keycloak.js';
|
import { isAuthenticated } from '../../keycloak.js';
|
||||||
import { checkPermissions } from '../../database/permissions.js';
|
import { checkPermissions } from '../../database/permissions.js';
|
||||||
import { parseFilter, getFilter, getSort } from '../../utils.js';
|
import { getFilter, convertPropertiesString, getSort } from '../../utils.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
const listAllowedFilters = [
|
||||||
|
'state',
|
||||||
|
'state.type',
|
||||||
|
'auditLevel',
|
||||||
|
'stockLocation',
|
||||||
|
'postedAt',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'_reference',
|
||||||
|
];
|
||||||
|
const listAllowedSorters = ['createdAt', 'updatedAt', 'postedAt', 'state'];
|
||||||
|
const propertiesAllowedFilters = ['state.type'];
|
||||||
|
|
||||||
import {
|
import {
|
||||||
listStockAuditsRouteHandler,
|
listStockAuditsRouteHandler,
|
||||||
getStockAuditRouteHandler,
|
getStockAuditRouteHandler,
|
||||||
@ -13,54 +27,66 @@ import {
|
|||||||
getStockAuditStatsRouteHandler,
|
getStockAuditStatsRouteHandler,
|
||||||
getStockAuditHistoryRouteHandler,
|
getStockAuditHistoryRouteHandler,
|
||||||
searchStockAuditsRouteHandler,
|
searchStockAuditsRouteHandler,
|
||||||
|
listStockAuditsByPropertiesRouteHandler,
|
||||||
|
getStockAuditPropertyValuesRouteHandler,
|
||||||
getStockAuditNeighborsRouteHandler,
|
getStockAuditNeighborsRouteHandler,
|
||||||
|
postStockAuditRouteHandler,
|
||||||
} from '../../services/inventory/stockaudits.js';
|
} from '../../services/inventory/stockaudits.js';
|
||||||
|
|
||||||
const listAllowedFilters = [
|
|
||||||
'status',
|
|
||||||
'type',
|
|
||||||
'createdBy',
|
|
||||||
'state',
|
|
||||||
'createdAt',
|
|
||||||
'updatedAt',
|
|
||||||
'_reference',
|
|
||||||
];
|
|
||||||
const listAllowedSorters = ['createdAt', 'updatedAt', 'state'];
|
|
||||||
|
|
||||||
// List stock audits
|
|
||||||
router.get('/', isAuthenticated, checkPermissions('stockAudit', 'list'), async (req, res) => {
|
router.get('/', isAuthenticated, checkPermissions('stockAudit', 'list'), async (req, res) => {
|
||||||
const { page, limit, property } = req.query;
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
var filter = {};
|
listStockAuditsRouteHandler(
|
||||||
|
req,
|
||||||
for (const [key, value] of Object.entries(req.query)) {
|
res,
|
||||||
for (var i = 0; i < listAllowedFilters.length; i++) {
|
page,
|
||||||
if (key == listAllowedFilters[i]) {
|
limit,
|
||||||
const parsedFilter = await parseFilter(key, value);
|
property,
|
||||||
filter = { ...filter, ...parsedFilter };
|
filter,
|
||||||
}
|
search,
|
||||||
}
|
getSort(sortProperty, listAllowedSorters),
|
||||||
}
|
sortOrder
|
||||||
|
);
|
||||||
listStockAuditsRouteHandler(req, res, page, limit, property, filter);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create new stock audit
|
router.get(
|
||||||
router.post('/', isAuthenticated, checkPermissions('stockAudit', 'new'), async (req, res) => {
|
'/properties',
|
||||||
newStockAuditRouteHandler(req, res);
|
checkPermissions('stockAudit', 'list'),
|
||||||
|
isAuthenticated,
|
||||||
|
async (req, res) => {
|
||||||
|
let properties = convertPropertiesString(req.query.properties);
|
||||||
|
const filter = await getFilter(req.query, propertiesAllowedFilters, false);
|
||||||
|
var masterFilter = {};
|
||||||
|
if (req.query.masterFilter) {
|
||||||
|
masterFilter = JSON.parse(req.query.masterFilter);
|
||||||
|
}
|
||||||
|
listStockAuditsByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get('/values', checkPermissions('stockAudit', 'list'), isAuthenticated, async (req, res) => {
|
||||||
|
const { property } = req.query;
|
||||||
|
const filter = await getFilter(req.query, listAllowedFilters, true);
|
||||||
|
var masterFilter = {};
|
||||||
|
if (req.query.masterFilter) {
|
||||||
|
masterFilter = await getFilter(JSON.parse(req.query.masterFilter), listAllowedFilters, true);
|
||||||
|
}
|
||||||
|
getStockAuditPropertyValuesRouteHandler(req, res, property, filter, masterFilter);
|
||||||
});
|
});
|
||||||
|
|
||||||
// get stock audit stats
|
|
||||||
router.get('/search', checkPermissions('stockAudit', 'list'), isAuthenticated, async (req, res) => {
|
router.get('/search', checkPermissions('stockAudit', 'list'), isAuthenticated, async (req, res) => {
|
||||||
const { search } = req.query;
|
const { search } = req.query;
|
||||||
searchStockAuditsRouteHandler(req, res, search);
|
searchStockAuditsRouteHandler(req, res, search);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post('/', isAuthenticated, checkPermissions('stockAudit', 'new'), async (req, res) => {
|
||||||
|
newStockAuditRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
router.get('/stats', isAuthenticated, async (req, res) => {
|
router.get('/stats', isAuthenticated, async (req, res) => {
|
||||||
getStockAuditStatsRouteHandler(req, res);
|
getStockAuditStatsRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
// get stock audit history
|
|
||||||
router.get('/history', isAuthenticated, async (req, res) => {
|
router.get('/history', isAuthenticated, async (req, res) => {
|
||||||
getStockAuditHistoryRouteHandler(req, res);
|
getStockAuditHistoryRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
@ -68,22 +94,37 @@ router.get('/history', isAuthenticated, async (req, res) => {
|
|||||||
router.get('/neighbors', isAuthenticated, async (req, res) => {
|
router.get('/neighbors', isAuthenticated, async (req, res) => {
|
||||||
const { property, search, sortProperty, sortOrder, id } = req.query;
|
const { property, search, sortProperty, sortOrder, id } = req.query;
|
||||||
const filter = await getFilter(req.query, listAllowedFilters);
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
getStockAuditNeighborsRouteHandler(req, res, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder, id);
|
getStockAuditNeighborsRouteHandler(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
property,
|
||||||
|
filter,
|
||||||
|
search,
|
||||||
|
getSort(sortProperty, listAllowedSorters),
|
||||||
|
sortOrder,
|
||||||
|
id
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get specific stock audit
|
|
||||||
router.get('/:id', isAuthenticated, checkPermissions('stockAudit', 'info'), async (req, res) => {
|
router.get('/:id', isAuthenticated, checkPermissions('stockAudit', 'info'), async (req, res) => {
|
||||||
getStockAuditRouteHandler(req, res);
|
getStockAuditRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update stock audit
|
|
||||||
router.put('/:id', isAuthenticated, checkPermissions('stockAudit', 'edit'), async (req, res) => {
|
router.put('/:id', isAuthenticated, checkPermissions('stockAudit', 'edit'), async (req, res) => {
|
||||||
updateStockAuditRouteHandler(req, res);
|
updateStockAuditRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete stock audit
|
|
||||||
router.delete('/:id', isAuthenticated, async (req, res) => {
|
router.delete('/:id', isAuthenticated, async (req, res) => {
|
||||||
deleteStockAuditRouteHandler(req, res);
|
deleteStockAuditRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:id/post',
|
||||||
|
isAuthenticated,
|
||||||
|
checkPermissions('stockAudit', 'post'),
|
||||||
|
async (req, res) => {
|
||||||
|
postStockAuditRouteHandler(req, res);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
131
src/routes/management/stockauditlevels.js
Normal file
131
src/routes/management/stockauditlevels.js
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { isAuthenticated } from '../../keycloak.js';
|
||||||
|
import { checkPermissions } from '../../database/permissions.js';
|
||||||
|
import { getFilter, convertPropertiesString, getSort } from '../../utils.js';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const listAllowedFilters = ['name', 'createdAt', 'updatedAt', '_reference'];
|
||||||
|
const listAllowedSorters = ['name', 'createdAt', '_id', 'updatedAt'];
|
||||||
|
const propertiesAllowedFilters = ['tags'];
|
||||||
|
|
||||||
|
import {
|
||||||
|
listStockAuditLevelsRouteHandler,
|
||||||
|
getStockAuditLevelRouteHandler,
|
||||||
|
editStockAuditLevelRouteHandler,
|
||||||
|
newStockAuditLevelRouteHandler,
|
||||||
|
deleteStockAuditLevelRouteHandler,
|
||||||
|
listStockAuditLevelsByPropertiesRouteHandler,
|
||||||
|
getStockAuditLevelStatsRouteHandler,
|
||||||
|
getStockAuditLevelHistoryRouteHandler,
|
||||||
|
searchStockAuditLevelsRouteHandler,
|
||||||
|
getStockAuditLevelPropertyValuesRouteHandler,
|
||||||
|
getStockAuditLevelNeighborsRouteHandler,
|
||||||
|
} from '../../services/management/stockauditlevels.js';
|
||||||
|
|
||||||
|
router.get('/', isAuthenticated, checkPermissions('stockAuditLevel', 'list'), async (req, res) => {
|
||||||
|
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||||
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
|
listStockAuditLevelsRouteHandler(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
property,
|
||||||
|
filter,
|
||||||
|
search,
|
||||||
|
getSort(sortProperty, listAllowedSorters),
|
||||||
|
sortOrder
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/properties',
|
||||||
|
checkPermissions('stockAuditLevel', 'list'),
|
||||||
|
isAuthenticated,
|
||||||
|
async (req, res) => {
|
||||||
|
let properties = convertPropertiesString(req.query.properties);
|
||||||
|
const filter = await getFilter(req.query, propertiesAllowedFilters, false);
|
||||||
|
var masterFilter = {};
|
||||||
|
if (req.query.masterFilter) {
|
||||||
|
masterFilter = JSON.parse(req.query.masterFilter);
|
||||||
|
}
|
||||||
|
listStockAuditLevelsByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/values',
|
||||||
|
checkPermissions('stockAuditLevel', 'list'),
|
||||||
|
isAuthenticated,
|
||||||
|
async (req, res) => {
|
||||||
|
const { property } = req.query;
|
||||||
|
const filter = await getFilter(req.query, listAllowedFilters, true);
|
||||||
|
var masterFilter = {};
|
||||||
|
if (req.query.masterFilter) {
|
||||||
|
masterFilter = await getFilter(JSON.parse(req.query.masterFilter), listAllowedFilters, true);
|
||||||
|
}
|
||||||
|
getStockAuditLevelPropertyValuesRouteHandler(req, res, property, filter, masterFilter);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/search',
|
||||||
|
checkPermissions('stockAuditLevel', 'list'),
|
||||||
|
isAuthenticated,
|
||||||
|
async (req, res) => {
|
||||||
|
const { search } = req.query;
|
||||||
|
searchStockAuditLevelsRouteHandler(req, res, search);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post('/', isAuthenticated, checkPermissions('stockAuditLevel', 'new'), async (req, res) => {
|
||||||
|
newStockAuditLevelRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/stats', isAuthenticated, async (req, res) => {
|
||||||
|
getStockAuditLevelStatsRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/history', isAuthenticated, async (req, res) => {
|
||||||
|
getStockAuditLevelHistoryRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/neighbors', isAuthenticated, async (req, res) => {
|
||||||
|
const { property, search, sortProperty, sortOrder, id } = req.query;
|
||||||
|
const filter = await getFilter(req.query, listAllowedFilters);
|
||||||
|
getStockAuditLevelNeighborsRouteHandler(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
property,
|
||||||
|
filter,
|
||||||
|
search,
|
||||||
|
getSort(sortProperty, listAllowedSorters),
|
||||||
|
sortOrder,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:id',
|
||||||
|
isAuthenticated,
|
||||||
|
checkPermissions('stockAuditLevel', 'info'),
|
||||||
|
async (req, res) => {
|
||||||
|
getStockAuditLevelRouteHandler(req, res);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.put(
|
||||||
|
'/:id',
|
||||||
|
isAuthenticated,
|
||||||
|
checkPermissions('stockAuditLevel', 'edit'),
|
||||||
|
async (req, res) => {
|
||||||
|
editStockAuditLevelRouteHandler(req, res);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete('/:id', isAuthenticated, async (req, res) => {
|
||||||
|
deleteStockAuditLevelRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@ -1,24 +1,49 @@
|
|||||||
import { jest } from '@jest/globals';
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../utils.js', () => ({
|
|
||||||
getAuditLogs: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/database.js', () => ({
|
jest.unstable_mockModule('../../../database/database.js', () => ({
|
||||||
searchObjects: jest.fn(),
|
searchObjects: jest.fn(),
|
||||||
getPropertyValues: jest.fn(),
|
getPropertyValues: jest.fn(),
|
||||||
|
listObjects: jest.fn(),
|
||||||
|
getObject: jest.fn(),
|
||||||
|
editObject: jest.fn(),
|
||||||
|
newObject: jest.fn(),
|
||||||
|
deleteObject: jest.fn(),
|
||||||
|
listObjectsByProperties: jest.fn(),
|
||||||
getModelStats: jest.fn(),
|
getModelStats: jest.fn(),
|
||||||
getModelHistory: jest.fn(),
|
getModelHistory: jest.fn(),
|
||||||
getObjectNeighbors: jest.fn(),
|
getObjectNeighbors: jest.fn(),
|
||||||
|
checkStates: jest.fn(),
|
||||||
|
deleteObjectCache: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/schemas/inventory/stockaudit.schema.js', () => ({
|
jest.unstable_mockModule('../../../database/schemas/inventory/stockaudit.schema.js', () => ({
|
||||||
stockAuditModel: {
|
stockAuditModel: {
|
||||||
modelName: 'StockAudit',
|
modelName: 'StockAudit',
|
||||||
aggregate: jest.fn(),
|
findById: jest.fn(),
|
||||||
findOne: jest.fn(),
|
|
||||||
create: jest.fn(),
|
|
||||||
},
|
},
|
||||||
|
getCurrentQuantityAtLocation: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../../database/schemas/inventory/filamentstock.schema.js', () => ({
|
||||||
|
filamentStockModel: {
|
||||||
|
find: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../../database/schemas/inventory/partstock.schema.js', () => ({
|
||||||
|
partStockModel: {
|
||||||
|
find: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../../database/schemas/inventory/productstock.schema.js', () => ({
|
||||||
|
productStockModel: {
|
||||||
|
find: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../../database/schemas/inventory/stockevent.schema.js', () => ({
|
||||||
|
stockEventModel: { modelName: 'StockEvent' },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.unstable_mockModule('log4js', () => ({
|
jest.unstable_mockModule('log4js', () => ({
|
||||||
@ -37,10 +62,18 @@ const {
|
|||||||
listStockAuditsRouteHandler,
|
listStockAuditsRouteHandler,
|
||||||
getStockAuditRouteHandler,
|
getStockAuditRouteHandler,
|
||||||
newStockAuditRouteHandler,
|
newStockAuditRouteHandler,
|
||||||
|
postStockAuditRouteHandler,
|
||||||
} = await import('../stockaudits.js');
|
} = await import('../stockaudits.js');
|
||||||
|
|
||||||
const { getAuditLogs } = await import('../../../utils.js');
|
const { listObjects, getObject, newObject, editObject, checkStates } = await import(
|
||||||
const { stockAuditModel } = await import('../../../database/schemas/inventory/stockaudit.schema.js');
|
'../../../database/database.js'
|
||||||
|
);
|
||||||
|
const { stockAuditModel, getCurrentQuantityAtLocation } = await import(
|
||||||
|
'../../../database/schemas/inventory/stockaudit.schema.js'
|
||||||
|
);
|
||||||
|
const { partStockModel } = await import(
|
||||||
|
'../../../database/schemas/inventory/partstock.schema.js'
|
||||||
|
);
|
||||||
|
|
||||||
describe('Stock Audit Service Route Handlers', () => {
|
describe('Stock Audit Service Route Handlers', () => {
|
||||||
let req, res;
|
let req, res;
|
||||||
@ -61,34 +94,173 @@ describe('Stock Audit Service Route Handlers', () => {
|
|||||||
|
|
||||||
describe('listStockAuditsRouteHandler', () => {
|
describe('listStockAuditsRouteHandler', () => {
|
||||||
it('should list stock audits', async () => {
|
it('should list stock audits', async () => {
|
||||||
const mockResult = [{ _id: '1', type: 'full' }];
|
const mockResult = [{ _id: '1', state: { type: 'draft' } }];
|
||||||
stockAuditModel.aggregate.mockResolvedValue(mockResult);
|
listObjects.mockResolvedValue(mockResult);
|
||||||
|
|
||||||
await listStockAuditsRouteHandler(req, res);
|
await listStockAuditsRouteHandler(req, res);
|
||||||
|
|
||||||
expect(stockAuditModel.aggregate).toHaveBeenCalled();
|
expect(listObjects).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ model: stockAuditModel })
|
||||||
|
);
|
||||||
expect(res.send).toHaveBeenCalledWith(mockResult);
|
expect(res.send).toHaveBeenCalledWith(mockResult);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getStockAuditRouteHandler', () => {
|
describe('getStockAuditRouteHandler', () => {
|
||||||
it('should get a stock audit by ID with audit logs', async () => {
|
it('should get a stock audit by ID', async () => {
|
||||||
req.params.id = '507f1f77bcf86cd799439011';
|
req.params.id = '507f1f77bcf86cd799439011';
|
||||||
const mockAudit = { _id: '507f1f77bcf86cd799439011', type: 'full', _doc: {} };
|
const mockAudit = { _id: '507f1f77bcf86cd799439011', state: { type: 'draft' } };
|
||||||
stockAuditModel.findOne.mockReturnValue({
|
getObject.mockResolvedValue(mockAudit);
|
||||||
populate: jest.fn().mockReturnValue({
|
|
||||||
populate: jest.fn().mockReturnValue({
|
|
||||||
populate: jest.fn().mockResolvedValue(mockAudit),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
getAuditLogs.mockResolvedValue([]);
|
|
||||||
|
|
||||||
await getStockAuditRouteHandler(req, res);
|
await getStockAuditRouteHandler(req, res);
|
||||||
|
|
||||||
expect(getAuditLogs).toHaveBeenCalled();
|
expect(getObject).toHaveBeenCalledWith(
|
||||||
expect(res.send).toHaveBeenCalled();
|
expect.objectContaining({ model: stockAuditModel, id: req.params.id })
|
||||||
});
|
);
|
||||||
|
expect(res.send).toHaveBeenCalledWith(mockAudit);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('newStockAuditRouteHandler', () => {
|
||||||
|
it('should create a stock audit with draft state', async () => {
|
||||||
|
req.body = {
|
||||||
|
auditLevel: 'level-1',
|
||||||
|
stockLocation: 'loc-1',
|
||||||
|
};
|
||||||
|
const mockResult = { _id: 'audit-1', state: { type: 'draft' } };
|
||||||
|
newObject.mockResolvedValue(mockResult);
|
||||||
|
|
||||||
|
await newStockAuditRouteHandler(req, res);
|
||||||
|
|
||||||
|
expect(newObject).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
model: stockAuditModel,
|
||||||
|
newData: expect.objectContaining({
|
||||||
|
state: { type: 'draft' },
|
||||||
|
auditLevel: 'level-1',
|
||||||
|
stockLocation: 'loc-1',
|
||||||
|
auditLines: [],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(res.send).toHaveBeenCalledWith(mockResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('postStockAuditRouteHandler', () => {
|
||||||
|
const auditId = '507f1f77bcf86cd799439011';
|
||||||
|
const stockLocationId = '507f1f77bcf86cd799439012';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
req.params.id = auditId;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject when stock audit is not in draft state', async () => {
|
||||||
|
checkStates.mockResolvedValue(false);
|
||||||
|
|
||||||
|
await postStockAuditRouteHandler(req, res);
|
||||||
|
|
||||||
|
expect(res.status).toHaveBeenCalledWith(400);
|
||||||
|
expect(res.send).toHaveBeenCalledWith({
|
||||||
|
error: 'Stock audit is not in draft state.',
|
||||||
|
code: 400,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject when live current differs from line current', async () => {
|
||||||
|
checkStates.mockResolvedValue(true);
|
||||||
|
stockAuditModel.findById.mockResolvedValue({
|
||||||
|
_id: auditId,
|
||||||
|
stockLocation: stockLocationId,
|
||||||
|
auditLines: [
|
||||||
|
{
|
||||||
|
toObject: () => ({
|
||||||
|
itemType: 'part',
|
||||||
|
item: 'part-1',
|
||||||
|
itemSku: 'sku-1',
|
||||||
|
current: 10,
|
||||||
|
actual: 8,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
getCurrentQuantityAtLocation.mockResolvedValue(12);
|
||||||
|
|
||||||
|
await postStockAuditRouteHandler(req, res);
|
||||||
|
|
||||||
|
expect(res.status).toHaveBeenCalledWith(400);
|
||||||
|
expect(res.send).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
error: expect.stringContaining('Current quantity for SKU has changed'),
|
||||||
|
code: 400,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create stock events and mark audit complete on success', async () => {
|
||||||
|
checkStates.mockResolvedValue(true);
|
||||||
|
stockAuditModel.findById.mockResolvedValue({
|
||||||
|
_id: auditId,
|
||||||
|
stockLocation: stockLocationId,
|
||||||
|
auditLines: [
|
||||||
|
{
|
||||||
|
toObject: () => ({
|
||||||
|
itemType: 'part',
|
||||||
|
item: 'part-1',
|
||||||
|
itemSku: 'sku-1',
|
||||||
|
current: 10,
|
||||||
|
actual: 8,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
getCurrentQuantityAtLocation.mockResolvedValue(10);
|
||||||
|
|
||||||
|
const mockStock = {
|
||||||
|
_id: 'stock-1',
|
||||||
|
currentQuantity: 10,
|
||||||
|
};
|
||||||
|
partStockModel.find.mockReturnValue({
|
||||||
|
sort: jest.fn().mockResolvedValue([mockStock]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const completeAudit = {
|
||||||
|
_id: auditId,
|
||||||
|
state: { type: 'complete' },
|
||||||
|
};
|
||||||
|
editObject.mockResolvedValue(completeAudit);
|
||||||
|
getObject.mockResolvedValue(completeAudit);
|
||||||
|
newObject.mockResolvedValue({ _id: 'event-1' });
|
||||||
|
|
||||||
|
await postStockAuditRouteHandler(req, res);
|
||||||
|
|
||||||
|
expect(editObject).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
model: stockAuditModel,
|
||||||
|
id: expect.anything(),
|
||||||
|
updateData: {
|
||||||
|
state: { type: 'complete' },
|
||||||
|
postedAt: expect.any(Date),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(newObject).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
model: expect.anything(),
|
||||||
|
newData: expect.objectContaining({
|
||||||
|
value: -2,
|
||||||
|
unit: 'each',
|
||||||
|
parent: 'stock-1',
|
||||||
|
parentType: 'partStock',
|
||||||
|
owner: auditId,
|
||||||
|
ownerType: 'stockAudit',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(editObject.mock.invocationCallOrder[0]).toBeLessThan(
|
||||||
|
newObject.mock.invocationCallOrder[0]
|
||||||
|
);
|
||||||
|
expect(res.send).toHaveBeenCalledWith(completeAudit);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -1,183 +1,480 @@
|
|||||||
import config from '../../config.js';
|
import config from '../../config.js';
|
||||||
import { stockAuditModel } from '../../database/schemas/inventory/stockaudit.schema.js';
|
import {
|
||||||
|
stockAuditModel,
|
||||||
|
getCurrentQuantityAtLocation,
|
||||||
|
} from '../../database/schemas/inventory/stockaudit.schema.js';
|
||||||
|
import { filamentStockModel } from '../../database/schemas/inventory/filamentstock.schema.js';
|
||||||
|
import { partStockModel } from '../../database/schemas/inventory/partstock.schema.js';
|
||||||
|
import { productStockModel } from '../../database/schemas/inventory/productstock.schema.js';
|
||||||
|
import { stockEventModel } from '../../database/schemas/inventory/stockevent.schema.js';
|
||||||
import log4js from 'log4js';
|
import log4js from 'log4js';
|
||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { getAuditLogs } from '../../utils.js';
|
|
||||||
import {
|
import {
|
||||||
getModelStats, getModelHistory,
|
deleteObject,
|
||||||
|
listObjects,
|
||||||
|
getObject,
|
||||||
|
editObject,
|
||||||
|
newObject,
|
||||||
|
listObjectsByProperties,
|
||||||
|
getModelStats,
|
||||||
|
getModelHistory,
|
||||||
|
checkStates,
|
||||||
searchObjects,
|
searchObjects,
|
||||||
|
getPropertyValues,
|
||||||
getObjectNeighbors,
|
getObjectNeighbors,
|
||||||
} from '../../database/database.js';
|
} from '../../database/database.js';
|
||||||
|
|
||||||
const logger = log4js.getLogger('Stock Audits');
|
const logger = log4js.getLogger('Stock Audits');
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
|
const STOCK_AUDIT_POPULATE = [
|
||||||
|
{ path: 'auditLevel' },
|
||||||
|
{ path: 'stockLocation' },
|
||||||
|
{ path: 'auditLines.item' },
|
||||||
|
{ path: 'auditLines.itemSku' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const normalizeAuditLineInput = (line) => {
|
||||||
|
const current = Number(line.current ?? line.currentQuantity) || 0;
|
||||||
|
const actual = Number(line.actual ?? line.actualQuantity) || 0;
|
||||||
|
return {
|
||||||
|
itemType: line.itemType,
|
||||||
|
item: line.item?._id ?? line.item,
|
||||||
|
itemSku: line.itemSku?._id ?? line.itemSku,
|
||||||
|
current,
|
||||||
|
actual,
|
||||||
|
new: actual,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const toId = (value) => {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (typeof value === 'object' && value._id != null) return value._id;
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stockConfigByItemType = {
|
||||||
|
filament: {
|
||||||
|
stockModel: filamentStockModel,
|
||||||
|
parentType: 'filamentStock',
|
||||||
|
skuField: 'filamentSku',
|
||||||
|
itemField: 'filament',
|
||||||
|
unit: 'g',
|
||||||
|
getAvailable: (stock) => Number(stock.currentWeight?.net) || 0,
|
||||||
|
},
|
||||||
|
part: {
|
||||||
|
stockModel: partStockModel,
|
||||||
|
parentType: 'partStock',
|
||||||
|
skuField: 'partSku',
|
||||||
|
itemField: 'part',
|
||||||
|
unit: 'each',
|
||||||
|
getAvailable: (stock) => Number(stock.currentQuantity) || 0,
|
||||||
|
},
|
||||||
|
product: {
|
||||||
|
stockModel: productStockModel,
|
||||||
|
parentType: 'productStock',
|
||||||
|
skuField: 'productSku',
|
||||||
|
itemField: 'product',
|
||||||
|
unit: 'each',
|
||||||
|
getAvailable: (stock) => Number(stock.currentQuantity) || 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function createStockEvent(newData, user) {
|
||||||
|
const result = await newObject({
|
||||||
|
model: stockEventModel,
|
||||||
|
newData,
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createStock(model, newData, user) {
|
||||||
|
const result = await newObject({
|
||||||
|
model,
|
||||||
|
newData,
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyPositiveVariance(auditId, line, stockLocationId, variance, config, user) {
|
||||||
|
const itemId = toId(line.item);
|
||||||
|
const itemSkuId = toId(line.itemSku);
|
||||||
|
const ts = new Date();
|
||||||
|
|
||||||
|
if (line.itemType === 'filament') {
|
||||||
|
const weight = { net: variance, gross: variance };
|
||||||
|
const stock = await createStock(
|
||||||
|
config.stockModel,
|
||||||
|
{
|
||||||
|
state: { type: 'unconsumed' },
|
||||||
|
startingWeight: weight,
|
||||||
|
currentWeight: weight,
|
||||||
|
filament: itemId,
|
||||||
|
filamentSku: itemSkuId,
|
||||||
|
stockLocation: stockLocationId,
|
||||||
|
},
|
||||||
|
user
|
||||||
|
);
|
||||||
|
|
||||||
|
await createStockEvent(
|
||||||
|
{
|
||||||
|
value: variance,
|
||||||
|
unit: config.unit,
|
||||||
|
parent: stock._id,
|
||||||
|
parentType: config.parentType,
|
||||||
|
owner: auditId,
|
||||||
|
ownerType: 'stockAudit',
|
||||||
|
timestamp: ts,
|
||||||
|
},
|
||||||
|
user
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.itemType === 'part') {
|
||||||
|
const stock = await createStock(
|
||||||
|
config.stockModel,
|
||||||
|
{
|
||||||
|
part: itemId,
|
||||||
|
partSku: itemSkuId,
|
||||||
|
currentQuantity: variance,
|
||||||
|
state: { type: 'new' },
|
||||||
|
postedAt: ts,
|
||||||
|
stockLocation: stockLocationId,
|
||||||
|
},
|
||||||
|
user
|
||||||
|
);
|
||||||
|
|
||||||
|
await createStockEvent(
|
||||||
|
{
|
||||||
|
value: variance,
|
||||||
|
unit: config.unit,
|
||||||
|
parent: stock._id,
|
||||||
|
parentType: config.parentType,
|
||||||
|
owner: auditId,
|
||||||
|
ownerType: 'stockAudit',
|
||||||
|
timestamp: ts,
|
||||||
|
},
|
||||||
|
user
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.itemType === 'product') {
|
||||||
|
const stock = await createStock(
|
||||||
|
config.stockModel,
|
||||||
|
{
|
||||||
|
product: itemId,
|
||||||
|
productSku: itemSkuId,
|
||||||
|
currentQuantity: variance,
|
||||||
|
state: { type: 'new' },
|
||||||
|
postedAt: ts,
|
||||||
|
partStockList: [],
|
||||||
|
stockLocation: stockLocationId,
|
||||||
|
},
|
||||||
|
user
|
||||||
|
);
|
||||||
|
|
||||||
|
await createStockEvent(
|
||||||
|
{
|
||||||
|
value: variance,
|
||||||
|
unit: config.unit,
|
||||||
|
parent: stock._id,
|
||||||
|
parentType: config.parentType,
|
||||||
|
owner: auditId,
|
||||||
|
ownerType: 'stockAudit',
|
||||||
|
timestamp: ts,
|
||||||
|
},
|
||||||
|
user
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyNegativeVariance(auditId, line, stockLocationId, variance, config, user) {
|
||||||
|
const itemSkuId = toId(line.itemSku);
|
||||||
|
const amountToRemove = Math.abs(variance);
|
||||||
|
const ts = new Date();
|
||||||
|
|
||||||
|
const stocks = await config.stockModel
|
||||||
|
.find({
|
||||||
|
[config.skuField]: itemSkuId,
|
||||||
|
stockLocation: stockLocationId,
|
||||||
|
'state.type': { $ne: 'draft' },
|
||||||
|
})
|
||||||
|
.sort({ createdAt: 1 });
|
||||||
|
|
||||||
|
let remaining = amountToRemove;
|
||||||
|
for (const stock of stocks) {
|
||||||
|
if (remaining <= 0) break;
|
||||||
|
const available = config.getAvailable(stock);
|
||||||
|
if (available <= 0) continue;
|
||||||
|
|
||||||
|
const deduction = Math.min(remaining, available);
|
||||||
|
await createStockEvent(
|
||||||
|
{
|
||||||
|
value: -deduction,
|
||||||
|
unit: config.unit,
|
||||||
|
parent: stock._id,
|
||||||
|
parentType: config.parentType,
|
||||||
|
owner: auditId,
|
||||||
|
ownerType: 'stockAudit',
|
||||||
|
timestamp: ts,
|
||||||
|
},
|
||||||
|
user
|
||||||
|
);
|
||||||
|
remaining -= deduction;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Insufficient stock to apply audit variance of ${variance} for SKU ${itemSkuId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executePostedAuditLine(auditId, line, stockLocationId, user) {
|
||||||
|
const current = Number(line.current) || 0;
|
||||||
|
const actual = Number(line.actual) || 0;
|
||||||
|
const variance = actual - current;
|
||||||
|
if (variance === 0) return;
|
||||||
|
|
||||||
|
const itemSkuId = toId(line.itemSku);
|
||||||
|
const liveCurrent = await getCurrentQuantityAtLocation(
|
||||||
|
line.itemType,
|
||||||
|
itemSkuId,
|
||||||
|
stockLocationId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (liveCurrent !== current) {
|
||||||
|
throw new Error(
|
||||||
|
`Current quantity for SKU has changed (expected ${current}, found ${liveCurrent}). Recalculate the audit before posting.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = stockConfigByItemType[line.itemType];
|
||||||
|
if (!config) {
|
||||||
|
throw new Error(`Unsupported item type: ${line.itemType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variance > 0) {
|
||||||
|
await applyPositiveVariance(auditId, line, stockLocationId, variance, config, user);
|
||||||
|
} else {
|
||||||
|
await applyNegativeVariance(auditId, line, stockLocationId, variance, config, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const listStockAuditsRouteHandler = async (
|
export const listStockAuditsRouteHandler = async (
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
page = 1,
|
page = 1,
|
||||||
limit = 25,
|
limit = 25,
|
||||||
property = '',
|
property = '',
|
||||||
filter = {}
|
filter = {},
|
||||||
|
search = '',
|
||||||
|
sort = '',
|
||||||
|
order = 'ascend'
|
||||||
) => {
|
) => {
|
||||||
try {
|
const result = await listObjects({
|
||||||
const skip = (page - 1) * limit;
|
model: stockAuditModel,
|
||||||
let stockAudits;
|
page,
|
||||||
let aggregateCommand = [];
|
limit,
|
||||||
|
property,
|
||||||
// Lookup createdBy user
|
filter,
|
||||||
aggregateCommand.push({
|
search,
|
||||||
$lookup: {
|
sort,
|
||||||
from: 'users',
|
order,
|
||||||
localField: 'createdBy',
|
populate: STOCK_AUDIT_POPULATE,
|
||||||
foreignField: '_id',
|
|
||||||
as: 'createdBy',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
aggregateCommand.push({ $unwind: '$createdBy' });
|
if (result?.error) {
|
||||||
|
logger.error('Error listing stock audits.');
|
||||||
if (filter != {}) {
|
res.status(result.code).send(result);
|
||||||
aggregateCommand.push({ $match: filter });
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (property != '') {
|
logger.debug(`List of stock audits (Page ${page}, Limit ${limit}). Count: ${result.length}`);
|
||||||
aggregateCommand.push({ $group: { _id: `$${property}` } });
|
res.send(result);
|
||||||
aggregateCommand.push({ $project: { _id: 0, [property]: '$_id' } });
|
};
|
||||||
|
|
||||||
|
export const listStockAuditsByPropertiesRouteHandler = async (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
properties = '',
|
||||||
|
filter = {},
|
||||||
|
masterFilter = {}
|
||||||
|
) => {
|
||||||
|
const result = await listObjectsByProperties({
|
||||||
|
model: stockAuditModel,
|
||||||
|
properties,
|
||||||
|
filter,
|
||||||
|
masterFilter,
|
||||||
|
populate: STOCK_AUDIT_POPULATE,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error listing stock audits.');
|
||||||
|
res.status(result.code).send(result);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
aggregateCommand.push({ $skip: skip });
|
logger.debug(`List of stock audits. Count: ${result.length}`);
|
||||||
aggregateCommand.push({ $limit: Number(limit) });
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
stockAudits = await stockAuditModel.aggregate(aggregateCommand);
|
export const getStockAuditPropertyValuesRouteHandler = async (
|
||||||
|
req,
|
||||||
logger.trace(
|
res,
|
||||||
`List of stock audits (Page ${page}, Limit ${limit}, Property ${property}):`,
|
property,
|
||||||
stockAudits
|
filter,
|
||||||
);
|
masterFilter
|
||||||
res.send(stockAudits);
|
) => {
|
||||||
} catch (error) {
|
const result = await getPropertyValues({
|
||||||
logger.error('Error listing stock audits:', error);
|
model: stockAuditModel,
|
||||||
res.status(500).send({ error: error });
|
property,
|
||||||
}
|
filter: { ...filter, ...masterFilter },
|
||||||
|
});
|
||||||
|
res.send(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const searchStockAuditsRouteHandler = async (req, res, search) => {
|
export const searchStockAuditsRouteHandler = async (req, res, search) => {
|
||||||
const result = await searchObjects({
|
const result = await searchObjects({
|
||||||
model: stockAuditModel,
|
model: stockAuditModel,
|
||||||
search,
|
search,
|
||||||
|
populate: STOCK_AUDIT_POPULATE,
|
||||||
});
|
});
|
||||||
res.send(result);
|
res.send(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStockAuditRouteHandler = async (req, res) => {
|
export const getStockAuditRouteHandler = async (req, res) => {
|
||||||
try {
|
const id = req.params.id;
|
||||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
const result = await getObject({
|
||||||
const stockAudit = await stockAuditModel
|
model: stockAuditModel,
|
||||||
.findOne({
|
id,
|
||||||
_id: id,
|
populate: STOCK_AUDIT_POPULATE,
|
||||||
})
|
});
|
||||||
.populate('createdBy')
|
if (result?.error) {
|
||||||
.populate('items.filamentStock')
|
|
||||||
.populate('items.partStock');
|
|
||||||
|
|
||||||
if (!stockAudit) {
|
|
||||||
logger.warn(`Stock audit not found with supplied id.`);
|
logger.warn(`Stock audit not found with supplied id.`);
|
||||||
return res.status(404).send({ error: 'Stock audit not found.' });
|
return res.status(result.code).send(result);
|
||||||
}
|
|
||||||
|
|
||||||
logger.trace(`Stock audit with ID: ${id}:`, stockAudit);
|
|
||||||
|
|
||||||
const auditLogs = await getAuditLogs(id);
|
|
||||||
|
|
||||||
res.send({ ...stockAudit._doc, auditLogs: auditLogs });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error fetching stock audit:', error);
|
|
||||||
res.status(500).send({ error: error.message });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const newStockAuditRouteHandler = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const newStockAudit = {
|
|
||||||
type: req.body.type,
|
|
||||||
status: req.body.status || 'pending',
|
|
||||||
notes: req.body.notes,
|
|
||||||
items: req.body.items.map((item) => ({
|
|
||||||
type: item.type,
|
|
||||||
stock:
|
|
||||||
item.type === 'filament'
|
|
||||||
? new mongoose.Types.ObjectId(item.filamentStock)
|
|
||||||
: new mongoose.Types.ObjectId(item.partStock),
|
|
||||||
expectedQuantity: item.expectedQuantity,
|
|
||||||
actualQuantity: item.actualQuantity,
|
|
||||||
notes: item.notes,
|
|
||||||
})),
|
|
||||||
createdBy: new mongoose.Types.ObjectId(req.body.createdBy),
|
|
||||||
completedAt: req.body.status === 'completed' ? new Date() : null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await stockAuditModel.create(newStockAudit);
|
|
||||||
if (!result) {
|
|
||||||
logger.error('No stock audit created.');
|
|
||||||
return res.status(500).send({ error: 'No stock audit created.' });
|
|
||||||
}
|
|
||||||
return res.send({ status: 'ok', id: result._id });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error adding stock audit:', error);
|
|
||||||
return res.status(500).send({ error: error.message });
|
|
||||||
}
|
}
|
||||||
|
logger.debug(`Retrieved stock audit with ID: ${id}`);
|
||||||
|
res.send(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateStockAuditRouteHandler = async (req, res) => {
|
export const updateStockAuditRouteHandler = async (req, res) => {
|
||||||
try {
|
|
||||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
const id = new mongoose.Types.ObjectId(req.params.id);
|
||||||
const updateData = {
|
|
||||||
...req.body,
|
const checkStatesResult = await checkStates({ model: stockAuditModel, id, states: ['draft'] });
|
||||||
items: req.body.items?.map((item) => ({
|
|
||||||
type: item.type,
|
if (checkStatesResult?.error) {
|
||||||
stock:
|
logger.error('Error checking stock audit state:', checkStatesResult.error);
|
||||||
item.type === 'filament'
|
res.status(checkStatesResult.code).send(checkStatesResult);
|
||||||
? new mongoose.Types.ObjectId(item.filamentStock)
|
return;
|
||||||
: new mongoose.Types.ObjectId(item.partStock),
|
}
|
||||||
expectedQuantity: item.expectedQuantity,
|
|
||||||
actualQuantity: item.actualQuantity,
|
if (checkStatesResult === false) {
|
||||||
notes: item.notes,
|
logger.error('Stock audit is not in draft state.');
|
||||||
})),
|
res.status(400).send({ error: 'Stock audit is not in draft state.', code: 400 });
|
||||||
completedAt: req.body.status === 'completed' ? new Date() : null,
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData = {};
|
||||||
|
|
||||||
|
if (req.body.state !== undefined) {
|
||||||
|
updateData.state = req.body.state;
|
||||||
|
}
|
||||||
|
if (req.body.auditLevel !== undefined) {
|
||||||
|
updateData.auditLevel = req.body.auditLevel?._id ?? req.body.auditLevel;
|
||||||
|
}
|
||||||
|
if (req.body.stockLocation !== undefined) {
|
||||||
|
updateData.stockLocation = req.body.stockLocation?._id ?? req.body.stockLocation;
|
||||||
|
}
|
||||||
|
if (req.body.auditLines !== undefined) {
|
||||||
|
updateData.auditLines = (req.body.auditLines || []).map((line) => normalizeAuditLineInput(line));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await editObject({
|
||||||
|
model: stockAuditModel,
|
||||||
|
id,
|
||||||
|
updateData,
|
||||||
|
user: req.user,
|
||||||
|
populate: STOCK_AUDIT_POPULATE,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
logger.error('Error updating stock audit:', result.error);
|
||||||
|
res.status(result.code).send(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Updated stock audit with ID: ${id}`);
|
||||||
|
res.send(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await stockAuditModel.findByIdAndUpdate(id, { $set: updateData }, { new: true });
|
export const newStockAuditRouteHandler = async (req, res) => {
|
||||||
|
const newData = {
|
||||||
|
state: req.body.state ?? { type: 'draft' },
|
||||||
|
auditLevel: req.body.auditLevel?._id ?? req.body.auditLevel,
|
||||||
|
stockLocation: req.body.stockLocation?._id ?? req.body.stockLocation,
|
||||||
|
auditLines: (req.body.auditLines || []).map((line) => normalizeAuditLineInput(line)),
|
||||||
|
};
|
||||||
|
|
||||||
if (!result) {
|
const result = await newObject({
|
||||||
logger.warn(`Stock audit not found with supplied id.`);
|
model: stockAuditModel,
|
||||||
return res.status(404).send({ error: 'Stock audit not found.' });
|
newData,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
logger.error('No stock audit created:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.trace(`Updated stock audit with ID: ${id}:`, result);
|
logger.debug(`New stock audit with ID: ${result._id}`);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error updating stock audit:', error);
|
|
||||||
res.status(500).send({ error: error.message });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteStockAuditRouteHandler = async (req, res) => {
|
export const deleteStockAuditRouteHandler = async (req, res) => {
|
||||||
try {
|
|
||||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
const id = new mongoose.Types.ObjectId(req.params.id);
|
||||||
const result = await stockAuditModel.findByIdAndDelete(id);
|
|
||||||
|
|
||||||
if (!result) {
|
const checkStatesResult = await checkStates({ model: stockAuditModel, id, states: ['draft'] });
|
||||||
logger.warn(`Stock audit not found with supplied id.`);
|
|
||||||
return res.status(404).send({ error: 'Stock audit not found.' });
|
if (checkStatesResult?.error) {
|
||||||
|
logger.error('Error checking stock audit state:', checkStatesResult.error);
|
||||||
|
res.status(checkStatesResult.code).send(checkStatesResult);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.trace(`Deleted stock audit with ID: ${id}`);
|
if (checkStatesResult === false) {
|
||||||
res.send({ status: 'ok' });
|
logger.error('Stock audit is not in draft state.');
|
||||||
} catch (error) {
|
res.status(400).send({ error: 'Stock audit is not in draft state.', code: 400 });
|
||||||
logger.error('Error deleting stock audit:', error);
|
return;
|
||||||
res.status(500).send({ error: error.message });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const result = await deleteObject({
|
||||||
|
model: stockAuditModel,
|
||||||
|
id,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
logger.error('No stock audit deleted:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Deleted stock audit with ID: ${result._id}`);
|
||||||
|
res.send(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStockAuditStatsRouteHandler = async (req, res) => {
|
export const getStockAuditStatsRouteHandler = async (req, res) => {
|
||||||
@ -233,3 +530,68 @@ export const getStockAuditNeighborsRouteHandler = async (
|
|||||||
logger.debug(`Retrieved stock audit neighbors for ID: ${id}`);
|
logger.debug(`Retrieved stock audit neighbors for ID: ${id}`);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const postStockAuditRouteHandler = async (req, res) => {
|
||||||
|
const id = new mongoose.Types.ObjectId(req.params.id);
|
||||||
|
|
||||||
|
const checkStatesResult = await checkStates({ model: stockAuditModel, id, states: ['draft'] });
|
||||||
|
|
||||||
|
if (checkStatesResult?.error) {
|
||||||
|
logger.error('Error checking stock audit state:', checkStatesResult.error);
|
||||||
|
res.status(checkStatesResult.code).send(checkStatesResult);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkStatesResult === false) {
|
||||||
|
logger.error('Stock audit is not in draft state.');
|
||||||
|
res.status(400).send({ error: 'Stock audit is not in draft state.', code: 400 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = await stockAuditModel.findById(id);
|
||||||
|
if (!doc) {
|
||||||
|
return res.status(404).send({ error: 'Stock audit not found.', code: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc.auditLines?.length) {
|
||||||
|
return res.status(400).send({ error: 'Stock audit has no audit lines.', code: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const stockLocationId = doc.stockLocation;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const completeResult = await editObject({
|
||||||
|
model: stockAuditModel,
|
||||||
|
id,
|
||||||
|
updateData: {
|
||||||
|
state: { type: 'complete' },
|
||||||
|
postedAt: new Date(),
|
||||||
|
},
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (completeResult?.error) {
|
||||||
|
throw new Error(completeResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const line of doc.auditLines) {
|
||||||
|
await executePostedAuditLine(doc._id, line.toObject(), stockLocationId, req.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
const complete = await getObject({
|
||||||
|
model: stockAuditModel,
|
||||||
|
id,
|
||||||
|
populate: STOCK_AUDIT_POPULATE,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (complete?.error) {
|
||||||
|
throw new Error(complete.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Posted stock audit with ID: ${id}`);
|
||||||
|
res.send(complete);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Error posting stock audit:', err);
|
||||||
|
res.status(400).send({ error: err.message || 'Failed to post stock audit', code: 400 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
130
src/services/management/__tests__/stockauditlevels.test.js
Normal file
130
src/services/management/__tests__/stockauditlevels.test.js
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../../database/database.js', () => ({
|
||||||
|
searchObjects: jest.fn(),
|
||||||
|
getPropertyValues: jest.fn(),
|
||||||
|
listObjects: jest.fn(),
|
||||||
|
getObject: jest.fn(),
|
||||||
|
editObject: jest.fn(),
|
||||||
|
newObject: jest.fn(),
|
||||||
|
deleteObject: jest.fn(),
|
||||||
|
listObjectsByProperties: jest.fn(),
|
||||||
|
getModelStats: jest.fn(),
|
||||||
|
getModelHistory: jest.fn(),
|
||||||
|
getObjectNeighbors: jest.fn(),
|
||||||
|
deleteObjectCache: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('../../../database/schemas/management/stockauditlevel.schema.js', () => ({
|
||||||
|
stockAuditLevelModel: { modelName: 'StockAuditLevel' },
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule('log4js', () => ({
|
||||||
|
default: {
|
||||||
|
getLogger: () => ({
|
||||||
|
level: 'info',
|
||||||
|
debug: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
trace: jest.fn(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const {
|
||||||
|
listStockAuditLevelsRouteHandler,
|
||||||
|
getStockAuditLevelRouteHandler,
|
||||||
|
newStockAuditLevelRouteHandler,
|
||||||
|
editStockAuditLevelRouteHandler,
|
||||||
|
} = await import('../stockauditlevels.js');
|
||||||
|
|
||||||
|
const { listObjects, getObject, editObject, newObject } = await import(
|
||||||
|
'../../../database/database.js'
|
||||||
|
);
|
||||||
|
const { stockAuditLevelModel } = await import(
|
||||||
|
'../../../database/schemas/management/stockauditlevel.schema.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('Stock Audit Level Service Route Handlers', () => {
|
||||||
|
let req, res;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
req = {
|
||||||
|
params: {},
|
||||||
|
query: {},
|
||||||
|
body: {},
|
||||||
|
user: { id: 'test-user-id' },
|
||||||
|
};
|
||||||
|
res = {
|
||||||
|
send: jest.fn(),
|
||||||
|
status: jest.fn().mockReturnThis(),
|
||||||
|
};
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listStockAuditLevelsRouteHandler', () => {
|
||||||
|
it('should list stock audit levels', async () => {
|
||||||
|
const mockResult = [{ _id: '1', name: 'Full audit' }];
|
||||||
|
listObjects.mockResolvedValue(mockResult);
|
||||||
|
|
||||||
|
await listStockAuditLevelsRouteHandler(req, res);
|
||||||
|
|
||||||
|
expect(listObjects).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ model: stockAuditLevelModel })
|
||||||
|
);
|
||||||
|
expect(res.send).toHaveBeenCalledWith(mockResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getStockAuditLevelRouteHandler', () => {
|
||||||
|
it('should get a stock audit level', async () => {
|
||||||
|
req.params.id = 'level-1';
|
||||||
|
const mockResult = { _id: 'level-1', name: 'Full audit' };
|
||||||
|
getObject.mockResolvedValue(mockResult);
|
||||||
|
|
||||||
|
await getStockAuditLevelRouteHandler(req, res);
|
||||||
|
|
||||||
|
expect(getObject).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ model: stockAuditLevelModel, id: 'level-1' })
|
||||||
|
);
|
||||||
|
expect(res.send).toHaveBeenCalledWith(mockResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('newStockAuditLevelRouteHandler', () => {
|
||||||
|
it('should create a stock audit level', async () => {
|
||||||
|
req.body = { name: 'Full audit', auditLines: [] };
|
||||||
|
const mockResult = { _id: 'level-1', name: 'Full audit' };
|
||||||
|
newObject.mockResolvedValue(mockResult);
|
||||||
|
|
||||||
|
await newStockAuditLevelRouteHandler(req, res);
|
||||||
|
|
||||||
|
expect(newObject).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
model: stockAuditLevelModel,
|
||||||
|
newData: expect.objectContaining({ name: 'Full audit', auditLines: [] }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(res.send).toHaveBeenCalledWith(mockResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('editStockAuditLevelRouteHandler', () => {
|
||||||
|
it('should edit a stock audit level', async () => {
|
||||||
|
req.params.id = '507f1f77bcf86cd799439011';
|
||||||
|
req.body = { name: 'Updated audit', auditLines: [] };
|
||||||
|
const mockResult = { _id: req.params.id, name: 'Updated audit' };
|
||||||
|
editObject.mockResolvedValue(mockResult);
|
||||||
|
|
||||||
|
await editStockAuditLevelRouteHandler(req, res);
|
||||||
|
|
||||||
|
expect(editObject).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
model: stockAuditLevelModel,
|
||||||
|
updateData: expect.objectContaining({ name: 'Updated audit' }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(res.send).toHaveBeenCalledWith(mockResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
241
src/services/management/stockauditlevels.js
Normal file
241
src/services/management/stockauditlevels.js
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
import config from '../../config.js';
|
||||||
|
import { stockAuditLevelModel } from '../../database/schemas/management/stockauditlevel.schema.js';
|
||||||
|
import log4js from 'log4js';
|
||||||
|
import mongoose from 'mongoose';
|
||||||
|
import {
|
||||||
|
deleteObject,
|
||||||
|
listObjects,
|
||||||
|
getObject,
|
||||||
|
editObject,
|
||||||
|
newObject,
|
||||||
|
listObjectsByProperties,
|
||||||
|
getModelStats,
|
||||||
|
getModelHistory,
|
||||||
|
searchObjects,
|
||||||
|
getPropertyValues,
|
||||||
|
getObjectNeighbors,
|
||||||
|
} from '../../database/database.js';
|
||||||
|
|
||||||
|
const logger = log4js.getLogger('Stock Audit Levels');
|
||||||
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
|
const STOCK_AUDIT_LEVEL_POPULATE = [{ path: 'auditLines.item' }, { path: 'auditLines.itemSku' }];
|
||||||
|
|
||||||
|
export const listStockAuditLevelsRouteHandler = async (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
page = 1,
|
||||||
|
limit = 25,
|
||||||
|
property = '',
|
||||||
|
filter = {},
|
||||||
|
search = '',
|
||||||
|
sort = '',
|
||||||
|
order = 'ascend'
|
||||||
|
) => {
|
||||||
|
const result = await listObjects({
|
||||||
|
model: stockAuditLevelModel,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
property,
|
||||||
|
filter,
|
||||||
|
search,
|
||||||
|
sort,
|
||||||
|
order,
|
||||||
|
populate: STOCK_AUDIT_LEVEL_POPULATE,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error listing stock audit levels.');
|
||||||
|
res.status(result.code).send(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`List of stock audit levels (Page ${page}, Limit ${limit}). Count: ${result.length}.`
|
||||||
|
);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listStockAuditLevelsByPropertiesRouteHandler = async (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
properties = '',
|
||||||
|
filter = {},
|
||||||
|
masterFilter = {}
|
||||||
|
) => {
|
||||||
|
const result = await listObjectsByProperties({
|
||||||
|
model: stockAuditLevelModel,
|
||||||
|
properties,
|
||||||
|
filter,
|
||||||
|
masterFilter,
|
||||||
|
populate: STOCK_AUDIT_LEVEL_POPULATE,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error listing stock audit levels.');
|
||||||
|
res.status(result.code).send(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`List of stock audit levels. Count: ${result.length}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStockAuditLevelPropertyValuesRouteHandler = async (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
property,
|
||||||
|
filter,
|
||||||
|
masterFilter
|
||||||
|
) => {
|
||||||
|
const result = await getPropertyValues({
|
||||||
|
model: stockAuditLevelModel,
|
||||||
|
property,
|
||||||
|
filter: { ...filter, ...masterFilter },
|
||||||
|
});
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const searchStockAuditLevelsRouteHandler = async (req, res, search) => {
|
||||||
|
const result = await searchObjects({
|
||||||
|
model: stockAuditLevelModel,
|
||||||
|
search,
|
||||||
|
populate: STOCK_AUDIT_LEVEL_POPULATE,
|
||||||
|
});
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStockAuditLevelRouteHandler = async (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
const result = await getObject({
|
||||||
|
model: stockAuditLevelModel,
|
||||||
|
id,
|
||||||
|
populate: STOCK_AUDIT_LEVEL_POPULATE,
|
||||||
|
});
|
||||||
|
if (result?.error) {
|
||||||
|
logger.warn(`Stock audit level not found with supplied id.`);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
logger.debug(`Retrieved stock audit level with ID: ${id}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const editStockAuditLevelRouteHandler = async (req, res) => {
|
||||||
|
const id = new mongoose.Types.ObjectId(req.params.id);
|
||||||
|
|
||||||
|
const updateData = {
|
||||||
|
name: req.body.name,
|
||||||
|
tags: req.body.tags,
|
||||||
|
auditLines: req.body.auditLines,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await editObject({
|
||||||
|
model: stockAuditLevelModel,
|
||||||
|
id,
|
||||||
|
updateData,
|
||||||
|
user: req.user,
|
||||||
|
populate: STOCK_AUDIT_LEVEL_POPULATE,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
logger.error('Error editing stock audit level:', result.error);
|
||||||
|
res.status(result.code).send(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Edited stock audit level with ID: ${id}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const newStockAuditLevelRouteHandler = async (req, res) => {
|
||||||
|
const newData = {
|
||||||
|
name: req.body.name,
|
||||||
|
tags: req.body.tags,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await newObject({
|
||||||
|
model: stockAuditLevelModel,
|
||||||
|
newData,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
logger.error('No stock audit level created:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`New stock audit level with ID: ${result._id}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteStockAuditLevelRouteHandler = async (req, res) => {
|
||||||
|
const id = new mongoose.Types.ObjectId(req.params.id);
|
||||||
|
|
||||||
|
const result = await deleteObject({
|
||||||
|
model: stockAuditLevelModel,
|
||||||
|
id,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
logger.error('No stock audit level deleted:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Deleted stock audit level with ID: ${result._id}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStockAuditLevelStatsRouteHandler = async (req, res) => {
|
||||||
|
const result = await getModelStats({ model: stockAuditLevelModel });
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error fetching stock audit level stats:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
logger.trace('Stock audit level stats:', result);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStockAuditLevelHistoryRouteHandler = async (req, res) => {
|
||||||
|
const from = req.query.from;
|
||||||
|
const to = req.query.to;
|
||||||
|
const result = await getModelHistory({ model: stockAuditLevelModel, from, to });
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error fetching stock audit level history:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
logger.trace('Stock audit level history:', result);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStockAuditLevelNeighborsRouteHandler = async (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
property = '',
|
||||||
|
filter = {},
|
||||||
|
search = '',
|
||||||
|
sort = '',
|
||||||
|
order = 'ascend',
|
||||||
|
id
|
||||||
|
) => {
|
||||||
|
if (!id) {
|
||||||
|
return res.status(400).send({ error: 'Missing id parameter', code: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await getObjectNeighbors({
|
||||||
|
model: stockAuditLevelModel,
|
||||||
|
id,
|
||||||
|
filter,
|
||||||
|
search,
|
||||||
|
sort,
|
||||||
|
order,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error fetching stock audit level neighbors.');
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Retrieved stock audit level neighbors for ID: ${id}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
@ -36,7 +36,8 @@ export const EXPORT_FILTER_BY_TYPE = {
|
|||||||
stockEvent: ['parent._id', 'parentType', 'owner._id', 'ownerType'],
|
stockEvent: ['parent._id', 'parentType', 'owner._id', 'ownerType'],
|
||||||
stockLocation: ['name', 'address'],
|
stockLocation: ['name', 'address'],
|
||||||
stockTransfer: ['name', 'state.type', 'postedAt'],
|
stockTransfer: ['name', 'state.type', 'postedAt'],
|
||||||
stockAudit: ['filamentStock._id', 'partStock._id'],
|
stockAudit: ['auditLevel._id', 'stockLocation._id', 'state.type', 'postedAt'],
|
||||||
|
stockAuditLevel: ['name', 'tags'],
|
||||||
documentJob: ['documentTemplate', 'documentPrinter', 'object._id', 'objectType'],
|
documentJob: ['documentTemplate', 'documentPrinter', 'object._id', 'objectType'],
|
||||||
documentTemplate: ['parent._id', 'documentSize._id'],
|
documentTemplate: ['parent._id', 'documentSize._id'],
|
||||||
salesOrder: ['client'],
|
salesOrder: ['client'],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user