Compare commits

..

5 Commits

Author SHA1 Message Date
4458a1d828 Implemented materials and export improvements.
Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
2026-03-08 01:28:21 +00:00
acd4b375af Fixed SKUs updating. 2026-03-08 01:14:52 +00:00
7eb774a297 Added missing SKUs. 2026-03-08 01:07:34 +00:00
73fbb50b34 Implemented Part SKUs. 2026-03-07 23:34:12 +00:00
17e46f6aee Gitignore test results. 2026-03-07 22:44:10 +00:00
41 changed files with 1358 additions and 496 deletions

2
.gitignore vendored
View File

@ -140,3 +140,5 @@ gocdefile/*
gcodefile gcodefile
gcodefiles/* gcodefiles/*
gcodefiles gcodefiles
test-results.xml

View File

@ -19,7 +19,7 @@ const filamentStockSchema = new Schema(
net: { type: Number, required: true }, net: { type: Number, required: true },
gross: { type: Number, required: true }, gross: { type: Number, required: true },
}, },
filament: { type: mongoose.Schema.Types.ObjectId, ref: 'filament', required: true }, filamentSku: { type: mongoose.Schema.Types.ObjectId, ref: 'filamentSku', required: true },
}, },
{ timestamps: true } { timestamps: true }
); );

View File

@ -2,6 +2,12 @@ import mongoose from 'mongoose';
import { purchaseOrderModel } from './purchaseorder.schema.js'; import { purchaseOrderModel } from './purchaseorder.schema.js';
import { salesOrderModel } from '../sales/salesorder.schema.js'; import { salesOrderModel } from '../sales/salesorder.schema.js';
import { taxRateModel } from '../management/taxrate.schema.js'; import { taxRateModel } from '../management/taxrate.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';
import { import {
aggregateRollups, aggregateRollups,
aggregateRollupsHistory, aggregateRollupsHistory,
@ -11,6 +17,18 @@ import {
import { generateId } from '../../utils.js'; import { generateId } from '../../utils.js';
const { Schema } = mongoose; const { Schema } = mongoose;
const skuModelsByItemType = {
filament: filamentSkuModel,
part: partSkuModel,
product: productSkuModel,
};
const parentModelsByItemType = {
filament: filamentModel,
part: partModel,
product: productModel,
};
const orderItemSchema = new Schema( const orderItemSchema = new Schema(
{ {
_reference: { type: String, default: () => generateId()() }, _reference: { type: String, default: () => generateId()() },
@ -21,7 +39,16 @@ const orderItemSchema = new Schema(
}, },
order: { type: Schema.Types.ObjectId, refPath: 'orderType', required: true }, order: { type: Schema.Types.ObjectId, refPath: 'orderType', required: true },
itemType: { type: String, required: true }, itemType: { type: String, required: true },
item: { type: Schema.Types.ObjectId, refPath: 'itemType', required: true }, item: { type: Schema.Types.ObjectId, refPath: 'itemType', required: false },
sku: {
type: Schema.Types.ObjectId,
ref: function () {
return ['filament', 'part', 'product'].includes(this.itemType)
? this.itemType + 'Sku'
: null;
},
required: false,
},
syncAmount: { type: String, required: false, default: null }, syncAmount: { type: String, required: false, default: null },
itemAmount: { type: Number, required: true }, itemAmount: { type: Number, required: true },
quantity: { type: Number, required: true }, quantity: { type: Number, required: true },
@ -88,9 +115,55 @@ orderItemSchema.statics.recalculate = async function (orderItem, user) {
return; return;
} }
var taxRate = orderItem.taxRate; // If SKU present and syncAmount is set, check if override is on for the price mode and use that price instead
let effectiveItemAmount = orderItem.itemAmount;
const syncAmount = orderItem.syncAmount;
const skuId = orderItem.sku?._id || orderItem.sku;
const itemType = orderItem.itemType;
if (syncAmount && skuId && itemType && ['filament', 'part', 'product'].includes(itemType)) {
const skuModel = skuModelsByItemType[itemType];
const parentModel = parentModelsByItemType[itemType];
if (skuModel && parentModel) {
const sku = await getObject({
model: skuModel,
id: skuId,
cached: true,
});
if (sku) {
const parentId = sku.part?._id || sku.part || sku.product?._id || sku.product || sku.filament?._id || sku.filament;
if (syncAmount === 'itemCost') {
if (sku.overrideCost && sku.cost != null) {
effectiveItemAmount = sku.cost;
} else if (parentId) {
const parent = await getObject({
model: parentModel,
id: parentId,
cached: true,
});
if (parent && parent.cost != null) {
effectiveItemAmount = parent.cost;
}
}
} else if (syncAmount === 'itemPrice' && itemType !== 'filament') {
if (sku.overridePrice && sku.price != null) {
effectiveItemAmount = sku.price;
} else if (parentId) {
const parent = await getObject({
model: parentModel,
id: parentId,
cached: true,
});
if (parent && parent.price != null) {
effectiveItemAmount = parent.price;
}
}
}
}
}
}
if (orderItem.taxRate?._id && Object.keys(orderItem.taxRate).length == 1) { let taxRate = orderItem.taxRate;
if (orderItem.taxRate?._id && Object.keys(orderItem.taxRate).length === 1) {
taxRate = await getObject({ taxRate = await getObject({
model: taxRateModel, model: taxRateModel,
id: orderItem.taxRate._id, id: orderItem.taxRate._id,
@ -98,18 +171,25 @@ orderItemSchema.statics.recalculate = async function (orderItem, user) {
}); });
} }
const orderTotalAmount = orderItem.itemAmount * orderItem.quantity; const orderTotalAmount = effectiveItemAmount * orderItem.quantity;
const orderTotalAmountWithTax = orderTotalAmount * (1 + (taxRate?.rate || 0) / 100); const orderTotalAmountWithTax = orderTotalAmount * (1 + (taxRate?.rate || 0) / 100);
const orderItemUpdateData = {
totalAmount: orderTotalAmount,
totalAmountWithTax: orderTotalAmountWithTax,
invoicedAmountRemaining: orderTotalAmount - orderItem.invoicedAmount,
invoicedAmountWithTaxRemaining: orderTotalAmountWithTax - orderItem.invoicedAmountWithTax,
invoicedQuantityRemaining: orderItem.quantity - orderItem.invoicedQuantity,
};
if (effectiveItemAmount !== orderItem.itemAmount) {
orderItemUpdateData.itemAmount = effectiveItemAmount;
orderItem.itemAmount = effectiveItemAmount;
}
await editObject({ await editObject({
model: orderItemModel, model: this,
id: orderItem._id, id: orderItem._id,
updateData: { updateData: orderItemUpdateData,
invoicedAmountRemaining: orderTotalAmount - orderItem.invoicedAmount,
invoicedAmountWithTaxRemaining: orderTotalAmountWithTax - orderItem.invoicedAmountWithTax,
invoicedQuantityRemaining: orderItem.quantity - orderItem.invoicedQuantity,
totalAmount: orderTotalAmount,
totalAmountWithTax: orderTotalAmountWithTax,
},
user, user,
recalculate: false, recalculate: false,
}); });

View File

@ -11,7 +11,7 @@ const partStockSchema = new Schema(
type: { type: String, required: true }, type: { type: String, required: true },
progress: { type: Number, required: false }, progress: { type: Number, required: false },
}, },
part: { type: mongoose.Schema.Types.ObjectId, ref: 'part', required: true }, partSku: { type: mongoose.Schema.Types.ObjectId, ref: 'partSku', required: true },
currentQuantity: { type: Number, required: true }, currentQuantity: { type: Number, required: true },
sourceType: { type: String, required: true }, sourceType: { type: String, required: true },
source: { type: Schema.Types.ObjectId, refPath: 'sourceType', required: true }, source: { type: Schema.Types.ObjectId, refPath: 'sourceType', required: true },

View File

@ -5,7 +5,7 @@ import { aggregateRollups, aggregateRollupsHistory } from '../../database.js';
const partStockUsageSchema = new Schema({ const partStockUsageSchema = new Schema({
partStock: { type: Schema.Types.ObjectId, ref: 'partStock', required: false }, partStock: { type: Schema.Types.ObjectId, ref: 'partStock', required: false },
part: { type: Schema.Types.ObjectId, ref: 'part', required: true }, partSku: { type: Schema.Types.ObjectId, ref: 'partSku', required: true },
quantity: { type: Number, required: true }, quantity: { type: Number, required: true },
}); });
@ -18,7 +18,7 @@ const productStockSchema = new Schema(
progress: { type: Number, required: false }, progress: { type: Number, required: false },
}, },
postedAt: { type: Date, required: false }, postedAt: { type: Date, required: false },
product: { type: mongoose.Schema.Types.ObjectId, ref: 'product', required: true }, productSku: { type: mongoose.Schema.Types.ObjectId, ref: 'productSku', required: true },
currentQuantity: { type: Number, required: true }, currentQuantity: { type: Number, required: true },
partStocks: [partStockUsageSchema], partStocks: [partStockUsageSchema],
}, },

View File

@ -2,24 +2,21 @@ import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; import { generateId } from '../../utils.js';
const { Schema } = mongoose; const { Schema } = mongoose;
// Filament base - cost and tax; color and cost override at FilamentSKU
const filamentSchema = new mongoose.Schema({ const filamentSchema = new mongoose.Schema({
_reference: { type: String, default: () => generateId()() }, _reference: { type: String, default: () => generateId()() },
name: { required: true, type: String }, name: { required: true, type: String },
barcode: { required: false, type: String }, barcode: { required: false, type: String },
url: { required: false, type: String }, url: { required: false, type: String },
image: { required: false, type: Buffer }, image: { required: false, type: Buffer },
color: { required: true, type: String }, material: { type: Schema.Types.ObjectId, ref: 'material', required: true },
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true },
type: { required: true, type: String },
cost: { required: true, type: Number },
costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: true },
costWithTax: { required: true, type: Number },
diameter: { required: true, type: Number }, diameter: { required: true, type: Number },
density: { required: true, type: Number }, density: { required: true, type: Number },
createdAt: { required: true, type: Date },
updatedAt: { required: true, type: Date },
emptySpoolWeight: { required: true, type: Number }, emptySpoolWeight: { required: true, type: Number },
}); cost: { type: Number, required: false },
costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
costWithTax: { type: Number, required: false },
}, { timestamps: true });
filamentSchema.virtual('id').get(function () { filamentSchema.virtual('id').get(function () {
return this._id; return this._id;
@ -27,4 +24,21 @@ filamentSchema.virtual('id').get(function () {
filamentSchema.set('toJSON', { virtuals: true }); filamentSchema.set('toJSON', { virtuals: true });
filamentSchema.statics.recalculate = async function (filament, user) {
const orderItemModel = mongoose.model('orderItem');
const itemId = filament._id;
const draftOrderItems = await orderItemModel
.find({
'state.type': 'draft',
itemType: 'filament',
item: itemId,
})
.populate('order')
.lean();
for (const orderItem of draftOrderItems) {
await orderItemModel.recalculate(orderItem, user);
}
};
export const filamentModel = mongoose.model('filament', filamentSchema); export const filamentModel = mongoose.model('filament', filamentSchema);

View File

@ -0,0 +1,48 @@
import mongoose from 'mongoose';
import { generateId } from '../../utils.js';
const { Schema } = mongoose;
// Define the main filament SKU schema - color and cost live at SKU level
const filamentSkuSchema = new Schema(
{
_reference: { type: String, default: () => generateId()() },
barcode: { type: String, required: false },
filament: { type: Schema.Types.ObjectId, ref: 'filament', required: true },
name: { type: String, required: true },
description: { type: String, required: false },
color: { type: String, required: true },
cost: { type: Number, required: false },
overrideCost: { type: Boolean, default: false },
costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
costWithTax: { type: Number, required: false },
},
{ timestamps: true }
);
// Add virtual id getter
filamentSkuSchema.virtual('id').get(function () {
return this._id;
});
// Configure JSON serialization to include virtuals
filamentSkuSchema.set('toJSON', { virtuals: true });
filamentSkuSchema.statics.recalculate = async function (filamentSku, user) {
const orderItemModel = mongoose.model('orderItem');
const skuId = filamentSku._id;
const draftOrderItems = await orderItemModel
.find({
'state.type': 'draft',
itemType: 'filament',
sku: skuId,
})
.populate('order')
.lean();
for (const orderItem of draftOrderItems) {
await orderItemModel.recalculate(orderItem, user);
}
};
// Create and export the model
export const filamentSkuModel = mongoose.model('filamentSku', filamentSkuSchema);

View File

@ -1,13 +1,15 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; import { generateId } from '../../utils.js';
const materialSchema = new mongoose.Schema({ const materialSchema = new mongoose.Schema(
_reference: { type: String, default: () => generateId()() }, {
name: { required: true, type: String }, _reference: { type: String, default: () => generateId()() },
url: { required: false, type: String }, name: { required: true, type: String },
image: { required: false, type: Buffer }, url: { required: false, type: String },
tags: [{ type: String }], tags: [{ type: String }],
}); },
{ timestamps: true }
);
materialSchema.virtual('id').get(function () { materialSchema.virtual('id').get(function () {
return this._id; return this._id;

View File

@ -2,22 +2,22 @@ import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; import { generateId } from '../../utils.js';
const { Schema } = mongoose; const { Schema } = mongoose;
// Define the main part schema // Define the main part schema - cost/price and tax; override at PartSku
const partSchema = new Schema( const partSchema = new Schema(
{ {
_reference: { type: String, default: () => generateId()() }, _reference: { type: String, default: () => generateId()() },
name: { type: String, required: true }, name: { type: String, required: true },
fileName: { type: String, required: false }, fileName: { type: String, required: false },
priceMode: { type: String, default: 'margin' },
price: { type: Number, required: true },
cost: { type: Number, required: true },
margin: { type: Number, required: false },
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true },
file: { type: mongoose.SchemaTypes.ObjectId, ref: 'file', required: false }, file: { type: mongoose.SchemaTypes.ObjectId, ref: 'file', required: false },
priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false }, cost: { type: Number, required: false },
price: { type: Number, required: false },
priceMode: { type: String, default: 'margin' },
margin: { type: Number, required: false },
amount: { type: Number, required: false },
costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false }, costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
priceWithTax: { type: Number, required: false }, priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
costWithTax: { type: Number, required: false }, costWithTax: { type: Number, required: false },
priceWithTax: { type: Number, required: false },
}, },
{ timestamps: true } { timestamps: true }
); );
@ -30,5 +30,22 @@ partSchema.virtual('id').get(function () {
// Configure JSON serialization to include virtuals // Configure JSON serialization to include virtuals
partSchema.set('toJSON', { virtuals: true }); partSchema.set('toJSON', { virtuals: true });
partSchema.statics.recalculate = async function (part, user) {
const orderItemModel = mongoose.model('orderItem');
const itemId = part._id;
const draftOrderItems = await orderItemModel
.find({
'state.type': 'draft',
itemType: 'part',
item: itemId,
})
.populate('order')
.lean();
for (const orderItem of draftOrderItems) {
await orderItemModel.recalculate(orderItem, user);
}
};
// Create and export the model // Create and export the model
export const partModel = mongoose.model('part', partSchema); export const partModel = mongoose.model('part', partSchema);

View File

@ -0,0 +1,54 @@
import mongoose from 'mongoose';
import { generateId } from '../../utils.js';
const { Schema } = mongoose;
// Define the main part SKU schema - pricing lives at SKU level
const partSkuSchema = new Schema(
{
_reference: { type: String, default: () => generateId()() },
barcode: { type: String, required: false },
part: { type: Schema.Types.ObjectId, ref: 'part', required: true },
name: { type: String, required: true },
description: { type: String, required: false },
priceMode: { type: String, default: 'margin' },
price: { type: Number, required: false },
cost: { type: Number, required: false },
overrideCost: { type: Boolean, default: false },
overridePrice: { type: Boolean, default: false },
margin: { type: Number, required: false },
amount: { type: Number, required: false },
priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
priceWithTax: { type: Number, required: false },
costWithTax: { type: Number, required: false },
},
{ timestamps: true }
);
// Add virtual id getter
partSkuSchema.virtual('id').get(function () {
return this._id;
});
// Configure JSON serialization to include virtuals
partSkuSchema.set('toJSON', { virtuals: true });
partSkuSchema.statics.recalculate = async function (partSku, user) {
const orderItemModel = mongoose.model('orderItem');
const skuId = partSku._id;
const draftOrderItems = await orderItemModel
.find({
'state.type': 'draft',
itemType: 'part',
sku: skuId,
})
.populate('order')
.lean();
for (const orderItem of draftOrderItems) {
await orderItemModel.recalculate(orderItem, user);
}
};
// Create and export the model
export const partSkuModel = mongoose.model('partSku', partSkuSchema);

View File

@ -2,11 +2,6 @@ import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; import { generateId } from '../../utils.js';
const { Schema } = mongoose; const { Schema } = mongoose;
const partSchema = new Schema({
part: { type: Schema.Types.ObjectId, ref: 'part', required: true },
quantity: { type: Number, required: true },
});
// Define the main product schema // Define the main product schema
const productSchema = new Schema( const productSchema = new Schema(
{ {
@ -14,13 +9,16 @@ const productSchema = new Schema(
name: { type: String, required: true }, name: { type: String, required: true },
tags: [{ type: String }], tags: [{ type: String }],
version: { type: String }, version: { type: String },
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true },
cost: { type: Number, required: false },
price: { type: Number, required: false },
priceMode: { type: String, default: 'margin' }, priceMode: { type: String, default: 'margin' },
margin: { type: Number, required: false }, margin: { type: Number, required: false },
amount: { type: Number, required: false }, amount: { type: Number, required: false },
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true },
parts: [partSchema],
priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false }, costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
costWithTax: { type: Number, required: false },
priceWithTax: { type: Number, required: false },
}, },
{ timestamps: true } { timestamps: true }
); );
@ -32,5 +30,22 @@ productSchema.virtual('id').get(function () {
// Configure JSON serialization to include virtuals // Configure JSON serialization to include virtuals
productSchema.set('toJSON', { virtuals: true }); productSchema.set('toJSON', { virtuals: true });
productSchema.statics.recalculate = async function (product, user) {
const orderItemModel = mongoose.model('orderItem');
const itemId = product._id;
const draftOrderItems = await orderItemModel
.find({
'state.type': 'draft',
itemType: 'product',
item: itemId,
})
.populate('order')
.lean();
for (const orderItem of draftOrderItems) {
await orderItemModel.recalculate(orderItem, user);
}
};
// Create and export the model // Create and export the model
export const productModel = mongoose.model('product', productSchema); export const productModel = mongoose.model('product', productSchema);

View File

@ -2,14 +2,31 @@ import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; import { generateId } from '../../utils.js';
const { Schema } = mongoose; const { Schema } = mongoose;
const partSkuUsageSchema = new Schema({
partSku: { type: Schema.Types.ObjectId, ref: 'partSku', required: true },
quantity: { type: Number, required: true },
});
// Define the main product SKU schema // Define the main product SKU schema
const productSkuSchema = new Schema( const productSkuSchema = new Schema(
{ {
_reference: { type: String, default: () => generateId()() }, _reference: { type: String, default: () => generateId()() },
sku: { type: String, required: true }, barcode: { type: String, required: false },
product: { type: Schema.Types.ObjectId, ref: 'product', required: true }, product: { type: Schema.Types.ObjectId, ref: 'product', required: true },
name: { type: String, required: true }, name: { type: String, required: true },
description: { type: String, required: false }, description: { type: String, required: false },
priceMode: { type: String, default: 'margin' },
price: { type: Number, required: false },
cost: { type: Number, required: false },
overrideCost: { type: Boolean, default: false },
overridePrice: { type: Boolean, default: false },
margin: { type: Number, required: false },
amount: { type: Number, required: false },
parts: [partSkuUsageSchema],
priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
priceWithTax: { type: Number, required: false },
costWithTax: { type: Number, required: false },
}, },
{ timestamps: true } { timestamps: true }
); );
@ -22,5 +39,22 @@ productSkuSchema.virtual('id').get(function () {
// Configure JSON serialization to include virtuals // Configure JSON serialization to include virtuals
productSkuSchema.set('toJSON', { virtuals: true }); productSkuSchema.set('toJSON', { virtuals: true });
productSkuSchema.statics.recalculate = async function (productSku, user) {
const orderItemModel = mongoose.model('orderItem');
const skuId = productSku._id;
const draftOrderItems = await orderItemModel
.find({
'state.type': 'draft',
itemType: 'product',
sku: skuId,
})
.populate('order')
.lean();
for (const orderItem of draftOrderItems) {
await orderItemModel.recalculate(orderItem, user);
}
};
// Create and export the model // Create and export the model
export const productSkuModel = mongoose.model('productSku', productSkuSchema); export const productSkuModel = mongoose.model('productSku', productSkuSchema);

View File

@ -2,11 +2,14 @@ import { jobModel } from './production/job.schema.js';
import { subJobModel } from './production/subjob.schema.js'; import { subJobModel } from './production/subjob.schema.js';
import { printerModel } from './production/printer.schema.js'; import { printerModel } from './production/printer.schema.js';
import { filamentModel } from './management/filament.schema.js'; import { filamentModel } from './management/filament.schema.js';
import { filamentSkuModel } from './management/filamentsku.schema.js';
import { gcodeFileModel } from './production/gcodefile.schema.js'; import { gcodeFileModel } from './production/gcodefile.schema.js';
import { partModel } from './management/part.schema.js'; import { partModel } from './management/part.schema.js';
import { partSkuModel } from './management/partsku.schema.js';
import { productModel } from './management/product.schema.js'; import { productModel } from './management/product.schema.js';
import { productSkuModel } from './management/productsku.schema.js'; import { productSkuModel } from './management/productsku.schema.js';
import { vendorModel } from './management/vendor.schema.js'; import { vendorModel } from './management/vendor.schema.js';
import { materialModel } from './management/material.schema.js';
import { filamentStockModel } from './inventory/filamentstock.schema.js'; import { filamentStockModel } from './inventory/filamentstock.schema.js';
import { purchaseOrderModel } from './inventory/purchaseorder.schema.js'; import { purchaseOrderModel } from './inventory/purchaseorder.schema.js';
import { orderItemModel } from './inventory/orderitem.schema.js'; import { orderItemModel } from './inventory/orderitem.schema.js';
@ -52,6 +55,13 @@ export const models = {
referenceField: '_reference', referenceField: '_reference',
label: 'Filament', label: 'Filament',
}, },
FSU: {
model: filamentSkuModel,
idField: '_id',
type: 'filamentSku',
referenceField: '_reference',
label: 'Filament SKU',
},
GCF: { GCF: {
model: gcodeFileModel, model: gcodeFileModel,
idField: '_id', idField: '_id',
@ -67,6 +77,13 @@ export const models = {
referenceField: '_reference', referenceField: '_reference',
label: 'Part', label: 'Part',
}, },
PSU: {
model: partSkuModel,
idField: '_id',
type: 'partSku',
referenceField: '_reference',
label: 'Part SKU',
},
PRD: { PRD: {
model: productModel, model: productModel,
idField: '_id', idField: '_id',
@ -88,6 +105,13 @@ export const models = {
referenceField: '_reference', referenceField: '_reference',
label: 'Vendor', label: 'Vendor',
}, },
MAT: {
model: materialModel,
idField: '_id',
type: 'material',
referenceField: '_reference',
label: 'Material',
},
SJB: { SJB: {
model: subJobModel, model: subJobModel,
idField: '_id', idField: '_id',

View File

@ -13,7 +13,7 @@ const gcodeFileSchema = new mongoose.Schema(
name: { required: true, type: String }, name: { required: true, type: String },
gcodeFileName: { required: false, type: String }, gcodeFileName: { required: false, type: String },
size: { type: Number, required: false }, size: { type: Number, required: false },
filament: { type: Schema.Types.ObjectId, ref: 'filament', required: true }, filamentSku: { type: Schema.Types.ObjectId, ref: 'filamentSku', required: true },
parts: [partSchema], parts: [partSchema],
file: { type: mongoose.SchemaTypes.ObjectId, ref: 'file', required: false }, file: { type: mongoose.SchemaTypes.ObjectId, ref: 'file', required: false },
cost: { type: Number, required: false }, cost: { type: Number, required: false },

View File

@ -14,8 +14,10 @@ import {
subJobRoutes, subJobRoutes,
gcodeFileRoutes, gcodeFileRoutes,
filamentRoutes, filamentRoutes,
filamentSkuRoutes,
spotlightRoutes, spotlightRoutes,
partRoutes, partRoutes,
partSkuRoutes,
productRoutes, productRoutes,
productSkuRoutes, productSkuRoutes,
vendorRoutes, vendorRoutes,
@ -132,7 +134,9 @@ app.use('/jobs', jobRoutes);
app.use('/subjobs', subJobRoutes); app.use('/subjobs', subJobRoutes);
app.use('/gcodefiles', gcodeFileRoutes); app.use('/gcodefiles', gcodeFileRoutes);
app.use('/filaments', filamentRoutes); app.use('/filaments', filamentRoutes);
app.use('/filamentskus', filamentSkuRoutes);
app.use('/parts', partRoutes); app.use('/parts', partRoutes);
app.use('/partskus', partSkuRoutes);
app.use('/products', productRoutes); app.use('/products', productRoutes);
app.use('/productskus', productSkuRoutes); app.use('/productskus', productSkuRoutes);
app.use('/vendors', vendorRoutes); app.use('/vendors', vendorRoutes);

View File

@ -8,8 +8,10 @@ import jobRoutes from './production/jobs.js';
import subJobRoutes from './production/subjobs.js'; import subJobRoutes from './production/subjobs.js';
import gcodeFileRoutes from './production/gcodefiles.js'; import gcodeFileRoutes from './production/gcodefiles.js';
import filamentRoutes from './management/filaments.js'; import filamentRoutes from './management/filaments.js';
import filamentSkuRoutes from './management/filamentskus.js';
import spotlightRoutes from './misc/spotlight.js'; import spotlightRoutes from './misc/spotlight.js';
import partRoutes from './management/parts.js'; import partRoutes from './management/parts.js';
import partSkuRoutes from './management/partskus.js';
import productRoutes from './management/products.js'; import productRoutes from './management/products.js';
import productSkuRoutes from './management/productskus.js'; import productSkuRoutes from './management/productskus.js';
import vendorRoutes from './management/vendors.js'; import vendorRoutes from './management/vendors.js';
@ -54,8 +56,10 @@ export {
subJobRoutes, subJobRoutes,
gcodeFileRoutes, gcodeFileRoutes,
filamentRoutes, filamentRoutes,
filamentSkuRoutes,
spotlightRoutes, spotlightRoutes,
partRoutes, partRoutes,
partSkuRoutes,
productRoutes, productRoutes,
productSkuRoutes, productSkuRoutes,
vendorRoutes, vendorRoutes,

View File

@ -18,14 +18,14 @@ import {
// list of filament stocks // list of filament stocks
router.get('/', isAuthenticated, (req, res) => { router.get('/', isAuthenticated, (req, res) => {
const { page, limit, property, search, sort, order } = req.query; const { page, limit, property, search, sort, order } = req.query;
const allowedFilters = ['filament', 'state', 'startingWeight', 'currentWeight', 'filament._id']; const allowedFilters = ['filamentSku', 'state', 'startingWeight', 'currentWeight', 'filamentSku._id'];
const filter = getFilter(req.query, allowedFilters); const filter = getFilter(req.query, allowedFilters);
listFilamentStocksRouteHandler(req, res, page, limit, property, filter, search, sort, order); listFilamentStocksRouteHandler(req, res, page, limit, property, filter, search, sort, order);
}); });
router.get('/properties', isAuthenticated, (req, res) => { router.get('/properties', isAuthenticated, (req, res) => {
let properties = convertPropertiesString(req.query.properties); let properties = convertPropertiesString(req.query.properties);
const allowedFilters = ['filament', 'state.type']; const allowedFilters = ['filamentSku', 'state.type'];
const filter = getFilter(req.query, allowedFilters, false); const filter = getFilter(req.query, allowedFilters, false);
var masterFilter = {}; var masterFilter = {};
if (req.query.masterFilter) { if (req.query.masterFilter) {

View File

@ -18,7 +18,7 @@ import {
// list of part stocks // list of part stocks
router.get('/', isAuthenticated, (req, res) => { router.get('/', isAuthenticated, (req, res) => {
const { page, limit, property, search, sort, order } = req.query; const { page, limit, property, search, sort, order } = req.query;
const allowedFilters = ['part', 'state', 'startingQuantity', 'currentQuantity', 'part._id']; const allowedFilters = ['partSku', 'state', 'startingQuantity', 'currentQuantity', 'partSku._id'];
const filter = getFilter(req.query, allowedFilters); const filter = getFilter(req.query, allowedFilters);
listPartStocksRouteHandler(req, res, page, limit, property, filter, search, sort, order); listPartStocksRouteHandler(req, res, page, limit, property, filter, search, sort, order);
}); });

View File

@ -18,14 +18,14 @@ import {
router.get('/', isAuthenticated, (req, res) => { router.get('/', isAuthenticated, (req, res) => {
const { page, limit, property, search, sort, order } = req.query; const { page, limit, property, search, sort, order } = req.query;
const allowedFilters = ['product', 'state', 'currentQuantity', 'product._id']; const allowedFilters = ['productSku', 'state', 'currentQuantity', 'productSku._id'];
const filter = getFilter(req.query, allowedFilters); const filter = getFilter(req.query, allowedFilters);
listProductStocksRouteHandler(req, res, page, limit, property, filter, search, sort, order); listProductStocksRouteHandler(req, res, page, limit, property, filter, search, sort, order);
}); });
router.get('/properties', isAuthenticated, (req, res) => { router.get('/properties', isAuthenticated, (req, res) => {
let properties = convertPropertiesString(req.query.properties); let properties = convertPropertiesString(req.query.properties);
const allowedFilters = ['product', 'state.type']; const allowedFilters = ['productSku', 'state.type'];
const filter = getFilter(req.query, allowedFilters, false); const filter = getFilter(req.query, allowedFilters, false);
var masterFilter = {}; var masterFilter = {};
if (req.query.masterFilter) { if (req.query.masterFilter) {

View File

@ -20,12 +20,10 @@ router.get('/', isAuthenticated, (req, res) => {
const allowedFilters = [ const allowedFilters = [
'_id', '_id',
'type', 'material',
'vendor.name', 'material._id',
'diameter', 'diameter',
'color',
'name', 'name',
'vendor._id',
'cost', 'cost',
]; ];
@ -44,7 +42,7 @@ router.get('/', isAuthenticated, (req, res) => {
router.get('/properties', isAuthenticated, (req, res) => { router.get('/properties', isAuthenticated, (req, res) => {
let properties = convertPropertiesString(req.query.properties); let properties = convertPropertiesString(req.query.properties);
const allowedFilters = ['diameter', 'type', 'vendor']; const allowedFilters = ['diameter', 'material'];
const filter = getFilter(req.query, allowedFilters, false); const filter = getFilter(req.query, allowedFilters, false);
listFilamentsByPropertiesRouteHandler(req, res, properties, filter); listFilamentsByPropertiesRouteHandler(req, res, properties, filter);
}); });

View File

@ -0,0 +1,59 @@
import express from 'express';
import { isAuthenticated } from '../../keycloak.js';
import { getFilter, convertPropertiesString } from '../../utils.js';
const router = express.Router();
import {
listFilamentSkusRouteHandler,
getFilamentSkuRouteHandler,
editFilamentSkuRouteHandler,
newFilamentSkuRouteHandler,
deleteFilamentSkuRouteHandler,
listFilamentSkusByPropertiesRouteHandler,
getFilamentSkuStatsRouteHandler,
getFilamentSkuHistoryRouteHandler,
} from '../../services/management/filamentskus.js';
router.get('/', isAuthenticated, (req, res) => {
const { page, limit, property, search, sort, order } = req.query;
const allowedFilters = ['_id', 'barcode', 'filament', 'filament._id', 'name', 'color', 'cost'];
const filter = getFilter(req.query, allowedFilters);
listFilamentSkusRouteHandler(req, res, page, limit, property, filter, search, sort, order);
});
router.get('/properties', isAuthenticated, (req, res) => {
let properties = convertPropertiesString(req.query.properties);
const allowedFilters = ['filament', 'filament._id'];
const filter = getFilter(req.query, allowedFilters, false);
let masterFilter = {};
if (req.query.masterFilter) {
masterFilter = JSON.parse(req.query.masterFilter);
}
listFilamentSkusByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
});
router.post('/', isAuthenticated, (req, res) => {
newFilamentSkuRouteHandler(req, res);
});
router.get('/stats', isAuthenticated, (req, res) => {
getFilamentSkuStatsRouteHandler(req, res);
});
router.get('/history', isAuthenticated, (req, res) => {
getFilamentSkuHistoryRouteHandler(req, res);
});
router.get('/:id', isAuthenticated, (req, res) => {
getFilamentSkuRouteHandler(req, res);
});
router.put('/:id', isAuthenticated, async (req, res) => {
editFilamentSkuRouteHandler(req, res);
});
router.delete('/:id', isAuthenticated, async (req, res) => {
deleteFilamentSkuRouteHandler(req, res);
});
export default router;

View File

@ -1,10 +1,11 @@
import express from 'express'; import express from 'express';
import { isAuthenticated } from '../../keycloak.js'; import { isAuthenticated } from '../../keycloak.js';
import { parseFilter } from '../../utils.js'; import { convertPropertiesString, getFilter, parseFilter } from '../../utils.js';
const router = express.Router(); const router = express.Router();
import { import {
listMaterialsRouteHandler, listMaterialsRouteHandler,
listMaterialsByPropertiesRouteHandler,
getMaterialRouteHandler, getMaterialRouteHandler,
editMaterialRouteHandler, editMaterialRouteHandler,
newMaterialRouteHandler, newMaterialRouteHandler,
@ -14,22 +15,26 @@ import {
// list of materials // list of materials
router.get('/', isAuthenticated, (req, res) => { router.get('/', isAuthenticated, (req, res) => {
const { page, limit, property } = req.query; const { page, limit, property, search, sort, order } = req.query;
const allowedFilters = ['type', 'brand', 'diameter', 'color']; const allowedFilters = ['_id', 'name', 'tags'];
var filter = {}; var filter = {};
for (const [key, value] of Object.entries(req.query)) { for (const [key, value] of Object.entries(req.query)) {
for (var i = 0; i < allowedFilters.length; i++) { if (allowedFilters.includes(key)) {
if (key == allowedFilters[i]) { filter = { ...filter, ...parseFilter(key, value) };
const parsedFilter = parseFilter(key, value);
filter = { ...filter, ...parsedFilter };
}
} }
} }
listMaterialsRouteHandler(req, res, page, limit, property, filter); listMaterialsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
});
router.get('/properties', isAuthenticated, (req, res) => {
let properties = convertPropertiesString(req.query.properties);
const allowedFilters = ['name', 'tags'];
const filter = getFilter(req.query, allowedFilters, false);
listMaterialsByPropertiesRouteHandler(req, res, properties, filter);
}); });
router.post('/', isAuthenticated, (req, res) => { router.post('/', isAuthenticated, (req, res) => {
@ -50,7 +55,7 @@ router.get('/:id', isAuthenticated, (req, res) => {
getMaterialRouteHandler(req, res); getMaterialRouteHandler(req, res);
}); });
// update printer info // update material info
router.put('/:id', isAuthenticated, async (req, res) => { router.put('/:id', isAuthenticated, async (req, res) => {
editMaterialRouteHandler(req, res); editMaterialRouteHandler(req, res);
}); });

View File

@ -0,0 +1,59 @@
import express from 'express';
import { isAuthenticated } from '../../keycloak.js';
import { getFilter, convertPropertiesString } from '../../utils.js';
const router = express.Router();
import {
listPartSkusRouteHandler,
getPartSkuRouteHandler,
editPartSkuRouteHandler,
newPartSkuRouteHandler,
deletePartSkuRouteHandler,
listPartSkusByPropertiesRouteHandler,
getPartSkuStatsRouteHandler,
getPartSkuHistoryRouteHandler,
} from '../../services/management/partskus.js';
router.get('/', isAuthenticated, (req, res) => {
const { page, limit, property, search, sort, order } = req.query;
const allowedFilters = ['_id', 'barcode', 'part', 'part._id', 'name', 'cost', 'price'];
const filter = getFilter(req.query, allowedFilters);
listPartSkusRouteHandler(req, res, page, limit, property, filter, search, sort, order);
});
router.get('/properties', isAuthenticated, (req, res) => {
let properties = convertPropertiesString(req.query.properties);
const allowedFilters = ['part', 'part._id'];
const filter = getFilter(req.query, allowedFilters, false);
let masterFilter = {};
if (req.query.masterFilter) {
masterFilter = JSON.parse(req.query.masterFilter);
}
listPartSkusByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
});
router.post('/', isAuthenticated, (req, res) => {
newPartSkuRouteHandler(req, res);
});
router.get('/stats', isAuthenticated, (req, res) => {
getPartSkuStatsRouteHandler(req, res);
});
router.get('/history', isAuthenticated, (req, res) => {
getPartSkuHistoryRouteHandler(req, res);
});
router.get('/:id', isAuthenticated, (req, res) => {
getPartSkuRouteHandler(req, res);
});
router.put('/:id', isAuthenticated, async (req, res) => {
editPartSkuRouteHandler(req, res);
});
router.delete('/:id', isAuthenticated, async (req, res) => {
deletePartSkuRouteHandler(req, res);
});
export default router;

View File

@ -16,7 +16,7 @@ import {
router.get('/', isAuthenticated, (req, res) => { router.get('/', isAuthenticated, (req, res) => {
const { page, limit, property, search, sort, order } = req.query; const { page, limit, property, search, sort, order } = req.query;
const allowedFilters = ['_id', 'sku', 'product', 'product._id', 'name']; const allowedFilters = ['_id', 'barcode', 'product', 'product._id', 'name', 'cost', 'price'];
const filter = getFilter(req.query, allowedFilters); const filter = getFilter(req.query, allowedFilters);
listProductSkusRouteHandler(req, res, page, limit, property, filter, search, sort, order); listProductSkusRouteHandler(req, res, page, limit, property, filter, search, sort, order);
}); });

View File

@ -36,7 +36,7 @@ export const listFilamentStocksRouteHandler = async (
search, search,
sort, sort,
order, order,
populate: [{ path: 'filament' }], populate: [{ path: 'filamentSku' }],
}); });
if (result?.error) { if (result?.error) {
@ -60,7 +60,7 @@ export const listFilamentStocksByPropertiesRouteHandler = async (
model: filamentStockModel, model: filamentStockModel,
properties, properties,
filter, filter,
populate: ['filament'], populate: ['filamentSku'],
masterFilter, masterFilter,
}); });
@ -79,7 +79,7 @@ export const getFilamentStockRouteHandler = async (req, res) => {
const result = await getObject({ const result = await getObject({
model: filamentStockModel, model: filamentStockModel,
id, id,
populate: [{ path: 'filament' }], populate: [{ path: 'filamentSku' }],
}); });
if (result?.error) { if (result?.error) {
logger.warn(`Filament Stock not found with supplied id.`); logger.warn(`Filament Stock not found with supplied id.`);
@ -146,7 +146,7 @@ export const newFilamentStockRouteHandler = async (req, res) => {
updatedAt: new Date(), updatedAt: new Date(),
startingWeight: req.body.startingWeight, startingWeight: req.body.startingWeight,
currentWeight: req.body.currentWeight, currentWeight: req.body.currentWeight,
filament: req.body.filament, filamentSku: req.body.filamentSku,
state: req.body.state, state: req.body.state,
}; };
const result = await newObject({ const result = await newObject({

View File

@ -1,5 +1,6 @@
import config from '../../config.js'; import config from '../../config.js';
import { orderItemModel } from '../../database/schemas/inventory/orderitem.schema.js'; import { orderItemModel } from '../../database/schemas/inventory/orderitem.schema.js';
import { getModelByName } from '../misc/model.js';
import log4js from 'log4js'; import log4js from 'log4js';
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { import {
@ -50,11 +51,39 @@ export const listOrderItemsRouteHandler = async (
}, },
{ {
path: 'item', path: 'item',
populate: { path: 'costTaxRate', strictPopulate: false }, populate: [
{ path: 'costTaxRate', strictPopulate: false },
{ path: 'priceTaxRate', strictPopulate: false },
],
}, },
{ {
path: 'item', path: 'sku',
populate: { path: 'priceTaxRate', strictPopulate: false }, strictPopulate: false,
populate: [
{
path: 'filament',
populate: { path: 'costTaxRate', strictPopulate: false },
strictPopulate: false,
},
{
path: 'part',
populate: [
{ path: 'costTaxRate', strictPopulate: false },
{ path: 'priceTaxRate', strictPopulate: false },
],
strictPopulate: false,
},
{
path: 'product',
populate: [
{ path: 'costTaxRate', strictPopulate: false },
{ path: 'priceTaxRate', strictPopulate: false },
],
strictPopulate: false,
},
{ path: 'costTaxRate', strictPopulate: false },
{ path: 'priceTaxRate', strictPopulate: false },
],
}, },
], ],
}); });
@ -109,13 +138,40 @@ export const getOrderItemRouteHandler = async (req, res) => {
}, },
{ {
path: 'item', path: 'item',
populate: { path: 'costTaxRate', strictPopulate: false }, populate: [
{ path: 'costTaxRate', strictPopulate: false },
{ path: 'priceTaxRate', strictPopulate: false },
],
strictPopulate: false, strictPopulate: false,
}, },
{ {
path: 'item', path: 'sku',
populate: { path: 'priceTaxRate', strictPopulate: false },
strictPopulate: false, strictPopulate: false,
populate: [
{
path: 'filament',
populate: { path: 'costTaxRate', strictPopulate: false },
strictPopulate: false,
},
{
path: 'part',
populate: [
{ path: 'costTaxRate', strictPopulate: false },
{ path: 'priceTaxRate', strictPopulate: false },
],
strictPopulate: false,
},
{
path: 'product',
populate: [
{ path: 'costTaxRate', strictPopulate: false },
{ path: 'priceTaxRate', strictPopulate: false },
],
strictPopulate: false,
},
{ path: 'costTaxRate', strictPopulate: false },
{ path: 'priceTaxRate', strictPopulate: false },
],
}, },
], ],
}); });
@ -133,11 +189,39 @@ export const editOrderItemRouteHandler = async (req, res) => {
logger.trace(`Order Item with ID: ${id}`); logger.trace(`Order Item with ID: ${id}`);
const skuType =
req.body.sku && req.body.itemType ? req.body.itemType + 'Sku' : null;
let name = req.body.name;
if (!name && req.body.sku && skuType) {
const skuEntry = getModelByName(skuType);
if (skuEntry?.model) {
const sku = await getObject({
model: skuEntry.model,
id: req.body.sku,
cached: true,
});
name = sku?.name;
}
}
if (!name && req.body.item && req.body.itemType) {
const itemEntry = getModelByName(req.body.itemType);
if (itemEntry?.model) {
const item = await getObject({
model: itemEntry.model,
id: req.body.item,
cached: true,
});
name = item?.name;
}
}
const updateData = { const updateData = {
updatedAt: new Date(), updatedAt: new Date(),
name: req.body.name, name: req.body.name ?? name,
itemType: req.body.itemType, itemType: req.body.itemType,
item: req.body.item, item: req.body.item,
sku: req.body.sku,
orderType: req.body.orderType, orderType: req.body.orderType,
order: req.body.order, order: req.body.order,
syncAmount: req.body.syncAmount, syncAmount: req.body.syncAmount,
@ -158,7 +242,7 @@ export const editOrderItemRouteHandler = async (req, res) => {
if (result.error) { if (result.error) {
logger.error('Error editing order item:', result.error); logger.error('Error editing order item:', result.error);
res.status(result).send(result); res.status(result.code || 500).send(result);
return; return;
} }
@ -173,13 +257,14 @@ export const editMultipleOrderItemsRouteHandler = async (req, res) => {
name: update.name, name: update.name,
itemType: update.itemType, itemType: update.itemType,
item: update.item, item: update.item,
sku: update.sku,
orderType: update.orderType, orderType: update.orderType,
order: update.order, order: update.order,
syncAmount: update.syncAmount, syncAmount: update.syncAmount,
itemAmount: update.itemAmount, itemAmount: update.itemAmount,
quantity: update.quantity, quantity: update.quantity,
totalAmount: update.totalAmount, totalAmount: update.totalAmount,
shipment: update.shipment, shipment: update.shipment,
taxRate: update.taxRate, taxRate: update.taxRate,
totalAmountWithTax: update.totalAmountWithTax, totalAmountWithTax: update.totalAmountWithTax,
})); }));
@ -206,13 +291,41 @@ export const editMultipleOrderItemsRouteHandler = async (req, res) => {
}; };
export const newOrderItemRouteHandler = async (req, res) => { export const newOrderItemRouteHandler = async (req, res) => {
const skuType =
req.body.sku && req.body.itemType ? req.body.itemType + 'Sku' : null;
let name = req.body.name;
if (!name && req.body.sku && skuType) {
const skuEntry = getModelByName(skuType);
if (skuEntry?.model) {
const sku = await getObject({
model: skuEntry.model,
id: req.body.sku,
cached: true,
});
name = sku?.name;
}
}
if (!name && req.body.item && req.body.itemType) {
const itemEntry = getModelByName(req.body.itemType);
if (itemEntry?.model) {
const item = await getObject({
model: itemEntry.model,
id: req.body.item,
cached: true,
});
name = item?.name;
}
}
const newData = { const newData = {
updatedAt: new Date(), updatedAt: new Date(),
name: req.body.name, name: name || 'Order Item',
purchaseOrder: req.body.purchaseOrder, purchaseOrder: req.body.purchaseOrder,
state: { type: 'draft' }, state: { type: 'draft' },
itemType: req.body.itemType, itemType: req.body.itemType,
item: req.body.item, item: req.body.item,
sku: req.body.sku,
orderType: req.body.orderType, orderType: req.body.orderType,
order: req.body.order, order: req.body.order,
syncAmount: req.body.syncAmount, syncAmount: req.body.syncAmount,

View File

@ -36,7 +36,7 @@ export const listPartStocksRouteHandler = async (
search, search,
sort, sort,
order, order,
populate: [{ path: 'part' }], populate: [{ path: 'partSku' }],
}); });
if (result?.error) { if (result?.error) {
@ -60,7 +60,7 @@ export const listPartStocksByPropertiesRouteHandler = async (
model: partStockModel, model: partStockModel,
properties, properties,
filter, filter,
populate: ['part'], populate: ['partSku'],
masterFilter, masterFilter,
}); });
@ -79,7 +79,7 @@ export const getPartStockRouteHandler = async (req, res) => {
const result = await getObject({ const result = await getObject({
model: partStockModel, model: partStockModel,
id, id,
populate: [{ path: 'part' }], populate: [{ path: 'partSku' }],
}); });
if (result?.error) { if (result?.error) {
logger.warn(`Part Stock not found with supplied id.`); logger.warn(`Part Stock not found with supplied id.`);
@ -146,7 +146,7 @@ export const newPartStockRouteHandler = async (req, res) => {
updatedAt: new Date(), updatedAt: new Date(),
startingQuantity: req.body.startingQuantity, startingQuantity: req.body.startingQuantity,
currentQuantity: req.body.currentQuantity, currentQuantity: req.body.currentQuantity,
part: req.body.part, partSku: req.body.partSku,
state: req.body.state, state: req.body.state,
}; };
const result = await newObject({ const result = await newObject({

View File

@ -14,7 +14,7 @@ import {
getModelHistory, getModelHistory,
checkStates, checkStates,
} from '../../database/database.js'; } from '../../database/database.js';
import { productModel } from '../../database/schemas/management/product.schema.js'; import { productSkuModel } from '../../database/schemas/management/productsku.schema.js';
const logger = log4js.getLogger('Product Stocks'); const logger = log4js.getLogger('Product Stocks');
logger.level = config.server.logLevel; logger.level = config.server.logLevel;
@ -38,7 +38,7 @@ export const listProductStocksRouteHandler = async (
search, search,
sort, sort,
order, order,
populate: [{ path: 'product' }, { path: 'partStocks.partStock' }], populate: [{ path: 'productSku' }, { path: 'partStocks.partStock' }],
}); });
if (result?.error) { if (result?.error) {
@ -62,7 +62,7 @@ export const listProductStocksByPropertiesRouteHandler = async (
model: productStockModel, model: productStockModel,
properties, properties,
filter, filter,
populate: ['product', 'partStocks.partStock'], populate: ['productSku', 'partStocks.partStock'],
masterFilter, masterFilter,
}); });
@ -81,7 +81,7 @@ export const getProductStockRouteHandler = async (req, res) => {
const result = await getObject({ const result = await getObject({
model: productStockModel, model: productStockModel,
id, id,
populate: [{ path: 'partStocks.part' }, { path: 'partStocks.partStock' }, { path: 'product' }], populate: [{ path: 'partStocks.partSku' }, { path: 'partStocks.partStock' }, { path: 'productSku' }],
}); });
if (result?.error) { if (result?.error) {
logger.warn(`Product Stock not found with supplied id.`); logger.warn(`Product Stock not found with supplied id.`);
@ -114,6 +114,7 @@ export const editProductStockRouteHandler = async (req, res) => {
partStocks: req.body?.partStocks?.map((partStock) => ({ partStocks: req.body?.partStocks?.map((partStock) => ({
quantity: partStock.quantity, quantity: partStock.quantity,
partStock: partStock.partStock, partStock: partStock.partStock,
partSku: partStock.partSku,
})), })),
}; };
@ -162,18 +163,18 @@ export const editMultipleProductStocksRouteHandler = async (req, res) => {
}; };
export const newProductStockRouteHandler = async (req, res) => { export const newProductStockRouteHandler = async (req, res) => {
const productId = new mongoose.Types.ObjectId(req.body.product?._id); const productSkuId = new mongoose.Types.ObjectId(req.body.productSku?._id);
const product = await getObject({ const productSku = await getObject({
model: productModel, model: productSkuModel,
id: productId, id: productSkuId,
}); });
const newData = { const newData = {
updatedAt: new Date(), updatedAt: new Date(),
currentQuantity: req.body.currentQuantity, currentQuantity: req.body.currentQuantity,
product: req.body.product, productSku: req.body.productSku,
state: req.body.state ?? { type: 'draft' }, state: req.body.state ?? { type: 'draft' },
partStocks: product.parts.map((part) => ({ partStocks: (productSku.parts || []).map((part) => ({
part: part.part, partSku: part.partSku,
quantity: part.quantity, quantity: part.quantity,
partStock: undefined, partStock: undefined,
})), })),

View File

@ -36,7 +36,7 @@ export const listFilamentsRouteHandler = async (
search, search,
sort, sort,
order, order,
populate: ['vendor', 'costTaxRate'], populate: ['costTaxRate', 'material'],
}); });
if (result?.error) { if (result?.error) {
@ -59,16 +59,7 @@ export const listFilamentsByPropertiesRouteHandler = async (
model: filamentModel, model: filamentModel,
properties, properties,
filter, filter,
populate: [ populate: [],
{
path: 'vendor',
from: 'vendors',
},
{
path: 'costTaxRate',
from: 'taxrates',
},
],
}); });
if (result?.error) { if (result?.error) {
@ -86,7 +77,7 @@ export const getFilamentRouteHandler = async (req, res) => {
const result = await getObject({ const result = await getObject({
model: filamentModel, model: filamentModel,
id, id,
populate: ['vendor', 'costTaxRate'], populate: ['costTaxRate', 'material'],
}); });
if (result?.error) { if (result?.error) {
logger.warn(`Filament not found with supplied id.`); logger.warn(`Filament not found with supplied id.`);
@ -108,15 +99,13 @@ export const editFilamentRouteHandler = async (req, res) => {
barcode: req.body.barcode, barcode: req.body.barcode,
url: req.body.url, url: req.body.url,
image: req.body.image, image: req.body.image,
color: req.body.color, material: req.body.material?._id ?? req.body.material,
vendor: req.body.vendor,
type: req.body.type,
cost: req.body.cost,
costTaxRate: req.body.costTaxRate,
costWithTax: req.body.costWithTax,
diameter: req.body.diameter, diameter: req.body.diameter,
density: req.body.density, density: req.body.density,
emptySpoolWeight: req.body.emptySpoolWeight, emptySpoolWeight: req.body.emptySpoolWeight,
cost: req.body?.cost,
costTaxRate: req.body?.costTaxRate,
costWithTax: req.body?.costWithTax,
}; };
const result = await editObject({ const result = await editObject({
model: filamentModel, model: filamentModel,
@ -143,15 +132,13 @@ export const editMultipleFilamentsRouteHandler = async (req, res) => {
barcode: update.barcode, barcode: update.barcode,
url: update.url, url: update.url,
image: update.image, image: update.image,
color: update.color, material: update.material?._id ?? update.material,
vendor: update.vendor,
type: update.type,
cost: update.cost,
costTaxRate: update.costTaxRate,
costWithTax: update.costWithTax,
diameter: update.diameter, diameter: update.diameter,
density: update.density, density: update.density,
emptySpoolWeight: update.emptySpoolWeight, emptySpoolWeight: update.emptySpoolWeight,
cost: update.cost,
costTaxRate: update.costTaxRate,
costWithTax: update.costWithTax,
})); }));
if (!Array.isArray(updates)) { if (!Array.isArray(updates)) {
@ -183,15 +170,13 @@ export const newFilamentRouteHandler = async (req, res) => {
barcode: req.body.barcode, barcode: req.body.barcode,
url: req.body.url, url: req.body.url,
image: req.body.image, image: req.body.image,
color: req.body.color, material: req.body.material?._id ?? req.body.material,
vendor: req.body.vendor,
type: req.body.type,
cost: req.body.cost,
costTaxRate: req.body.costTaxRate,
costWithTax: req.body.costWithTax,
diameter: req.body.diameter, diameter: req.body.diameter,
density: req.body.density, density: req.body.density,
emptySpoolWeight: req.body.emptySpoolWeight, emptySpoolWeight: req.body.emptySpoolWeight,
cost: req.body?.cost,
costTaxRate: req.body?.costTaxRate,
costWithTax: req.body?.costWithTax,
}; };
const result = await newObject({ const result = await newObject({

View File

@ -0,0 +1,196 @@
import config from '../../config.js';
import { filamentSkuModel } from '../../database/schemas/management/filamentsku.schema.js';
import log4js from 'log4js';
import mongoose from 'mongoose';
import {
deleteObject,
listObjects,
getObject,
editObject,
newObject,
listObjectsByProperties,
getModelStats,
getModelHistory,
} from '../../database/database.js';
const logger = log4js.getLogger('Filament SKUs');
logger.level = config.server.logLevel;
export const listFilamentSkusRouteHandler = async (
req,
res,
page = 1,
limit = 25,
property = '',
filter = {},
search = '',
sort = '',
order = 'ascend'
) => {
const result = await listObjects({
model: filamentSkuModel,
page,
limit,
property,
filter,
search,
sort,
order,
populate: ['costTaxRate'],
});
if (result?.error) {
logger.error('Error listing filament SKUs.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of filament SKUs (Page ${page}, Limit ${limit}). Count: ${result.length}.`);
res.send(result);
};
export const listFilamentSkusByPropertiesRouteHandler = async (
req,
res,
properties = '',
filter = {},
masterFilter = {}
) => {
const result = await listObjectsByProperties({
model: filamentSkuModel,
properties,
filter,
populate: ['costTaxRate'],
masterFilter,
});
if (result?.error) {
logger.error('Error listing filament SKUs.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of filament SKUs. Count: ${result.length}`);
res.send(result);
};
export const getFilamentSkuRouteHandler = async (req, res) => {
const id = req.params.id;
const result = await getObject({
model: filamentSkuModel,
id,
populate: [{ path: 'filament', populate: 'costTaxRate' }, 'costTaxRate'],
});
if (result?.error) {
logger.warn(`Filament SKU not found with supplied id.`);
return res.status(result.code).send(result);
}
logger.debug(`Retrieved filament SKU with ID: ${id}`);
res.send(result);
};
export const editFilamentSkuRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Filament SKU with ID: ${id}`);
const overrideCost = req.body?.overrideCost;
const updateData = {
updatedAt: new Date(),
barcode: req.body?.barcode,
filament: req.body?.filament,
name: req.body?.name,
description: req.body?.description,
color: req.body?.color,
cost: overrideCost ? req.body?.cost : null,
overrideCost,
costTaxRate: overrideCost ? req.body?.costTaxRate : null,
costWithTax: overrideCost ? req.body?.costWithTax : null,
};
const result = await editObject({
model: filamentSkuModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error editing filament SKU:', result.error);
res.status(result.code || 500).send(result);
return;
}
logger.debug(`Edited filament SKU with ID: ${id}`);
res.send(result);
};
export const newFilamentSkuRouteHandler = async (req, res) => {
const overrideCost = req.body?.overrideCost;
const newData = {
barcode: req.body?.barcode,
filament: req.body?.filament,
name: req.body?.name,
description: req.body?.description,
color: req.body?.color,
cost: overrideCost ? req.body?.cost : null,
overrideCost,
costTaxRate: overrideCost ? req.body?.costTaxRate : null,
costWithTax: overrideCost ? req.body?.costWithTax : null,
};
const result = await newObject({
model: filamentSkuModel,
newData,
user: req.user,
});
if (result.error) {
logger.error('No filament SKU created:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`New filament SKU with ID: ${result._id}`);
res.send(result);
};
export const deleteFilamentSkuRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Filament SKU with ID: ${id}`);
const result = await deleteObject({
model: filamentSkuModel,
id,
user: req.user,
});
if (result.error) {
logger.error('No filament SKU deleted:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`Deleted filament SKU with ID: ${id}`);
res.send(result);
};
export const getFilamentSkuStatsRouteHandler = async (req, res) => {
const result = await getModelStats({ model: filamentSkuModel });
if (result?.error) {
logger.error('Error fetching filament SKU stats:', result.error);
return res.status(result.code).send(result);
}
logger.trace('Filament SKU stats:', result);
res.send(result);
};
export const getFilamentSkuHistoryRouteHandler = async (req, res) => {
const from = req.query.from;
const to = req.query.to;
const result = await getModelHistory({ model: filamentSkuModel, from, to });
if (result?.error) {
logger.error('Error fetching filament SKU history:', result.error);
return res.status(result.code).send(result);
}
logger.trace('Filament SKU history:', result);
res.send(result);
};

View File

@ -2,7 +2,16 @@ import config from '../../config.js';
import { materialModel } from '../../database/schemas/management/material.schema.js'; import { materialModel } from '../../database/schemas/management/material.schema.js';
import log4js from 'log4js'; import log4js from 'log4js';
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { getModelStats, getModelHistory } from '../../database/database.js'; import {
getObject,
listObjects,
listObjectsByProperties,
editObject,
newObject,
getModelStats,
getModelHistory,
} from '../../database/database.js';
const logger = log4js.getLogger('Materials'); const logger = log4js.getLogger('Materials');
logger.level = config.server.logLevel; logger.level = config.server.logLevel;
@ -12,118 +21,121 @@ export const listMaterialsRouteHandler = async (
page = 1, page = 1,
limit = 25, limit = 25,
property = '', property = '',
filter = {},
search = '',
sort = '',
order = 'ascend'
) => {
const result = await listObjects({
model: materialModel,
page,
limit,
property,
filter,
search,
sort,
order,
populate: [],
});
if (result?.error) {
logger.error('Error listing materials.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of materials (Page ${page}, Limit ${limit}). Count: ${result.length}`);
res.send(result);
};
export const listMaterialsByPropertiesRouteHandler = async (
req,
res,
properties = [],
filter = {} filter = {}
) => { ) => {
try { const result = await listObjectsByProperties({
// Calculate the skip value based on the page number and limit model: materialModel,
const skip = (page - 1) * limit; properties,
filter,
populate: [],
});
let material; if (result?.error) {
let aggregateCommand = []; logger.error('Error listing materials.');
res.status(result.code).send(result);
if (filter != {}) { return;
// use filtering if present
aggregateCommand.push({ $match: filter });
}
if (property != '') {
aggregateCommand.push({ $group: { _id: `$${property}` } }); // group all same properties
aggregateCommand.push({ $project: { _id: 0, [property]: '$_id' } }); // rename _id to the property name
} else {
aggregateCommand.push({ $project: { image: 0, url: 0 } });
}
aggregateCommand.push({ $skip: skip });
aggregateCommand.push({ $limit: Number(limit) });
material = await materialModel.aggregate(aggregateCommand);
logger.trace(
`List of materials (Page ${page}, Limit ${limit}, Property ${property}):`,
material
);
res.send(material);
} catch (error) {
logger.error('Error listing materials:', error);
res.status(500).send({ error: error });
} }
logger.debug(`List of materials. Count: ${result.length}`);
res.send(result);
}; };
export const getMaterialRouteHandler = async (req, res) => { export const getMaterialRouteHandler = async (req, res) => {
try { const id = req.params.id;
// Get ID from params const result = await getObject({
const id = new mongoose.Types.ObjectId(req.params.id); model: materialModel,
// Fetch the material with the given remote address id,
const material = await materialModel.findOne({ populate: [],
_id: id, });
}); if (result?.error) {
logger.warn(`Material not found with supplied id.`);
if (!material) { return res.status(result.code).send(result);
logger.warn(`Material not found with supplied id.`);
return res.status(404).send({ error: 'Print job not found.' });
}
logger.trace(`Material with ID: ${id}:`, material);
res.send(material);
} catch (error) {
logger.error('Error fetching Material:', error);
res.status(500).send({ error: error.message });
} }
logger.debug(`Retrieved material with ID: ${id}`);
res.send(result);
}; };
export const editMaterialRouteHandler = async (req, res) => { export const editMaterialRouteHandler = async (req, res) => {
try { const id = new mongoose.Types.ObjectId(req.params.id);
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
// Fetch the material with the given remote address
const material = await materialModel.findOne({ _id: id });
if (!material) { logger.trace(`Material with ID: ${id}`);
// Error handling
logger.warn(`Material not found with supplied id.`);
return res.status(404).send({ error: 'Print job not found.' });
}
logger.trace(`Material with ID: ${id}:`, material); const updateData = {
updatedAt: new Date(),
name: req.body.name,
url: req.body.url,
tags: req.body.tags,
};
try { const result = await editObject({
const updateData = req.body; model: materialModel,
id,
updateData,
user: req.user,
});
const result = await materialModel.updateOne({ _id: id }, { $set: updateData }); if (result.error) {
if (result.nModified === 0) { logger.error('Error editing material:', result.error);
logger.error('No Material updated.'); res.status(result.code).send(result);
res.status(500).send({ error: 'No materials updated.' }); return;
}
} catch (updateError) {
logger.error('Error updating material:', updateError);
res.status(500).send({ error: updateError.message });
}
res.send('OK');
} catch (fetchError) {
logger.error('Error fetching material:', fetchError);
res.status(500).send({ error: fetchError.message });
} }
logger.debug(`Edited material with ID: ${id}`);
res.send(result);
}; };
export const newMaterialRouteHandler = async (req, res) => { export const newMaterialRouteHandler = async (req, res) => {
try { const newData = {
let { ...newMaterial } = req.body; createdAt: new Date(),
newMaterial = { updatedAt: new Date(),
...newMaterial, name: req.body.name,
createdAt: new Date(), url: req.body.url,
updatedAt: new Date(), tags: req.body.tags,
}; };
const result = await materialModel.create(newMaterial); const result = await newObject({
if (result.nCreated === 0) { model: materialModel,
logger.error('No material created.'); newData,
res.status(500).send({ error: 'No material created.' }); user: req.user,
} });
res.status(200).send({ status: 'ok' }); if (result.error) {
} catch (updateError) { logger.error('No material created:', result.error);
logger.error('Error updating material:', updateError); return res.status(result.code).send(result);
res.status(500).send({ error: updateError.message });
} }
logger.debug(`New material with ID: ${result._id}`);
res.send(result);
}; };
export const getMaterialStatsRouteHandler = async (req, res) => { export const getMaterialStatsRouteHandler = async (req, res) => {

View File

@ -35,7 +35,7 @@ export const listPartsRouteHandler = async (
search, search,
sort, sort,
order, order,
populate: ['vendor'], populate: ['costTaxRate', 'priceTaxRate'],
}); });
if (result?.error) { if (result?.error) {
@ -53,20 +53,7 @@ export const listPartsByPropertiesRouteHandler = async (req, res, properties = '
model: partModel, model: partModel,
properties, properties,
filter, filter,
populate: [ populate: [],
{
path: 'vendor',
from: 'vendors',
},
{
path: 'priceTaxRate',
from: 'taxrates',
},
{
path: 'costTaxRate',
from: 'taxrates',
},
],
}); });
if (result?.error) { if (result?.error) {
@ -84,7 +71,7 @@ export const getPartRouteHandler = async (req, res) => {
const result = await getObject({ const result = await getObject({
model: partModel, model: partModel,
id, id,
populate: ['vendor', 'priceTaxRate', 'costTaxRate'], populate: ['costTaxRate', 'priceTaxRate'],
}); });
if (result?.error) { if (result?.error) {
logger.warn(`Part not found with supplied id.`); logger.warn(`Part not found with supplied id.`);
@ -103,16 +90,16 @@ export const editPartRouteHandler = async (req, res) => {
const updateData = { const updateData = {
updatedAt: new Date(), updatedAt: new Date(),
name: req.body?.name, name: req.body?.name,
fileName: req.body?.fileName,
file: req.body?.file, file: req.body?.file,
vendor: req.body?.vendor,
margin: req.body?.margin,
price: req.body?.price,
cost: req.body?.cost, cost: req.body?.cost,
price: req.body?.price,
priceMode: req.body?.priceMode, priceMode: req.body?.priceMode,
priceTaxRate: req.body?.priceTaxRate, margin: req.body?.margin,
costTaxRate: req.body?.costTaxRate, costTaxRate: req.body?.costTaxRate,
priceWithTax: req.body?.priceWithTax, priceTaxRate: req.body?.priceTaxRate,
costWithTax: req.body?.costWithTax, costWithTax: req.body?.costWithTax,
priceWithTax: req.body?.priceWithTax,
}; };
// Create audit log before updating // Create audit log before updating
const result = await editObject({ const result = await editObject({
@ -137,16 +124,16 @@ export const newPartRouteHandler = async (req, res) => {
const newData = { const newData = {
updatedAt: new Date(), updatedAt: new Date(),
name: req.body?.name, name: req.body?.name,
fileName: req.body?.fileName,
file: req.body?.file, file: req.body?.file,
vendor: req.body?.vendor,
margin: req.body?.margin,
price: req.body?.price,
cost: req.body?.cost, cost: req.body?.cost,
price: req.body?.price,
priceMode: req.body?.priceMode, priceMode: req.body?.priceMode,
priceTaxRate: req.body?.priceTaxRate, margin: req.body?.margin,
costTaxRate: req.body?.costTaxRate, costTaxRate: req.body?.costTaxRate,
priceWithTax: req.body?.priceWithTax, priceTaxRate: req.body?.priceTaxRate,
costWithTax: req.body?.costWithTax, costWithTax: req.body?.costWithTax,
priceWithTax: req.body?.priceWithTax,
}; };
const result = await newObject({ const result = await newObject({

View File

@ -0,0 +1,212 @@
import config from '../../config.js';
import { partSkuModel } from '../../database/schemas/management/partsku.schema.js';
import log4js from 'log4js';
import mongoose from 'mongoose';
import {
deleteObject,
listObjects,
getObject,
editObject,
newObject,
listObjectsByProperties,
getModelStats,
getModelHistory,
} from '../../database/database.js';
const logger = log4js.getLogger('Part SKUs');
logger.level = config.server.logLevel;
export const listPartSkusRouteHandler = async (
req,
res,
page = 1,
limit = 25,
property = '',
filter = {},
search = '',
sort = '',
order = 'ascend'
) => {
const result = await listObjects({
model: partSkuModel,
page,
limit,
property,
filter,
search,
sort,
order,
populate: ['priceTaxRate', 'costTaxRate'],
});
if (result?.error) {
logger.error('Error listing part SKUs.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of part SKUs (Page ${page}, Limit ${limit}). Count: ${result.length}.`);
res.send(result);
};
export const listPartSkusByPropertiesRouteHandler = async (
req,
res,
properties = '',
filter = {},
masterFilter = {}
) => {
const result = await listObjectsByProperties({
model: partSkuModel,
properties,
filter,
populate: ['priceTaxRate', 'costTaxRate'],
masterFilter,
});
if (result?.error) {
logger.error('Error listing part SKUs.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of part SKUs. Count: ${result.length}`);
res.send(result);
};
export const getPartSkuRouteHandler = async (req, res) => {
const id = req.params.id;
const result = await getObject({
model: partSkuModel,
id,
populate: [
{ path: 'part', populate: ['costTaxRate', 'priceTaxRate'] },
'priceTaxRate',
'costTaxRate',
],
});
if (result?.error) {
logger.warn(`Part SKU not found with supplied id.`);
return res.status(result.code).send(result);
}
logger.debug(`Retrieved part SKU with ID: ${id}`);
res.send(result);
};
export const editPartSkuRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Part SKU with ID: ${id}`);
const overrideCost = req.body?.overrideCost;
const overridePrice = req.body?.overridePrice;
const updateData = {
updatedAt: new Date(),
barcode: req.body?.barcode,
part: req.body?.part,
name: req.body?.name,
description: req.body?.description,
priceMode: req.body?.priceMode,
price: overridePrice ? req.body?.price : null,
cost: overrideCost ? req.body?.cost : null,
overrideCost,
overridePrice,
margin: overridePrice ? req.body?.margin : null,
priceTaxRate: overridePrice ? req.body?.priceTaxRate : null,
costTaxRate: overrideCost ? req.body?.costTaxRate : null,
priceWithTax: overridePrice ? req.body?.priceWithTax : null,
costWithTax: overrideCost ? req.body?.costWithTax : null,
};
const result = await editObject({
model: partSkuModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error editing part SKU:', result.error);
res.status(result.code || 500).send(result);
return;
}
logger.debug(`Edited part SKU with ID: ${id}`);
res.send(result);
};
export const newPartSkuRouteHandler = async (req, res) => {
const overrideCost = req.body?.overrideCost;
const overridePrice = req.body?.overridePrice;
const newData = {
barcode: req.body?.barcode,
part: req.body?.part,
name: req.body?.name,
description: req.body?.description,
priceMode: req.body?.priceMode,
price: overridePrice ? req.body?.price : null,
cost: overrideCost ? req.body?.cost : null,
overrideCost,
overridePrice,
margin: overridePrice ? req.body?.margin : null,
priceTaxRate: overridePrice ? req.body?.priceTaxRate : null,
costTaxRate: overrideCost ? req.body?.costTaxRate : null,
priceWithTax: overridePrice ? req.body?.priceWithTax : null,
costWithTax: overrideCost ? req.body?.costWithTax : null,
};
const result = await newObject({
model: partSkuModel,
newData,
user: req.user,
});
if (result.error) {
logger.error('No part SKU created:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`New part SKU with ID: ${result._id}`);
res.send(result);
};
export const deletePartSkuRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Part SKU with ID: ${id}`);
const result = await deleteObject({
model: partSkuModel,
id,
user: req.user,
});
if (result.error) {
logger.error('No part SKU deleted:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`Deleted part SKU with ID: ${id}`);
res.send(result);
};
export const getPartSkuStatsRouteHandler = async (req, res) => {
const result = await getModelStats({ model: partSkuModel });
if (result?.error) {
logger.error('Error fetching part SKU stats:', result.error);
return res.status(result.code).send(result);
}
logger.trace('Part SKU stats:', result);
res.send(result);
};
export const getPartSkuHistoryRouteHandler = async (req, res) => {
const from = req.query.from;
const to = req.query.to;
const result = await getModelHistory({ model: partSkuModel, from, to });
if (result?.error) {
logger.error('Error fetching part SKU history:', result.error);
return res.status(result.code).send(result);
}
logger.trace('Part SKU history:', result);
res.send(result);
};

View File

@ -35,7 +35,7 @@ export const listProductsRouteHandler = async (
search, search,
sort, sort,
order, order,
populate: ['vendor'], populate: ['vendor', 'costTaxRate', 'priceTaxRate'],
}); });
if (result?.error) { if (result?.error) {
@ -76,7 +76,7 @@ export const getProductRouteHandler = async (req, res) => {
const result = await getObject({ const result = await getObject({
model: productModel, model: productModel,
id, id,
populate: ['vendor', 'parts.part'], populate: ['vendor', 'costTaxRate', 'priceTaxRate'],
}); });
if (result?.error) { if (result?.error) {
logger.warn(`Product not found with supplied id.`); logger.warn(`Product not found with supplied id.`);
@ -97,11 +97,15 @@ export const editProductRouteHandler = async (req, res) => {
name: req.body?.name, name: req.body?.name,
tags: req.body?.tags, tags: req.body?.tags,
version: req.body?.version, version: req.body?.version,
margin: req.body.margin,
amount: req.body.amount,
priceMode: req.body.priceMode,
vendor: req.body.vendor, vendor: req.body.vendor,
parts: req.body.parts, cost: req.body?.cost,
price: req.body?.price,
priceMode: req.body?.priceMode,
margin: req.body?.margin,
costTaxRate: req.body?.costTaxRate,
priceTaxRate: req.body?.priceTaxRate,
costWithTax: req.body?.costWithTax,
priceWithTax: req.body?.priceWithTax,
}; };
// Create audit log before updating // Create audit log before updating
const result = await editObject({ const result = await editObject({
@ -128,11 +132,15 @@ export const newProductRouteHandler = async (req, res) => {
name: req.body?.name, name: req.body?.name,
tags: req.body?.tags, tags: req.body?.tags,
version: req.body?.version, version: req.body?.version,
margin: req.body.margin,
amount: req.body.amount,
priceMode: req.body.priceMode,
vendor: req.body.vendor, vendor: req.body.vendor,
parts: req.body.parts, cost: req.body?.cost,
price: req.body?.price,
priceMode: req.body?.priceMode,
margin: req.body?.margin,
costTaxRate: req.body?.costTaxRate,
priceTaxRate: req.body?.priceTaxRate,
costWithTax: req.body?.costWithTax,
priceWithTax: req.body?.priceWithTax,
}; };
const result = await newObject({ const result = await newObject({

View File

@ -35,7 +35,7 @@ export const listProductSkusRouteHandler = async (
search, search,
sort, sort,
order, order,
populate: ['product'], populate: ['priceTaxRate', 'costTaxRate'],
}); });
if (result?.error) { if (result?.error) {
@ -59,7 +59,7 @@ export const listProductSkusByPropertiesRouteHandler = async (
model: productSkuModel, model: productSkuModel,
properties, properties,
filter, filter,
populate: ['product'], populate: ['priceTaxRate', 'costTaxRate'],
masterFilter, masterFilter,
}); });
@ -78,7 +78,12 @@ export const getProductSkuRouteHandler = async (req, res) => {
const result = await getObject({ const result = await getObject({
model: productSkuModel, model: productSkuModel,
id, id,
populate: ['product'], populate: [
{ path: 'product', populate: ['costTaxRate', 'priceTaxRate'] },
'priceTaxRate',
'costTaxRate',
'parts.partSku',
],
}); });
if (result?.error) { if (result?.error) {
logger.warn(`Product SKU not found with supplied id.`); logger.warn(`Product SKU not found with supplied id.`);
@ -93,12 +98,26 @@ export const editProductSkuRouteHandler = async (req, res) => {
logger.trace(`Product SKU with ID: ${id}`); logger.trace(`Product SKU with ID: ${id}`);
const overrideCost = req.body?.overrideCost;
const overridePrice = req.body?.overridePrice;
const updateData = { const updateData = {
updatedAt: new Date(), updatedAt: new Date(),
sku: req.body?.sku, barcode: req.body?.barcode,
product: req.body?.product, product: req.body?.product,
name: req.body?.name, name: req.body?.name,
description: req.body?.description, description: req.body?.description,
priceMode: req.body?.priceMode,
price: overridePrice ? req.body?.price : null,
cost: overrideCost ? req.body?.cost : null,
overrideCost,
overridePrice,
margin: overridePrice ? req.body?.margin : null,
parts: req.body?.parts,
priceTaxRate: overridePrice ? req.body?.priceTaxRate : null,
costTaxRate: overrideCost ? req.body?.costTaxRate : null,
priceWithTax: overridePrice ? req.body?.priceWithTax : null,
costWithTax: overrideCost ? req.body?.costWithTax : null,
}; };
const result = await editObject({ const result = await editObject({
@ -119,11 +138,25 @@ export const editProductSkuRouteHandler = async (req, res) => {
}; };
export const newProductSkuRouteHandler = async (req, res) => { export const newProductSkuRouteHandler = async (req, res) => {
const overrideCost = req.body?.overrideCost;
const overridePrice = req.body?.overridePrice;
const newData = { const newData = {
sku: req.body?.sku, barcode: req.body?.barcode,
product: req.body?.product, product: req.body?.product,
name: req.body?.name, name: req.body?.name,
description: req.body?.description, description: req.body?.description,
priceMode: req.body?.priceMode,
price: overridePrice ? req.body?.price : null,
cost: overrideCost ? req.body?.cost : null,
overrideCost,
overridePrice,
margin: overridePrice ? req.body?.margin : null,
parts: req.body?.parts,
priceTaxRate: overridePrice ? req.body?.priceTaxRate : null,
costTaxRate: overrideCost ? req.body?.costTaxRate : null,
priceWithTax: overridePrice ? req.body?.priceWithTax : null,
costWithTax: overrideCost ? req.body?.costWithTax : null,
}; };
const result = await newObject({ const result = await newObject({

View File

@ -4,96 +4,11 @@ import { getModelByName } from './model.js';
import { listObjectsOData } from '../../database/odata.js'; import { listObjectsOData } from '../../database/odata.js';
import { getFilter } from '../../utils.js'; import { getFilter } from '../../utils.js';
import { generateCsvTable } from '../../database/csv.js'; import { generateCsvTable } from '../../database/csv.js';
import { getModelFilterFields, parseOrderBy, rowToFlat } from './export.js';
const logger = log4js.getLogger('CSV'); const logger = log4js.getLogger('CSV');
logger.level = config.server.logLevel; logger.level = config.server.logLevel;
/**
* Flatten nested objects for CSV display.
* Objects become "key.subkey: value" or JSON string; arrays become comma-separated.
*/
function flattenForCsv(obj, prefix = '') {
if (obj === null || obj === undefined) return {};
if (typeof obj !== 'object') return { [prefix]: obj };
if (obj instanceof Date) return { [prefix]: obj };
if (Array.isArray(obj)) {
const str = obj
.map((v) => (v && typeof v === 'object' && !(v instanceof Date) ? JSON.stringify(v) : v))
.join(', ');
return { [prefix]: str };
}
const result = {};
for (const [k, v] of Object.entries(obj)) {
const key = prefix ? `${prefix}.${k}` : k;
if (v !== null && typeof v === 'object' && !(v instanceof Date) && !Array.isArray(v)) {
Object.assign(result, flattenForCsv(v, key));
} else {
result[key] = v;
}
}
return result;
}
/**
* Convert a row to flat key-value for CSV. Nested objects are flattened.
*/
function rowToFlat(row) {
const flat = {};
for (const [key, val] of Object.entries(row)) {
if (key.startsWith('@')) continue;
if (val !== null && typeof val === 'object' && !(val instanceof Date) && !Array.isArray(val)) {
Object.assign(flat, flattenForCsv(val, key));
} else {
flat[key] = val;
}
}
return flat;
}
/**
* Get allowed filter fields for CSV export (reuse OData logic).
*/
function getModelFilterFields(objectType) {
const base = ['_id'];
const byType = {
note: ['parent._id', 'noteType', 'user'],
notification: ['user'],
userNotifier: ['user', 'object', 'objectType'],
printer: ['host'],
job: ['printer', 'gcodeFile'],
subJob: ['job'],
filamentStock: ['filament'],
partStock: ['part'],
productStock: ['product'],
productSku: ['product'],
purchaseOrder: ['vendor'],
orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'],
shipment: ['order._id', 'orderType', 'courierService._id'],
stockEvent: ['parent._id', 'parentType', 'owner._id', 'ownerType'],
stockAudit: ['filamentStock._id', 'partStock._id'],
documentJob: ['documentTemplate', 'documentPrinter', 'object._id', 'objectType'],
documentTemplate: ['parent._id', 'documentSize._id'],
salesOrder: ['client'],
invoice: ['to._id', 'from._id', 'order._id', 'orderType'],
auditLog: ['parent._id', 'parentType', 'owner._id', 'ownerType'],
appPassword: ['name', 'user', 'active'],
};
const extra = byType[objectType] || [];
return [...base, ...extra];
}
function parseOrderBy(orderby) {
if (!orderby || typeof orderby !== 'string') {
return { sort: 'createdAt', order: 'ascend' };
}
const trimmed = orderby.trim();
const parts = trimmed.split(/\s+/);
const sort = parts[0] || 'createdAt';
const dir = (parts[1] || 'asc').toLowerCase();
const order = dir === 'desc' ? 'descend' : 'ascend';
return { sort, order };
}
/** /**
* Generate CSV file for the given object type. * Generate CSV file for the given object type.
* @param {Object} options * @param {Object} options

View File

@ -10,96 +10,11 @@ import {
incrementExcelTempRequestCount, incrementExcelTempRequestCount,
deleteExcelTempToken, deleteExcelTempToken,
} from '../../database/excel.js'; } from '../../database/excel.js';
import { getModelFilterFields, parseOrderBy, rowToFlat } from './export.js';
const logger = log4js.getLogger('Excel'); const logger = log4js.getLogger('Excel');
logger.level = config.server.logLevel; logger.level = config.server.logLevel;
/**
* Flatten nested objects for Excel display.
* Objects become "key.subkey: value" or JSON string; arrays become comma-separated.
*/
function flattenForExcel(obj, prefix = '') {
if (obj === null || obj === undefined) return {};
if (typeof obj !== 'object') return { [prefix]: obj };
if (obj instanceof Date) return { [prefix]: obj };
if (Array.isArray(obj)) {
const str = obj
.map((v) => (v && typeof v === 'object' && !(v instanceof Date) ? JSON.stringify(v) : v))
.join(', ');
return { [prefix]: str };
}
const result = {};
for (const [k, v] of Object.entries(obj)) {
const key = prefix ? `${prefix}.${k}` : k;
if (v !== null && typeof v === 'object' && !(v instanceof Date) && !Array.isArray(v)) {
Object.assign(result, flattenForExcel(v, key));
} else {
result[key] = v;
}
}
return result;
}
/**
* Convert a row to flat key-value for Excel. Nested objects are flattened.
*/
function rowToFlat(row) {
const flat = {};
for (const [key, val] of Object.entries(row)) {
if (key.startsWith('@')) continue;
if (val !== null && typeof val === 'object' && !(val instanceof Date) && !Array.isArray(val)) {
Object.assign(flat, flattenForExcel(val, key));
} else {
flat[key] = val;
}
}
return flat;
}
/**
* Get allowed filter fields for Excel export (reuse OData logic).
*/
function getModelFilterFields(objectType) {
const base = ['_id'];
const byType = {
note: ['parent._id', 'noteType', 'user'],
notification: ['user'],
userNotifier: ['user', 'object', 'objectType'],
printer: ['host'],
job: ['printer', 'gcodeFile'],
subJob: ['job'],
filamentStock: ['filament'],
partStock: ['part'],
productStock: ['product'],
productSku: ['product'],
purchaseOrder: ['vendor'],
orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'],
shipment: ['order._id', 'orderType', 'courierService._id'],
stockEvent: ['parent._id', 'parentType', 'owner._id', 'ownerType'],
stockAudit: ['filamentStock._id', 'partStock._id'],
documentJob: ['documentTemplate', 'documentPrinter', 'object._id', 'objectType'],
documentTemplate: ['parent._id', 'documentSize._id'],
salesOrder: ['client'],
invoice: ['to._id', 'from._id', 'order._id', 'orderType'],
auditLog: ['parent._id', 'parentType', 'owner._id', 'ownerType'],
appPassword: ['name', 'user', 'active'],
};
const extra = byType[objectType] || [];
return [...base, ...extra];
}
function parseOrderBy(orderby) {
if (!orderby || typeof orderby !== 'string') {
return { sort: 'createdAt', order: 'ascend' };
}
const trimmed = orderby.trim();
const parts = trimmed.split(/\s+/);
const sort = parts[0] || 'createdAt';
const dir = (parts[1] || 'asc').toLowerCase();
const order = dir === 'desc' ? 'descend' : 'ascend';
return { sort, order };
}
/** /**
* Generate Excel file for the given object type. * Generate Excel file for the given object type.
* @param {Object} options * @param {Object} options

110
src/services/misc/export.js Normal file
View File

@ -0,0 +1,110 @@
/**
* Shared export utilities for OData, CSV, and Excel.
* Centralizes filter fields, order-by parsing, and row flattening.
*/
/** Allowed filter fields per object type for OData/CSV/Excel exports */
export const EXPORT_FILTER_BY_TYPE = {
note: ['parent._id', 'noteType', 'user'],
notification: ['user'],
userNotifier: ['user', 'object', 'objectType'],
printer: ['host'],
job: ['printer', 'gcodeFile'],
subJob: ['job'],
filamentStock: ['filamentSku'],
filament: ['material', 'material._id', 'name', 'diameter', 'cost'],
filamentSku: ['filament', 'vendor', 'costTaxRate'],
material: ['name', 'tags'],
partStock: ['partSku'],
partSku: ['part', 'vendor', 'priceTaxRate', 'costTaxRate'],
productStock: ['productSku'],
productSku: ['product', 'vendor', 'priceTaxRate', 'costTaxRate'],
purchaseOrder: ['vendor'],
orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'sku._id', 'shipment._id'],
shipment: ['order._id', 'orderType', 'courierService._id'],
stockEvent: ['parent._id', 'parentType', 'owner._id', 'ownerType'],
stockAudit: ['filamentStock._id', 'partStock._id'],
documentJob: ['documentTemplate', 'documentPrinter', 'object._id', 'objectType'],
documentTemplate: ['parent._id', 'documentSize._id'],
salesOrder: ['client'],
invoice: ['to._id', 'from._id', 'order._id', 'orderType'],
auditLog: ['parent._id', 'parentType', 'owner._id', 'ownerType'],
appPassword: ['name', 'user', 'active'],
};
/**
* Get allowed filter fields for a given object type.
* @param {string} objectType - Model type (e.g. 'filament', 'material')
* @returns {string[]} Allowed filter field names
*/
export function getModelFilterFields(objectType) {
const base = ['_id'];
const extra = EXPORT_FILTER_BY_TYPE[objectType] || [];
return [...base, ...extra];
}
/**
* Parse OData $orderby or orderby string into sort and order.
* Supports "field asc", "field desc", or just "field" (defaults asc).
* @param {string} [orderby] - Orderby string (e.g. "createdAt desc")
* @returns {{ sort: string, order: 'ascend'|'descend' }}
*/
export function parseOrderBy(orderby) {
if (!orderby || typeof orderby !== 'string') {
return { sort: 'createdAt', order: 'ascend' };
}
const trimmed = orderby.trim();
const parts = trimmed.split(/\s+/);
const sort = parts[0] || 'createdAt';
const dir = (parts[1] || 'asc').toLowerCase();
const order = dir === 'desc' ? 'descend' : 'ascend';
return { sort, order };
}
/**
* Flatten nested objects for export display.
* Objects become "key.subkey: value"; arrays become comma-separated strings.
* @param {*} obj - Value to flatten
* @param {string} [prefix=''] - Key prefix for nested values
* @returns {Object} Flat key-value object
*/
export function flattenForExport(obj, prefix = '') {
if (obj === null || obj === undefined) return {};
if (typeof obj !== 'object') return { [prefix]: obj };
if (obj instanceof Date) return { [prefix]: obj };
if (Array.isArray(obj)) {
const str = obj
.map((v) => (v && typeof v === 'object' && !(v instanceof Date) ? JSON.stringify(v) : v))
.join(', ');
return { [prefix]: str };
}
const result = {};
for (const [k, v] of Object.entries(obj)) {
const key = prefix ? `${prefix}.${k}` : k;
if (v !== null && typeof v === 'object' && !(v instanceof Date) && !Array.isArray(v)) {
Object.assign(result, flattenForExport(v, key));
} else {
result[key] = v;
}
}
return result;
}
/**
* Convert a row (e.g. OData value item) to flat key-value for CSV/Excel.
* Nested objects are flattened; @odata.* keys are skipped.
* @param {Object} row - Row object
* @returns {Object} Flat key-value object
*/
export function rowToFlat(row) {
const flat = {};
for (const [key, val] of Object.entries(row)) {
if (key.startsWith('@')) continue;
if (val !== null && typeof val === 'object' && !(val instanceof Date) && !Array.isArray(val)) {
Object.assign(flat, flattenForExport(val, key));
} else {
flat[key] = val;
}
}
return flat;
}

