Added missing SKUs.

This commit is contained in:
Tom Butcher 2026-03-08 01:07:39 +00:00
parent bea9e45cd4
commit 7a500ffa58
10 changed files with 265 additions and 27 deletions

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

@ -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 },
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true },
type: { required: true, type: String }, 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

@ -2,13 +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 - pricing moved to PartSku // 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 },
file: { type: mongoose.SchemaTypes.ObjectId, ref: 'file', 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 } { timestamps: true }
); );
@ -21,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

@ -6,16 +6,17 @@ const { Schema } = mongoose;
const partSkuSchema = new Schema( const partSkuSchema = new Schema(
{ {
_reference: { type: String, default: () => generateId()() }, _reference: { type: String, default: () => generateId()() },
sku: { type: String, required: true }, barcode: { type: String, required: false },
part: { type: Schema.Types.ObjectId, ref: 'part', required: true }, part: { type: Schema.Types.ObjectId, ref: 'part', 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' }, priceMode: { type: String, default: 'margin' },
price: { type: Number, required: false }, price: { type: Number, required: false },
cost: { type: Number, required: false }, cost: { type: Number, required: false },
overrideCost: { type: Boolean, default: false },
overridePrice: { type: Boolean, default: false },
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: false },
priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false }, 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 },
priceWithTax: { type: Number, required: false }, priceWithTax: { type: Number, required: false },
@ -32,5 +33,22 @@ partSkuSchema.virtual('id').get(function () {
// Configure JSON serialization to include virtuals // Configure JSON serialization to include virtuals
partSkuSchema.set('toJSON', { virtuals: true }); 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 // Create and export the model
export const partSkuModel = mongoose.model('partSku', partSkuSchema); export const partSkuModel = mongoose.model('partSku', partSkuSchema);

View File

@ -10,6 +10,15 @@ const productSchema = new Schema(
tags: [{ type: String }], tags: [{ type: String }],
version: { type: String }, version: { type: String },
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true }, 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 } { timestamps: true }
); );
@ -21,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

@ -11,16 +11,17 @@ const partSkuUsageSchema = new 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' }, priceMode: { type: String, default: 'margin' },
price: { type: Number, required: false }, price: { type: Number, required: false },
cost: { type: Number, required: false }, cost: { type: Number, required: false },
overrideCost: { type: Boolean, default: false },
overridePrice: { type: Boolean, default: false },
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: false },
parts: [partSkuUsageSchema], parts: [partSkuUsageSchema],
priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false }, 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 },
@ -38,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,6 +2,7 @@ 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 { partSkuModel } from './management/partsku.schema.js';
@ -53,6 +54,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',

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 },