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, editObject, getObject, } from '../../database.js'; 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()() }, orderType: { type: String, required: true }, name: { type: String, required: true }, state: { type: { type: String, required: true, default: 'draft' }, }, order: { type: Schema.Types.ObjectId, refPath: 'orderType', required: true }, itemType: { type: String, 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 }, totalAmount: { type: Number, required: true }, taxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false }, totalAmountWithTax: { type: Number, required: true }, invoicedAmountWithTax: { type: Number, required: false, default: 0 }, invoicedAmount: { type: Number, required: false, default: 0 }, invoicedQuantity: { type: Number, required: false, default: 0 }, invoicedAmountRemaining: { type: Number, required: false, default: 0 }, invoicedAmountWithTaxRemaining: { type: Number, required: false, default: 0 }, invoicedQuantityRemaining: { type: Number, required: false, default: 0 }, timestamp: { type: Date, default: Date.now }, shipment: { type: Schema.Types.ObjectId, ref: 'shipment', required: false }, orderedAt: { type: Date, required: false }, receivedAt: { type: Date, required: false }, }, { timestamps: true } ); const rollupConfigs = [ { name: 'shipped', filter: { 'state.type': 'shipped' }, rollups: [{ name: 'shipped', property: 'state.type', operation: 'count' }], }, { name: 'received', filter: { 'state.type': 'received' }, rollups: [{ name: 'received', property: 'state.type', operation: 'count' }], }, ]; orderItemSchema.statics.stats = async function () { const results = await aggregateRollups({ model: this, baseFilter: {}, rollupConfigs: rollupConfigs, }); // Transform the results to match the expected format return results; }; orderItemSchema.statics.history = async function (from, to) { const results = await aggregateRollupsHistory({ model: this, startDate: from, endDate: to, rollupConfigs: rollupConfigs, }); // Return time-series data array return results; }; orderItemSchema.statics.recalculate = async function (orderItem, user) { if (orderItem.orderType !== 'purchaseOrder' && orderItem.orderType !== 'salesOrder') { return; } const orderId = orderItem.order?._id || orderItem.order; if (!orderId) { return; } // 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; } } } } } } let taxRate = orderItem.taxRate; if (orderItem.taxRate?._id && Object.keys(orderItem.taxRate).length === 1) { taxRate = await getObject({ model: taxRateModel, id: orderItem.taxRate._id, cached: true, }); } 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: this, id: orderItem._id, updateData: orderItemUpdateData, user, recalculate: false, }); const rollupResults = await aggregateRollups({ model: this, baseFilter: { order: new mongoose.Types.ObjectId(orderId), orderType: orderItem.orderType, }, rollupConfigs: [ { name: 'orderTotals', rollups: [ { name: 'totalAmount', property: 'totalAmount', operation: 'sum' }, { name: 'totalAmountWithTax', property: 'totalAmountWithTax', operation: 'sum', }, ], }, { name: 'overallCount', rollups: [{ name: 'overallCount', property: '_id', operation: 'count' }], }, ...rollupConfigs, ], }); const totals = rollupResults.orderTotals || {}; const totalAmount = totals.totalAmount.sum?.toFixed(2) || 0; const totalAmountWithTax = totals.totalAmountWithTax.sum?.toFixed(2) || 0; const orderModel = orderItem.orderType === 'purchaseOrder' ? purchaseOrderModel : salesOrderModel; const order = await getObject({ model: orderModel, id: orderId, cached: true, }); const grandTotalAmount = parseFloat(totalAmountWithTax || 0) + parseFloat(order.shippingAmountWithTax || 0); var updateData = { totalAmount: parseFloat(totalAmount).toFixed(2), totalAmountWithTax: parseFloat(totalAmountWithTax).toFixed(2), totalTaxAmount: parseFloat((totalAmountWithTax - totalAmount).toFixed(2)), grandTotalAmount: parseFloat(grandTotalAmount).toFixed(2), }; const overallCount = rollupResults.overallCount.count || 0; const shippedCount = rollupResults.shipped.count || 0; const receivedCount = rollupResults.received.count || 0; if (orderItem.orderType === 'purchaseOrder') { if (shippedCount > 0 && shippedCount < overallCount) { updateData = { ...updateData, state: { type: 'partiallyShipped' } }; } if (shippedCount > 0 && shippedCount == overallCount) { updateData = { ...updateData, state: { type: 'shipped' } }; } if (receivedCount > 0 && receivedCount < overallCount) { updateData = { ...updateData, state: { type: 'partiallyReceived' } }; } if (receivedCount > 0 && receivedCount == overallCount) { updateData = { ...updateData, state: { type: 'received' } }; } } else { if (shippedCount > 0 && shippedCount < overallCount) { updateData = { ...updateData, state: { type: 'partiallyShipped' } }; } if (shippedCount > 0 && shippedCount == overallCount) { updateData = { ...updateData, state: { type: 'shipped' } }; } if (receivedCount > 0 && receivedCount < overallCount) { updateData = { ...updateData, state: { type: 'partiallyDelivered' } }; } if (receivedCount > 0 && receivedCount == overallCount) { updateData = { ...updateData, state: { type: 'delivered' } }; } } await editObject({ model: orderModel, id: orderId, updateData: updateData, user, }); }; // Add virtual id getter orderItemSchema.virtual('id').get(function () { return this._id; }); // Configure JSON serialization to include virtuals orderItemSchema.set('toJSON', { virtuals: true }); // Create and export the model export const orderItemModel = mongoose.model('orderItem', orderItemSchema);