View File

@ -4,6 +4,7 @@ import mongoose from 'mongoose';
import { getModelByName, getAllModels } from './model.js'; import { getModelByName, getAllModels } from './model.js';
import { listObjectsOData } from '../../database/odata.js'; import { listObjectsOData } from '../../database/odata.js';
import { getFilter } from '../../utils.js'; import { getFilter } from '../../utils.js';
import { getModelFilterFields, parseOrderBy } from './export.js';
const logger = log4js.getLogger('OData'); const logger = log4js.getLogger('OData');
logger.level = config.server.logLevel; logger.level = config.server.logLevel;
@ -253,22 +254,6 @@ export const metadataODataRouteHandler = (req, res) => {
res.send(xml); res.send(xml);
}; };
/**
* Parse OData $orderby into sort and order.
* Supports "field asc", "field desc", or just "field" (defaults asc).
*/
function parseOrderBy(orderby) {
if (!orderby || typeof orderby !== 'string') {
return { sort: 'createdAt', order: 'ascend' };
}
const trimmed = orderby.trim();
const parts = trimmed.split(/\s+/);
const sort = parts[0] || 'createdAt';
const dir = (parts[1] || 'asc').toLowerCase();
const order = dir === 'desc' ? 'descend' : 'ascend';
return { sort, order };
}
/** /**
* Route handler for GET /odata/:objectType * Route handler for GET /odata/:objectType
* Supports OData query options: $top, $skip, $orderby, $count, $select * Supports OData query options: $top, $skip, $orderby, $count, $select
@ -329,35 +314,3 @@ export const listODataRouteHandler = async (req, res) => {
res.send(result); res.send(result);
}; };
/**
* Return allowed filter fields for a given object type.
* Extends a base set with type-specific fields.
*/
function getModelFilterFields(objectType) {
const base = ['_id'];
const byType = {
note: ['parent._id', 'noteType', 'user'],
notification: ['user'],
userNotifier: ['user', 'object', 'objectType'],
printer: ['host'],
job: ['printer', 'gcodeFile'],
subJob: ['job'],
filamentStock: ['filament'],
partStock: ['part'],
productStock: ['product'],
productSku: ['product'],
purchaseOrder: ['vendor'],
orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'],
shipment: ['order._id', 'orderType', 'courierService._id'],
stockEvent: ['parent._id', 'parentType', 'owner._id', 'ownerType'],
stockAudit: ['filamentStock._id', 'partStock._id'],
documentJob: ['documentTemplate', 'documentPrinter', 'object._id', 'objectType'],
documentTemplate: ['parent._id', 'documentSize._id'],
salesOrder: ['client'],
invoice: ['to._id', 'from._id', 'order._id', 'orderType'],
auditLog: ['parent._id', 'parentType', 'owner._id', 'ownerType'],
appPassword: ['name', 'user', 'active'],
};
const extra = byType[objectType] || [];
return [...base, ...extra];
}

