From 7a500ffa58cde0bf570735be27020d24249e22c5 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sun, 8 Mar 2026 01:07:39 +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 +- 10 files changed, 265 insertions(+), 27 deletions(-) create mode 100644 src/database/schemas/management/filamentsku.schema.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 },