From 7eb774a2973064f5db02edd560aba6c179f18a4b Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sun, 8 Mar 2026 01:07:34 +0000 Subject: [PATCH] Added missing SKUs. --- .../schemas/inventory/filamentstock.schema.js | 2 +- .../schemas/inventory/orderitem.schema.js | 104 ++++++++-- .../schemas/management/filament.schema.js | 30 ++- .../schemas/management/filamentsku.schema.js | 48 +++++ .../schemas/management/part.schema.js | 28 ++- .../schemas/management/partsku.schema.js | 22 +- .../schemas/management/product.schema.js | 26 +++ .../schemas/management/productsku.schema.js | 22 +- src/database/schemas/models.js | 8 + .../schemas/production/gcodefile.schema.js | 2 +- src/index.js | 2 + src/routes/index.js | 2 + src/routes/inventory/filamentstocks.js | 4 +- src/routes/management/filamentskus.js | 59 ++++++ src/routes/management/partskus.js | 2 +- src/routes/management/productskus.js | 2 +- src/services/inventory/filamentstocks.js | 8 +- src/services/inventory/orderitems.js | 141 +++++++++++-- src/services/management/filaments.js | 39 ++-- src/services/management/filamentskus.js | 196 ++++++++++++++++++ src/services/management/parts.js | 20 +- src/services/management/partskus.js | 60 ++++-- src/services/management/products.js | 20 +- src/services/management/productskus.js | 62 ++++-- src/services/misc/csv.js | 5 +- src/services/misc/excel.js | 5 +- src/services/misc/odata.js | 5 +- src/services/production/gcodefiles.js | 10 +- src/utils.js | 9 +- 29 files changed, 803 insertions(+), 140 deletions(-) create mode 100644 src/database/schemas/management/filamentsku.schema.js create mode 100644 src/routes/management/filamentskus.js create mode 100644 src/services/management/filamentskus.js diff --git a/src/database/schemas/inventory/filamentstock.schema.js b/src/database/schemas/inventory/filamentstock.schema.js index 468c8c4..8e48672 100644 --- a/src/database/schemas/inventory/filamentstock.schema.js +++ b/src/database/schemas/inventory/filamentstock.schema.js @@ -19,7 +19,7 @@ const filamentStockSchema = new Schema( net: { 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 } ); diff --git a/src/database/schemas/inventory/orderitem.schema.js b/src/database/schemas/inventory/orderitem.schema.js index 0d3a46c..f0e8ea1 100644 --- a/src/database/schemas/inventory/orderitem.schema.js +++ b/src/database/schemas/inventory/orderitem.schema.js @@ -2,6 +2,12 @@ import mongoose from 'mongoose'; import { purchaseOrderModel } from './purchaseorder.schema.js'; import { salesOrderModel } from '../sales/salesorder.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 { aggregateRollups, aggregateRollupsHistory, @@ -11,6 +17,18 @@ import { import { generateId } from '../../utils.js'; const { Schema } = mongoose; +const skuModelsByItemType = { + filament: filamentSkuModel, + part: partSkuModel, + product: productSkuModel, +}; + +const parentModelsByItemType = { + filament: filamentModel, + part: partModel, + product: productModel, +}; + const orderItemSchema = new Schema( { _reference: { type: String, default: () => generateId()() }, @@ -21,7 +39,16 @@ const orderItemSchema = new Schema( }, order: { type: Schema.Types.ObjectId, refPath: 'orderType', 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 }, itemAmount: { type: Number, required: true }, quantity: { type: Number, required: true }, @@ -88,9 +115,55 @@ orderItemSchema.statics.recalculate = async function (orderItem, user) { 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({ model: taxRateModel, 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 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({ - model: orderItemModel, + model: this, id: orderItem._id, - updateData: { - invoicedAmountRemaining: orderTotalAmount - orderItem.invoicedAmount, - invoicedAmountWithTaxRemaining: orderTotalAmountWithTax - orderItem.invoicedAmountWithTax, - invoicedQuantityRemaining: orderItem.quantity - orderItem.invoicedQuantity, - totalAmount: orderTotalAmount, - totalAmountWithTax: orderTotalAmountWithTax, - }, + updateData: orderItemUpdateData, user, recalculate: false, }); diff --git a/src/database/schemas/management/filament.schema.js b/src/database/schemas/management/filament.schema.js index 54589c2..9295a8d 100644 --- a/src/database/schemas/management/filament.schema.js +++ b/src/database/schemas/management/filament.schema.js @@ -2,24 +2,21 @@ import mongoose from 'mongoose'; import { generateId } from '../../utils.js'; const { Schema } = mongoose; +// Filament base - cost and tax; color and cost override at FilamentSKU const filamentSchema = new mongoose.Schema({ _reference: { type: String, default: () => generateId()() }, name: { required: true, type: String }, barcode: { required: false, type: String }, url: { required: false, type: String }, image: { required: false, type: Buffer }, - color: { required: true, type: String }, - 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 }, density: { required: true, type: Number }, - createdAt: { required: true, type: Date }, - updatedAt: { required: true, type: Date }, 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 () { return this._id; @@ -27,4 +24,21 @@ filamentSchema.virtual('id').get(function () { 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); diff --git a/src/database/schemas/management/filamentsku.schema.js b/src/database/schemas/management/filamentsku.schema.js new file mode 100644 index 0000000..3d4caa5 --- /dev/null +++ b/src/database/schemas/management/filamentsku.schema.js @@ -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); diff --git a/src/database/schemas/management/part.schema.js b/src/database/schemas/management/part.schema.js index c7fdff4..de89983 100644 --- a/src/database/schemas/management/part.schema.js +++ b/src/database/schemas/management/part.schema.js @@ -2,13 +2,22 @@ import mongoose from 'mongoose'; import { generateId } from '../../utils.js'; const { Schema } = mongoose; -// Define the main part schema - pricing moved to PartSku +// Define the main part schema - cost/price and tax; override at PartSku const partSchema = new Schema( { _reference: { type: String, default: () => generateId()() }, name: { type: String, required: true }, fileName: { type: String, required: false }, file: { type: mongoose.SchemaTypes.ObjectId, ref: 'file', 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 }, + priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false }, + costWithTax: { type: Number, required: false }, + priceWithTax: { type: Number, required: false }, }, { timestamps: true } ); @@ -21,5 +30,22 @@ partSchema.virtual('id').get(function () { // Configure JSON serialization to include virtuals 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 export const partModel = mongoose.model('part', partSchema); diff --git a/src/database/schemas/management/partsku.schema.js b/src/database/schemas/management/partsku.schema.js index d21e2df..7c8d056 100644 --- a/src/database/schemas/management/partsku.schema.js +++ b/src/database/schemas/management/partsku.schema.js @@ -6,16 +6,17 @@ const { Schema } = mongoose; const partSkuSchema = new Schema( { _reference: { type: String, default: () => generateId()() }, - sku: { type: String, required: true }, + 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 }, - vendor: { type: Schema.Types.ObjectId, ref: 'vendor', 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 }, @@ -32,5 +33,22 @@ partSkuSchema.virtual('id').get(function () { // 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); diff --git a/src/database/schemas/management/product.schema.js b/src/database/schemas/management/product.schema.js index 80c7b16..ec10453 100644 --- a/src/database/schemas/management/product.schema.js +++ b/src/database/schemas/management/product.schema.js @@ -10,6 +10,15 @@ const productSchema = new Schema( tags: [{ 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' }, + margin: { type: Number, required: false }, + amount: { type: Number, 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 } ); @@ -21,5 +30,22 @@ productSchema.virtual('id').get(function () { // Configure JSON serialization to include virtuals 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 export const productModel = mongoose.model('product', productSchema); diff --git a/src/database/schemas/management/productsku.schema.js b/src/database/schemas/management/productsku.schema.js index ac0c452..1b5b807 100644 --- a/src/database/schemas/management/productsku.schema.js +++ b/src/database/schemas/management/productsku.schema.js @@ -11,16 +11,17 @@ const partSkuUsageSchema = new Schema({ const productSkuSchema = new Schema( { _reference: { type: String, default: () => generateId()() }, - sku: { type: String, required: true }, + barcode: { type: String, required: false }, product: { type: Schema.Types.ObjectId, ref: 'product', 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 }, - vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: false }, parts: [partSkuUsageSchema], priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false }, costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false }, @@ -38,5 +39,22 @@ productSkuSchema.virtual('id').get(function () { // Configure JSON serialization to include virtuals 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 export const productSkuModel = mongoose.model('productSku', productSkuSchema); diff --git a/src/database/schemas/models.js b/src/database/schemas/models.js index 0130206..968f7b3 100644 --- a/src/database/schemas/models.js +++ b/src/database/schemas/models.js @@ -2,6 +2,7 @@ import { jobModel } from './production/job.schema.js'; import { subJobModel } from './production/subjob.schema.js'; import { printerModel } from './production/printer.schema.js'; import { filamentModel } from './management/filament.schema.js'; +import { filamentSkuModel } from './management/filamentsku.schema.js'; import { gcodeFileModel } from './production/gcodefile.schema.js'; import { partModel } from './management/part.schema.js'; import { partSkuModel } from './management/partsku.schema.js'; @@ -53,6 +54,13 @@ export const models = { referenceField: '_reference', label: 'Filament', }, + FSU: { + model: filamentSkuModel, + idField: '_id', + type: 'filamentSku', + referenceField: '_reference', + label: 'Filament SKU', + }, GCF: { model: gcodeFileModel, idField: '_id', diff --git a/src/database/schemas/production/gcodefile.schema.js b/src/database/schemas/production/gcodefile.schema.js index 9b74d3a..2152e4c 100644 --- a/src/database/schemas/production/gcodefile.schema.js +++ b/src/database/schemas/production/gcodefile.schema.js @@ -13,7 +13,7 @@ const gcodeFileSchema = new mongoose.Schema( name: { required: true, type: String }, gcodeFileName: { required: false, type: String }, 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], file: { type: mongoose.SchemaTypes.ObjectId, ref: 'file', required: false }, cost: { type: Number, required: false }, diff --git a/src/index.js b/src/index.js index 4e4d6c7..2b5b537 100644 --- a/src/index.js +++ b/src/index.js @@ -14,6 +14,7 @@ import { subJobRoutes, gcodeFileRoutes, filamentRoutes, + filamentSkuRoutes, spotlightRoutes, partRoutes, partSkuRoutes, @@ -133,6 +134,7 @@ app.use('/jobs', jobRoutes); app.use('/subjobs', subJobRoutes); app.use('/gcodefiles', gcodeFileRoutes); app.use('/filaments', filamentRoutes); +app.use('/filamentskus', filamentSkuRoutes); app.use('/parts', partRoutes); app.use('/partskus', partSkuRoutes); app.use('/products', productRoutes); diff --git a/src/routes/index.js b/src/routes/index.js index f37b754..1839922 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -8,6 +8,7 @@ import jobRoutes from './production/jobs.js'; import subJobRoutes from './production/subjobs.js'; import gcodeFileRoutes from './production/gcodefiles.js'; import filamentRoutes from './management/filaments.js'; +import filamentSkuRoutes from './management/filamentskus.js'; import spotlightRoutes from './misc/spotlight.js'; import partRoutes from './management/parts.js'; import partSkuRoutes from './management/partskus.js'; @@ -55,6 +56,7 @@ export { subJobRoutes, gcodeFileRoutes, filamentRoutes, + filamentSkuRoutes, spotlightRoutes, partRoutes, partSkuRoutes, diff --git a/src/routes/inventory/filamentstocks.js b/src/routes/inventory/filamentstocks.js index ef60df3..7dc22ed 100644 --- a/src/routes/inventory/filamentstocks.js +++ b/src/routes/inventory/filamentstocks.js @@ -18,14 +18,14 @@ import { // list of filament stocks router.get('/', isAuthenticated, (req, res) => { 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); listFilamentStocksRouteHandler(req, res, page, limit, property, filter, search, sort, order); }); router.get('/properties', isAuthenticated, (req, res) => { let properties = convertPropertiesString(req.query.properties); - const allowedFilters = ['filament', 'state.type']; + const allowedFilters = ['filamentSku', 'state.type']; const filter = getFilter(req.query, allowedFilters, false); var masterFilter = {}; if (req.query.masterFilter) { diff --git a/src/routes/management/filamentskus.js b/src/routes/management/filamentskus.js new file mode 100644 index 0000000..285a6ab --- /dev/null +++ b/src/routes/management/filamentskus.js @@ -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; diff --git a/src/routes/management/partskus.js b/src/routes/management/partskus.js index b508a84..9728b38 100644 --- a/src/routes/management/partskus.js +++ b/src/routes/management/partskus.js @@ -16,7 +16,7 @@ import { router.get('/', isAuthenticated, (req, res) => { const { page, limit, property, search, sort, order } = req.query; - const allowedFilters = ['_id', 'sku', 'part', 'part._id', 'name', 'cost', 'price']; + 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); }); diff --git a/src/routes/management/productskus.js b/src/routes/management/productskus.js index a91b9ec..b0ddb8a 100644 --- a/src/routes/management/productskus.js +++ b/src/routes/management/productskus.js @@ -16,7 +16,7 @@ import { router.get('/', isAuthenticated, (req, res) => { const { page, limit, property, search, sort, order } = req.query; - const allowedFilters = ['_id', 'sku', 'product', 'product._id', 'name', 'cost', 'price']; + const allowedFilters = ['_id', 'barcode', 'product', 'product._id', 'name', 'cost', 'price']; const filter = getFilter(req.query, allowedFilters); listProductSkusRouteHandler(req, res, page, limit, property, filter, search, sort, order); }); diff --git a/src/services/inventory/filamentstocks.js b/src/services/inventory/filamentstocks.js index 46dac25..5bd73f0 100644 --- a/src/services/inventory/filamentstocks.js +++ b/src/services/inventory/filamentstocks.js @@ -36,7 +36,7 @@ export const listFilamentStocksRouteHandler = async ( search, sort, order, - populate: [{ path: 'filament' }], + populate: [{ path: 'filamentSku' }], }); if (result?.error) { @@ -60,7 +60,7 @@ export const listFilamentStocksByPropertiesRouteHandler = async ( model: filamentStockModel, properties, filter, - populate: ['filament'], + populate: ['filamentSku'], masterFilter, }); @@ -79,7 +79,7 @@ export const getFilamentStockRouteHandler = async (req, res) => { const result = await getObject({ model: filamentStockModel, id, - populate: [{ path: 'filament' }], + populate: [{ path: 'filamentSku' }], }); if (result?.error) { logger.warn(`Filament Stock not found with supplied id.`); @@ -146,7 +146,7 @@ export const newFilamentStockRouteHandler = async (req, res) => { updatedAt: new Date(), startingWeight: req.body.startingWeight, currentWeight: req.body.currentWeight, - filament: req.body.filament, + filamentSku: req.body.filamentSku, state: req.body.state, }; const result = await newObject({ diff --git a/src/services/inventory/orderitems.js b/src/services/inventory/orderitems.js index fa02f3d..17c2680 100644 --- a/src/services/inventory/orderitems.js +++ b/src/services/inventory/orderitems.js @@ -1,5 +1,6 @@ import config from '../../config.js'; import { orderItemModel } from '../../database/schemas/inventory/orderitem.schema.js'; +import { getModelByName } from '../misc/model.js'; import log4js from 'log4js'; import mongoose from 'mongoose'; import { @@ -50,11 +51,39 @@ export const listOrderItemsRouteHandler = async ( }, { path: 'item', - populate: { path: 'costTaxRate', strictPopulate: false }, + populate: [ + { path: 'costTaxRate', strictPopulate: false }, + { path: 'priceTaxRate', strictPopulate: false }, + ], }, { - path: 'item', - populate: { path: 'priceTaxRate', strictPopulate: false }, + path: 'sku', + 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', - populate: { path: 'costTaxRate', strictPopulate: false }, + populate: [ + { path: 'costTaxRate', strictPopulate: false }, + { path: 'priceTaxRate', strictPopulate: false }, + ], strictPopulate: false, }, { - path: 'item', - populate: { path: 'priceTaxRate', strictPopulate: false }, + path: 'sku', 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}`); + 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 = { updatedAt: new Date(), - name: req.body.name, + name: req.body.name ?? name, itemType: req.body.itemType, item: req.body.item, + sku: req.body.sku, orderType: req.body.orderType, order: req.body.order, syncAmount: req.body.syncAmount, @@ -158,7 +242,7 @@ export const editOrderItemRouteHandler = async (req, res) => { if (result.error) { logger.error('Error editing order item:', result.error); - res.status(result).send(result); + res.status(result.code || 500).send(result); return; } @@ -173,13 +257,14 @@ export const editMultipleOrderItemsRouteHandler = async (req, res) => { name: update.name, itemType: update.itemType, item: update.item, + sku: update.sku, orderType: update.orderType, order: update.order, - syncAmount: update.syncAmount, - itemAmount: update.itemAmount, - quantity: update.quantity, - totalAmount: update.totalAmount, - shipment: update.shipment, + syncAmount: update.syncAmount, + itemAmount: update.itemAmount, + quantity: update.quantity, + totalAmount: update.totalAmount, + shipment: update.shipment, taxRate: update.taxRate, totalAmountWithTax: update.totalAmountWithTax, })); @@ -206,13 +291,41 @@ export const editMultipleOrderItemsRouteHandler = 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 = { updatedAt: new Date(), - name: req.body.name, + name: name || 'Order Item', purchaseOrder: req.body.purchaseOrder, state: { type: 'draft' }, itemType: req.body.itemType, item: req.body.item, + sku: req.body.sku, orderType: req.body.orderType, order: req.body.order, syncAmount: req.body.syncAmount, diff --git a/src/services/management/filaments.js b/src/services/management/filaments.js index 38cc8c9..247a03d 100644 --- a/src/services/management/filaments.js +++ b/src/services/management/filaments.js @@ -36,7 +36,7 @@ export const listFilamentsRouteHandler = async ( search, sort, order, - populate: ['vendor', 'costTaxRate'], + populate: ['costTaxRate'], }); if (result?.error) { @@ -59,16 +59,7 @@ export const listFilamentsByPropertiesRouteHandler = async ( model: filamentModel, properties, filter, - populate: [ - { - path: 'vendor', - from: 'vendors', - }, - { - path: 'costTaxRate', - from: 'taxrates', - }, - ], + populate: [], }); if (result?.error) { @@ -86,7 +77,7 @@ export const getFilamentRouteHandler = async (req, res) => { const result = await getObject({ model: filamentModel, id, - populate: ['vendor', 'costTaxRate'], + populate: ['costTaxRate'], }); if (result?.error) { logger.warn(`Filament not found with supplied id.`); @@ -108,15 +99,13 @@ export const editFilamentRouteHandler = async (req, res) => { barcode: req.body.barcode, url: req.body.url, image: req.body.image, - color: req.body.color, - vendor: req.body.vendor, type: req.body.type, - cost: req.body.cost, - costTaxRate: req.body.costTaxRate, - costWithTax: req.body.costWithTax, diameter: req.body.diameter, density: req.body.density, emptySpoolWeight: req.body.emptySpoolWeight, + cost: req.body?.cost, + costTaxRate: req.body?.costTaxRate, + costWithTax: req.body?.costWithTax, }; const result = await editObject({ model: filamentModel, @@ -143,15 +132,13 @@ export const editMultipleFilamentsRouteHandler = async (req, res) => { barcode: update.barcode, url: update.url, image: update.image, - color: update.color, - vendor: update.vendor, type: update.type, - cost: update.cost, - costTaxRate: update.costTaxRate, - costWithTax: update.costWithTax, diameter: update.diameter, density: update.density, emptySpoolWeight: update.emptySpoolWeight, + cost: update.cost, + costTaxRate: update.costTaxRate, + costWithTax: update.costWithTax, })); if (!Array.isArray(updates)) { @@ -183,15 +170,13 @@ export const newFilamentRouteHandler = async (req, res) => { barcode: req.body.barcode, url: req.body.url, image: req.body.image, - color: req.body.color, - vendor: req.body.vendor, type: req.body.type, - cost: req.body.cost, - costTaxRate: req.body.costTaxRate, - costWithTax: req.body.costWithTax, diameter: req.body.diameter, density: req.body.density, emptySpoolWeight: req.body.emptySpoolWeight, + cost: req.body?.cost, + costTaxRate: req.body?.costTaxRate, + costWithTax: req.body?.costWithTax, }; const result = await newObject({ diff --git a/src/services/management/filamentskus.js b/src/services/management/filamentskus.js new file mode 100644 index 0000000..7ba9667 --- /dev/null +++ b/src/services/management/filamentskus.js @@ -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: [{ path: 'filament', populate: 'costTaxRate' }, '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); +}; diff --git a/src/services/management/parts.js b/src/services/management/parts.js index cde2db5..b694359 100644 --- a/src/services/management/parts.js +++ b/src/services/management/parts.js @@ -35,7 +35,7 @@ export const listPartsRouteHandler = async ( search, sort, order, - populate: [], + populate: ['costTaxRate', 'priceTaxRate'], }); if (result?.error) { @@ -71,7 +71,7 @@ export const getPartRouteHandler = async (req, res) => { const result = await getObject({ model: partModel, id, - populate: [], + populate: ['costTaxRate', 'priceTaxRate'], }); if (result?.error) { logger.warn(`Part not found with supplied id.`); @@ -92,6 +92,14 @@ export const editPartRouteHandler = async (req, res) => { name: req.body?.name, fileName: req.body?.fileName, file: req.body?.file, + 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 const result = await editObject({ @@ -118,6 +126,14 @@ export const newPartRouteHandler = async (req, res) => { name: req.body?.name, fileName: req.body?.fileName, file: req.body?.file, + 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({ diff --git a/src/services/management/partskus.js b/src/services/management/partskus.js index 8d077d8..4f6c8f2 100644 --- a/src/services/management/partskus.js +++ b/src/services/management/partskus.js @@ -35,7 +35,7 @@ export const listPartSkusRouteHandler = async ( search, sort, order, - populate: ['part', 'vendor', 'priceTaxRate', 'costTaxRate'], + populate: ['priceTaxRate', 'costTaxRate'], }); if (result?.error) { @@ -59,7 +59,11 @@ export const listPartSkusByPropertiesRouteHandler = async ( model: partSkuModel, properties, filter, - populate: ['part', 'vendor', 'priceTaxRate', 'costTaxRate'], + populate: [ + { path: 'part', populate: ['costTaxRate', 'priceTaxRate'] }, + 'priceTaxRate', + 'costTaxRate', + ], masterFilter, }); @@ -78,7 +82,11 @@ export const getPartSkuRouteHandler = async (req, res) => { const result = await getObject({ model: partSkuModel, id, - populate: ['part', 'vendor', 'priceTaxRate', 'costTaxRate'], + populate: [ + { path: 'part', populate: ['costTaxRate', 'priceTaxRate'] }, + 'priceTaxRate', + 'costTaxRate', + ], }); if (result?.error) { logger.warn(`Part SKU not found with supplied id.`); @@ -93,22 +101,25 @@ export const editPartSkuRouteHandler = async (req, res) => { logger.trace(`Part SKU with ID: ${id}`); + const overrideCost = req.body?.overrideCost; + const overridePrice = req.body?.overridePrice; + const updateData = { updatedAt: new Date(), - sku: req.body?.sku, + barcode: req.body?.barcode, part: req.body?.part, name: req.body?.name, description: req.body?.description, priceMode: req.body?.priceMode, - price: req.body?.price, - cost: req.body?.cost, - margin: req.body?.margin, - amount: req.body?.amount, - vendor: req.body?.vendor, - priceTaxRate: req.body?.priceTaxRate, - costTaxRate: req.body?.costTaxRate, - priceWithTax: req.body?.priceWithTax, - costWithTax: req.body?.costWithTax, + 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({ @@ -129,21 +140,24 @@ export const editPartSkuRouteHandler = async (req, res) => { }; export const newPartSkuRouteHandler = async (req, res) => { + const overrideCost = req.body?.overrideCost; + const overridePrice = req.body?.overridePrice; + const newData = { - sku: req.body?.sku, + barcode: req.body?.barcode, part: req.body?.part, name: req.body?.name, description: req.body?.description, priceMode: req.body?.priceMode, - price: req.body?.price, - cost: req.body?.cost, - margin: req.body?.margin, - amount: req.body?.amount, - vendor: req.body?.vendor, - priceTaxRate: req.body?.priceTaxRate, - costTaxRate: req.body?.costTaxRate, - priceWithTax: req.body?.priceWithTax, - costWithTax: req.body?.costWithTax, + 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({ diff --git a/src/services/management/products.js b/src/services/management/products.js index 201471b..0bc6d4c 100644 --- a/src/services/management/products.js +++ b/src/services/management/products.js @@ -35,7 +35,7 @@ export const listProductsRouteHandler = async ( search, sort, order, - populate: ['vendor'], + populate: ['vendor', 'costTaxRate', 'priceTaxRate'], }); if (result?.error) { @@ -76,7 +76,7 @@ export const getProductRouteHandler = async (req, res) => { const result = await getObject({ model: productModel, id, - populate: ['vendor'], + populate: ['vendor', 'costTaxRate', 'priceTaxRate'], }); if (result?.error) { logger.warn(`Product not found with supplied id.`); @@ -98,6 +98,14 @@ export const editProductRouteHandler = async (req, res) => { tags: req.body?.tags, version: req.body?.version, vendor: req.body.vendor, + 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 const result = await editObject({ @@ -125,6 +133,14 @@ export const newProductRouteHandler = async (req, res) => { tags: req.body?.tags, version: req.body?.version, vendor: req.body.vendor, + 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({ diff --git a/src/services/management/productskus.js b/src/services/management/productskus.js index 16e3131..065ae44 100644 --- a/src/services/management/productskus.js +++ b/src/services/management/productskus.js @@ -35,7 +35,7 @@ export const listProductSkusRouteHandler = async ( search, sort, order, - populate: ['product', 'vendor', 'priceTaxRate', 'costTaxRate', 'parts.partSku'], + populate: ['priceTaxRate', 'costTaxRate'], }); if (result?.error) { @@ -59,7 +59,12 @@ export const listProductSkusByPropertiesRouteHandler = async ( model: productSkuModel, properties, filter, - populate: ['product', 'vendor', 'priceTaxRate', 'costTaxRate', 'parts.partSku'], + populate: [ + { path: 'product', populate: ['costTaxRate', 'priceTaxRate'] }, + 'priceTaxRate', + 'costTaxRate', + 'parts.partSku', + ], masterFilter, }); @@ -78,7 +83,12 @@ export const getProductSkuRouteHandler = async (req, res) => { const result = await getObject({ model: productSkuModel, id, - populate: ['product', 'vendor', 'priceTaxRate', 'costTaxRate', 'parts.partSku'], + populate: [ + { path: 'product', populate: ['costTaxRate', 'priceTaxRate'] }, + 'priceTaxRate', + 'costTaxRate', + 'parts.partSku', + ], }); if (result?.error) { logger.warn(`Product SKU not found with supplied id.`); @@ -93,23 +103,26 @@ export const editProductSkuRouteHandler = async (req, res) => { logger.trace(`Product SKU with ID: ${id}`); + const overrideCost = req.body?.overrideCost; + const overridePrice = req.body?.overridePrice; + const updateData = { updatedAt: new Date(), - sku: req.body?.sku, + barcode: req.body?.barcode, product: req.body?.product, name: req.body?.name, description: req.body?.description, priceMode: req.body?.priceMode, - price: req.body?.price, - cost: req.body?.cost, - margin: req.body?.margin, - amount: req.body?.amount, - vendor: req.body?.vendor, + 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: req.body?.priceTaxRate, - costTaxRate: req.body?.costTaxRate, - priceWithTax: req.body?.priceWithTax, - costWithTax: req.body?.costWithTax, + 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({ @@ -130,22 +143,25 @@ export const editProductSkuRouteHandler = async (req, res) => { }; export const newProductSkuRouteHandler = async (req, res) => { + const overrideCost = req.body?.overrideCost; + const overridePrice = req.body?.overridePrice; + const newData = { - sku: req.body?.sku, + barcode: req.body?.barcode, product: req.body?.product, name: req.body?.name, description: req.body?.description, priceMode: req.body?.priceMode, - price: req.body?.price, - cost: req.body?.cost, - margin: req.body?.margin, - amount: req.body?.amount, - vendor: req.body?.vendor, + 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: req.body?.priceTaxRate, - costTaxRate: req.body?.costTaxRate, - priceWithTax: req.body?.priceWithTax, - costWithTax: req.body?.costWithTax, + 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({ diff --git a/src/services/misc/csv.js b/src/services/misc/csv.js index 8549f6f..168e58b 100644 --- a/src/services/misc/csv.js +++ b/src/services/misc/csv.js @@ -62,13 +62,14 @@ function getModelFilterFields(objectType) { printer: ['host'], job: ['printer', 'gcodeFile'], subJob: ['job'], - filamentStock: ['filament'], + filamentStock: ['filamentSku'], + filamentSku: ['filament', 'vendor', 'costTaxRate'], partStock: ['partSku'], partSku: ['part', 'vendor', 'priceTaxRate', 'costTaxRate'], productStock: ['productSku'], productSku: ['product', 'vendor', 'priceTaxRate', 'costTaxRate'], purchaseOrder: ['vendor'], - orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'], + 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'], diff --git a/src/services/misc/excel.js b/src/services/misc/excel.js index 279c777..e7f91ce 100644 --- a/src/services/misc/excel.js +++ b/src/services/misc/excel.js @@ -68,13 +68,14 @@ function getModelFilterFields(objectType) { printer: ['host'], job: ['printer', 'gcodeFile'], subJob: ['job'], - filamentStock: ['filament'], + filamentStock: ['filamentSku'], + filamentSku: ['filament', 'vendor', 'costTaxRate'], partStock: ['partSku'], partSku: ['part', 'vendor', 'priceTaxRate', 'costTaxRate'], productStock: ['productSku'], productSku: ['product', 'vendor', 'priceTaxRate', 'costTaxRate'], purchaseOrder: ['vendor'], - orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'], + 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'], diff --git a/src/services/misc/odata.js b/src/services/misc/odata.js index e462271..2e1ca09 100644 --- a/src/services/misc/odata.js +++ b/src/services/misc/odata.js @@ -342,13 +342,14 @@ function getModelFilterFields(objectType) { printer: ['host'], job: ['printer', 'gcodeFile'], subJob: ['job'], - filamentStock: ['filament'], + filamentStock: ['filamentSku'], + filamentSku: ['filament', 'vendor', 'costTaxRate'], partStock: ['partSku'], partSku: ['part', 'vendor', 'priceTaxRate', 'costTaxRate'], productStock: ['productSku'], productSku: ['product', 'vendor', 'priceTaxRate', 'costTaxRate'], purchaseOrder: ['vendor'], - orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'shipment._id'], + 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'], diff --git a/src/services/production/gcodefiles.js b/src/services/production/gcodefiles.js index d0a79a4..0ba99a1 100644 --- a/src/services/production/gcodefiles.js +++ b/src/services/production/gcodefiles.js @@ -35,7 +35,7 @@ export const listGCodeFilesRouteHandler = async ( search, sort, order, - populate: ['filament'], + populate: ['filamentSku'], }); if (result?.error) { @@ -58,7 +58,7 @@ export const listGCodeFilesByPropertiesRouteHandler = async ( model: gcodeFileModel, properties, filter, - populate: 'filament', + populate: 'filamentSku', }); if (result?.error) { @@ -76,7 +76,7 @@ export const getGCodeFileRouteHandler = async (req, res) => { const result = await getObject({ model: gcodeFileModel, id, - populate: ['filament', 'parts.part'], + populate: ['filamentSku', 'parts.partSku'], }); if (result?.error) { logger.warn(`GCodeFile not found with supplied id.`); @@ -113,7 +113,7 @@ export const editGCodeFileRouteHandler = async (req, res) => { updatedAt: new Date(), name: req.body.name, file: req.body.file, - filament: req.body.filament, + filamentSku: req.body.filamentSku, parts: req.body.parts, }; // Create audit log before updating @@ -140,7 +140,7 @@ export const newGCodeFileRouteHandler = async (req, res) => { updatedAt: new Date(), name: req.body.name, file: req.body.file, - filament: req.body.filament, + filamentSku: req.body.filamentSku, parts: req.body.parts, }; const result = await newObject({ diff --git a/src/utils.js b/src/utils.js index e1a1e7d..e7e1a55 100644 --- a/src/utils.js +++ b/src/utils.js @@ -858,7 +858,10 @@ function buildDeepPopulateSpec(object, model, populated = new Set()) { const ref = directRef || arrayRef; 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 id = object[pathname]?._id || object[pathname]; @@ -866,9 +869,9 @@ function buildDeepPopulateSpec(object, model, populated = new Set()) { if (id == null || !id) return; 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 { - populateSpec.push({ path: pathname, ref: ref, _id: id }); + populateSpec.push({ path: pathname, ref: refName, _id: id }); } });