View File

@ -35,7 +35,7 @@ export const listGCodeFilesRouteHandler = async (
search, search,
sort, sort,
order, order,
populate: ['filament'], populate: ['filamentSku'],
}); });
if (result?.error) { if (result?.error) {
@ -58,7 +58,7 @@ export const listGCodeFilesByPropertiesRouteHandler = async (
model: gcodeFileModel, model: gcodeFileModel,
properties, properties,
filter, filter,
populate: 'filament', populate: 'filamentSku',
}); });
if (result?.error) { if (result?.error) {
@ -76,7 +76,7 @@ export const getGCodeFileRouteHandler = async (req, res) => {
const result = await getObject({ const result = await getObject({
model: gcodeFileModel, model: gcodeFileModel,
id, id,
populate: ['filament', 'parts.part'], populate: ['filamentSku', 'parts.partSku'],
}); });
if (result?.error) { if (result?.error) {
logger.warn(`GCodeFile not found with supplied id.`); logger.warn(`GCodeFile not found with supplied id.`);
@ -113,7 +113,7 @@ export const editGCodeFileRouteHandler = async (req, res) => {
updatedAt: new Date(), updatedAt: new Date(),
name: req.body.name, name: req.body.name,
file: req.body.file, file: req.body.file,
filament: req.body.filament, filamentSku: req.body.filamentSku,
parts: req.body.parts, parts: req.body.parts,
}; };
// Create audit log before updating // Create audit log before updating
@ -140,7 +140,7 @@ export const newGCodeFileRouteHandler = async (req, res) => {
updatedAt: new Date(), updatedAt: new Date(),
name: req.body.name, name: req.body.name,
file: req.body.file, file: req.body.file,
filament: req.body.filament, filamentSku: req.body.filamentSku,
parts: req.body.parts, parts: req.body.parts,
}; };
const result = await newObject({ const result = await newObject({

View File

@ -858,7 +858,10 @@ function buildDeepPopulateSpec(object, model, populated = new Set()) {
const ref = directRef || arrayRef; const ref = directRef || arrayRef;
if (!ref) return; if (!ref) return;
const refModel = model.db.model(ref); const refName = typeof ref === 'function' ? ref.call(object) : ref;
if (!refName) return;
const refModel = model.db.model(refName);
const childPopulate = buildDeepPopulateSpec(object, refModel, populated); const childPopulate = buildDeepPopulateSpec(object, refModel, populated);
const id = object[pathname]?._id || object[pathname]; const id = object[pathname]?._id || object[pathname];
@ -866,9 +869,9 @@ function buildDeepPopulateSpec(object, model, populated = new Set()) {
if (id == null || !id) return; if (id == null || !id) return;
if (childPopulate.length > 0) { if (childPopulate.length > 0) {
populateSpec.push({ path: pathname, populate: childPopulate, ref: ref, _id: id }); populateSpec.push({ path: pathname, populate: childPopulate, ref: refName, _id: id });
} else { } else {
populateSpec.push({ path: pathname, ref: ref, _id: id }); populateSpec.push({ path: pathname, ref: refName, _id: id });
} }
}); });