From 8eafc81321305438d1e1e57901192fff2f0d42ab Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sat, 27 Dec 2025 14:15:13 +0000 Subject: [PATCH] Updated models inline with API. --- .../schemas/finance/invoice.schema.js | 98 ++++++++++++++ .../schemas/inventory/orderitem.schema.js | 126 +++++++++++++++++- .../schemas/inventory/purchaseorder.schema.js | 84 +++++++++++- .../schemas/inventory/shipment.schema.js | 107 ++++++++++++--- .../schemas/management/taxrate.schema.js | 25 ++++ src/database/schemas/models.js | 6 +- src/templates/templatemanager.js | 14 +- 7 files changed, 425 insertions(+), 35 deletions(-) create mode 100644 src/database/schemas/finance/invoice.schema.js create mode 100644 src/database/schemas/management/taxrate.schema.js diff --git a/src/database/schemas/finance/invoice.schema.js b/src/database/schemas/finance/invoice.schema.js new file mode 100644 index 0000000..90627ee --- /dev/null +++ b/src/database/schemas/finance/invoice.schema.js @@ -0,0 +1,98 @@ +import mongoose from 'mongoose'; +import { generateId } from '../../utils.js'; +const { Schema } = mongoose; +import { aggregateRollups, aggregateRollupsHistory } from '../../database.js'; + +const invoiceSchema = new Schema( + { + _reference: { type: String, default: () => generateId()() }, + totalAmount: { type: Number, required: true, default: 0 }, + totalAmountWithTax: { type: Number, required: true, default: 0 }, + shippingAmount: { type: Number, required: true, default: 0 }, + shippingAmountWithTax: { type: Number, required: true, default: 0 }, + grandTotalAmount: { type: Number, required: true, default: 0 }, + totalTaxAmount: { type: Number, required: true, default: 0 }, + timestamp: { type: Date, default: Date.now }, + invoiceDate: { type: Date, required: false }, + dueDate: { type: Date, required: false }, + vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: false }, + customer: { type: Schema.Types.ObjectId, ref: 'customer', required: false }, + invoiceType: { type: String, required: true, default: 'sales', enum: ['sales', 'purchase'] }, + relatedOrderType: { type: String, required: false }, + relatedOrder: { type: Schema.Types.ObjectId, refPath: 'relatedOrderType', required: false }, + state: { + type: { type: String, required: true, default: 'draft' }, + }, + sentAt: { type: Date, required: false }, + paidAt: { type: Date, required: false }, + cancelledAt: { type: Date, required: false }, + overdueAt: { type: Date, required: false }, + }, + { timestamps: true } +); + +const rollupConfigs = [ + { + name: 'draft', + filter: { 'state.type': 'draft' }, + rollups: [{ name: 'draft', property: 'state.type', operation: 'count' }], + }, + { + name: 'sent', + filter: { 'state.type': 'sent' }, + rollups: [{ name: 'sent', property: 'state.type', operation: 'count' }], + }, + { + name: 'partiallyPaid', + filter: { 'state.type': 'partiallyPaid' }, + rollups: [{ name: 'partiallyPaid', property: 'state.type', operation: 'count' }], + }, + { + name: 'paid', + filter: { 'state.type': 'paid' }, + rollups: [{ name: 'paid', property: 'state.type', operation: 'count' }], + }, + { + name: 'overdue', + filter: { 'state.type': 'overdue' }, + rollups: [{ name: 'overdue', property: 'state.type', operation: 'count' }], + }, + { + name: 'cancelled', + filter: { 'state.type': 'cancelled' }, + rollups: [{ name: 'cancelled', property: 'state.type', operation: 'count' }], + }, +]; + +invoiceSchema.statics.stats = async function () { + const results = await aggregateRollups({ + model: this, + rollupConfigs: rollupConfigs, + }); + + // Transform the results to match the expected format + return results; +}; + +invoiceSchema.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; +}; + +// Add virtual id getter +invoiceSchema.virtual('id').get(function () { + return this._id; +}); + +// Configure JSON serialization to include virtuals +invoiceSchema.set('toJSON', { virtuals: true }); + +// Create and export the model +export const invoiceModel = mongoose.model('invoice', invoiceSchema); diff --git a/src/database/schemas/inventory/orderitem.schema.js b/src/database/schemas/inventory/orderitem.schema.js index b5d0e9b..6b63879 100644 --- a/src/database/schemas/inventory/orderitem.schema.js +++ b/src/database/schemas/inventory/orderitem.schema.js @@ -1,6 +1,12 @@ import mongoose from 'mongoose'; import { purchaseOrderModel } from './purchaseorder.schema.js'; -import { aggregateRollups, editObject } from '../../database.js'; +import { taxRateModel } from '../management/taxrate.schema.js'; +import { + aggregateRollups, + aggregateRollupsHistory, + editObject, + getObject, +} from '../../database.js'; import { generateId } from '../../utils.js'; const { Schema } = mongoose; @@ -8,20 +14,64 @@ const orderItemSchema = new Schema( { _reference: { type: String, default: () => generateId()() }, orderType: { 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: true }, - syncAmount: { type: String, required: true, default: null }, + 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 }, 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, + }); + + console.log(results); + + // 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) { // Only purchase orders are supported for now if (orderItem.orderType !== 'purchaseOrder') { @@ -33,6 +83,29 @@ orderItemSchema.statics.recalculate = async function (orderItem, user) { return; } + var 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 = orderItem.itemAmount * orderItem.quantity; + const orderTotalAmountWithTax = orderTotalAmount * (1 + (taxRate?.rate || 0) / 100); + await editObject({ + model: orderItemModel, + id: orderItem._id, + updateData: { + totalAmount: orderTotalAmount, + totalAmountWithTax: orderTotalAmountWithTax, + }, + user, + recalculate: false, + }); + const rollupResults = await aggregateRollups({ model: this, baseFilter: { @@ -51,21 +124,60 @@ orderItemSchema.statics.recalculate = async function (orderItem, user) { }, ], }, + { + name: 'overallCount', + rollups: [{ name: 'overallCount', property: '_id', operation: 'count' }], + }, + ...rollupConfigs, ], }); + console.log('rollupResults', rollupResults); + const totals = rollupResults.orderTotals || {}; const totalAmount = totals.totalAmount.sum?.toFixed(2) || 0; const totalAmountWithTax = totals.totalAmountWithTax.sum?.toFixed(2) || 0; + const purchaseOrder = await getObject({ + model: purchaseOrderModel, + id: orderId, + cached: true, + }); + + const grandTotalAmount = + parseFloat(totalAmountWithTax || 0) + parseFloat(purchaseOrder.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 (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' } }; + } + await editObject({ model: purchaseOrderModel, id: orderId, - updateData: { - totalAmount: parseFloat(totalAmount), - totalAmountWithTax: parseFloat(totalAmountWithTax), - totalTaxAmount: parseFloat(totalAmountWithTax - totalAmount), - }, + updateData: updateData, user, }); }; diff --git a/src/database/schemas/inventory/purchaseorder.schema.js b/src/database/schemas/inventory/purchaseorder.schema.js index e4d8d5d..a7ee7cc 100644 --- a/src/database/schemas/inventory/purchaseorder.schema.js +++ b/src/database/schemas/inventory/purchaseorder.schema.js @@ -1,22 +1,100 @@ import mongoose from 'mongoose'; import { generateId } from '../../utils.js'; const { Schema } = mongoose; +import { aggregateRollups, aggregateRollupsHistory } from '../../database.js'; const purchaseOrderSchema = new Schema( { _reference: { type: String, default: () => generateId()() }, - totalAmount: { type: Number, required: true }, - totalAmountWithTax: { type: Number, required: true }, - totalTaxAmount: { type: Number, required: true }, + totalAmount: { type: Number, required: true, default: 0 }, + totalAmountWithTax: { type: Number, required: true, default: 0 }, + shippingAmount: { type: Number, required: true, default: 0 }, + shippingAmountWithTax: { type: Number, required: true, default: 0 }, + grandTotalAmount: { type: Number, required: true, default: 0 }, + totalTaxAmount: { type: Number, required: true, default: 0 }, timestamp: { type: Date, default: Date.now }, vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true }, state: { type: { type: String, required: true, default: 'draft' }, }, + postedAt: { type: Date, required: false }, + acknowledgedAt: { type: Date, required: false }, + cancelledAt: { type: Date, required: false }, + completedAt: { type: Date, required: false }, }, { timestamps: true } ); +const rollupConfigs = [ + { + name: 'draft', + filter: { 'state.type': 'draft' }, + rollups: [{ name: 'draft', property: 'state.type', operation: 'count' }], + }, + { + name: 'sent', + filter: { 'state.type': 'sent' }, + rollups: [{ name: 'sent', property: 'state.type', operation: 'count' }], + }, + { + name: 'acknowledged', + filter: { 'state.type': 'acknowledged' }, + rollups: [{ name: 'acknowledged', property: 'state.type', operation: 'count' }], + }, + { + name: 'partiallyShipped', + filter: { 'state.type': 'partiallyShipped' }, + rollups: [{ name: 'partiallyShipped', property: 'state.type', operation: 'count' }], + }, + { + name: 'shipped', + filter: { 'state.type': 'shipped' }, + rollups: [{ name: 'shipped', property: 'state.type', operation: 'count' }], + }, + { + name: 'partiallyReceived', + filter: { 'state.type': 'partiallyReceived' }, + rollups: [{ name: 'partiallyReceived', property: 'state.type', operation: 'count' }], + }, + { + name: 'received', + filter: { 'state.type': 'received' }, + rollups: [{ name: 'received', property: 'state.type', operation: 'count' }], + }, + { + name: 'cancelled', + filter: { 'state.type': 'cancelled' }, + rollups: [{ name: 'cancelled', property: 'state.type', operation: 'count' }], + }, + { + name: 'completed', + filter: { 'state.type': 'completed' }, + rollups: [{ name: 'completed', property: 'state.type', operation: 'count' }], + }, +]; + +purchaseOrderSchema.statics.stats = async function () { + const results = await aggregateRollups({ + model: this, + rollupConfigs: rollupConfigs, + }); + + // Transform the results to match the expected format + return results; +}; + +purchaseOrderSchema.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; +}; + // Add virtual id getter purchaseOrderSchema.virtual('id').get(function () { return this._id; diff --git a/src/database/schemas/inventory/shipment.schema.js b/src/database/schemas/inventory/shipment.schema.js index 67331ae..16bf11d 100644 --- a/src/database/schemas/inventory/shipment.schema.js +++ b/src/database/schemas/inventory/shipment.schema.js @@ -1,43 +1,108 @@ import mongoose from 'mongoose'; import { generateId } from '../../utils.js'; const { Schema } = mongoose; - -const shipmentItemSchema = new Schema({ - itemType: { type: String, required: true }, - item: { type: Schema.Types.ObjectId, refPath: 'itemType', required: true }, - quantity: { type: Number, required: true }, - itemCost: { type: Number, required: true }, - totalCost: { type: Number, required: true }, - totalCostWithTax: { type: Number, required: true }, - taxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false }, -}); +import { purchaseOrderModel } from './purchaseorder.schema.js'; +import { taxRateModel } from '../management/taxrate.schema.js'; +import { aggregateRollups, editObject, getObject } from '../../database.js'; const shipmentSchema = new Schema( { _reference: { type: String, default: () => generateId()() }, - purchaseOrder: { type: Schema.Types.ObjectId, ref: 'purchaseOrder', required: true }, - vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true }, + orderType: { type: String, required: true }, + order: { type: Schema.Types.ObjectId, refPath: 'orderType', required: true }, courierService: { type: Schema.Types.ObjectId, ref: 'courierService', required: false }, trackingNumber: { type: String, required: false }, - items: [shipmentItemSchema], - cost: { net: { type: Number, required: true }, gross: { type: Number, required: true } }, - shippedDate: { type: Date, required: false }, - expectedDeliveryDate: { type: Date, required: false }, - actualDeliveryDate: { type: Date, required: false }, + amount: { type: Number, required: true }, + amountWithTax: { type: Number, required: true }, + taxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false }, + shippedAt: { type: Date, required: false }, + expectedAt: { type: Date, required: false }, + deliveredAt: { type: Date, required: false }, + cancelledAt: { type: Date, required: false }, state: { type: { type: String, required: true, - default: 'pending', - enum: ['pending', 'shipped', 'in_transit', 'delivered', 'cancelled'], }, }, - notes: { type: String }, - timestamp: { type: Date, default: Date.now }, }, { timestamps: true } ); +shipmentSchema.statics.recalculate = async function (shipment, user) { + // Only purchase orders are supported for now + if (shipment.orderType !== 'purchaseOrder') { + return; + } + + const orderId = shipment.order?._id || shipment.order; + if (!orderId) { + return; + } + + var taxRate = shipment.taxRate; + + if (shipment.taxRate?._id && Object.keys(shipment.taxRate).length == 1) { + taxRate = await getObject({ + model: taxRateModel, + id: shipment.taxRate._id, + cached: true, + }); + } + + const amountWithTax = shipment.amount * (1 + (taxRate?.rate || 0) / 100); + await editObject({ + model: shipmentModel, + id: shipment._id, + updateData: { + amountWithTax: amountWithTax, + }, + user, + recalculate: false, + }); + + const rollupResults = await aggregateRollups({ + model: this, + baseFilter: { + order: new mongoose.Types.ObjectId(orderId), + orderType: shipment.orderType, + }, + rollupConfigs: [ + { + name: 'shipmentTotals', + rollups: [ + { name: 'amount', property: 'amount', operation: 'sum' }, + { name: 'amountWithTax', property: 'amountWithTax', operation: 'sum' }, + ], + }, + ], + }); + + const totals = rollupResults.shipmentTotals || {}; + const totalShippingAmount = totals.amount.sum?.toFixed(2) || 0; + const totalShippingAmountWithTax = totals.amountWithTax.sum?.toFixed(2) || 0; + + const purchaseOrder = await getObject({ + model: purchaseOrderModel, + id: orderId, + cached: true, + }); + + const grandTotalAmount = + parseFloat(purchaseOrder.totalAmountWithTax || 0) + parseFloat(totalShippingAmountWithTax || 0); + await editObject({ + model: purchaseOrderModel, + id: orderId, + updateData: { + shippingAmount: parseFloat(totalShippingAmount).toFixed(2), + shippingAmountWithTax: parseFloat(totalShippingAmountWithTax).toFixed(2), + grandTotalAmount: parseFloat(grandTotalAmount).toFixed(2), + }, + user, + recalculate: false, + }); +}; + // Add virtual id getter shipmentSchema.virtual('id').get(function () { return this._id; diff --git a/src/database/schemas/management/taxrate.schema.js b/src/database/schemas/management/taxrate.schema.js new file mode 100644 index 0000000..898c3ca --- /dev/null +++ b/src/database/schemas/management/taxrate.schema.js @@ -0,0 +1,25 @@ +import mongoose from 'mongoose'; +import { generateId } from '../../utils.js'; + +const taxRateSchema = new mongoose.Schema( + { + _reference: { type: String, default: () => generateId()() }, + name: { required: true, type: String }, + rate: { required: true, type: Number }, + rateType: { required: true, type: String, enum: ['percentage', 'fixed'] }, + active: { required: true, type: Boolean, default: true }, + description: { required: false, type: String }, + country: { required: false, type: String }, + effectiveFrom: { required: false, type: Date }, + effectiveTo: { required: false, type: Date }, + }, + { timestamps: true } +); + +taxRateSchema.virtual('id').get(function () { + return this._id; +}); + +taxRateSchema.set('toJSON', { virtuals: true }); + +export const taxRateModel = mongoose.model('taxRate', taxRateSchema); diff --git a/src/database/schemas/models.js b/src/database/schemas/models.js index 98c37bf..215fd7f 100644 --- a/src/database/schemas/models.js +++ b/src/database/schemas/models.js @@ -24,8 +24,10 @@ import { documentJobModel } from './management/documentjob.schema.js'; import { fileModel } from './management/file.schema.js'; import { courierServiceModel } from './management/courierservice.schema.js'; import { courierModel } from './management/courier.schema.js'; -import { taxRateModel } from './management/taxrates.schema.js'; +import { taxRateModel } from './management/taxrate.schema.js'; import { taxRecordModel } from './management/taxrecord.schema.js'; +import { shipmentModel } from './inventory/shipment.schema.js'; +import { invoiceModel } from './finance/invoice.schema.js'; // Map prefixes to models and id fields export const models = { @@ -98,4 +100,6 @@ export const models = { COR: { model: courierModel, idField: '_id', type: 'courier', referenceField: '_reference' }, TXR: { model: taxRateModel, idField: '_id', type: 'taxRate', referenceField: '_reference' }, TXD: { model: taxRecordModel, idField: '_id', type: 'taxRecord', referenceField: '_reference' }, + SHP: { model: shipmentModel, idField: '_id', type: 'shipment', referenceField: '_reference' }, + INV: { model: invoiceModel, idField: '_id', type: 'invoice', referenceField: '_reference' }, }; diff --git a/src/templates/templatemanager.js b/src/templates/templatemanager.js index 898d6d3..c06c0ce 100644 --- a/src/templates/templatemanager.js +++ b/src/templates/templatemanager.js @@ -596,12 +596,20 @@ export class TemplateManager { return dayjs(date).format(format); } - async getObject(objectType, id) { + async getObject(objectType = undefined, id = undefined, populate = []) { + if (objectType == undefined) { + logger.warn('Object type is required'); + return { error: 'Object type is required' }; + } + if (id == undefined) { + logger.warn('Object ID is required'); + return { error: 'Object ID is required' }; + } const model = getModelByName(objectType); if (model == undefined) { - throw new Error('Farm Control: Object type not found.'); + logger.warn('Object type not found:', objectType); } - const object = await getObject({ model, id, cached: true }); + const object = await getObject({ model, id, cached: true, populate }); return object; } }