diff --git a/src/database/schemas/finance/invoice.schema.js b/src/database/schemas/finance/invoice.schema.js index 90627ee..89cb964 100644 --- a/src/database/schemas/finance/invoice.schema.js +++ b/src/database/schemas/finance/invoice.schema.js @@ -1,7 +1,28 @@ import mongoose from 'mongoose'; import { generateId } from '../../utils.js'; const { Schema } = mongoose; -import { aggregateRollups, aggregateRollupsHistory } from '../../database.js'; +import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js'; + +const invoiceOrderItemSchema = new Schema( + { + orderItem: { type: Schema.Types.ObjectId, ref: 'orderItem', required: true }, + taxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false }, + invoiceAmountWithTax: { type: Number, required: true, default: 0 }, + invoiceAmount: { type: Number, required: true, default: 0 }, + invoiceQuantity: { type: Number, required: true, default: 0 }, + }, + { timestamps: true } +); + +const invoiceShipmentSchema = new Schema( + { + shipment: { type: Schema.Types.ObjectId, ref: 'shipment', required: true }, + taxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false }, + invoiceAmountWithTax: { type: Number, required: true, default: 0 }, + invoiceAmount: { type: Number, required: true, default: 0 }, + }, + { timestamps: true } +); const invoiceSchema = new Schema( { @@ -12,21 +33,21 @@ const invoiceSchema = new Schema( 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 }, + from: { type: Schema.Types.ObjectId, ref: 'vendor', required: false }, + to: { type: Schema.Types.ObjectId, ref: 'client', required: false }, state: { type: { type: String, required: true, default: 'draft' }, }, - sentAt: { type: Date, required: false }, + orderType: { type: String, required: true }, + order: { type: Schema.Types.ObjectId, refPath: 'orderType', required: true }, + issuedAt: { type: Date, required: false }, + dueAt: { type: Date, required: false }, + postedAt: { type: Date, required: false }, + acknowledgedAt: { type: Date, required: false }, paidAt: { type: Date, required: false }, cancelledAt: { type: Date, required: false }, - overdueAt: { type: Date, required: false }, + invoiceOrderItems: [invoiceOrderItemSchema], + invoiceShipments: [invoiceShipmentSchema], }, { timestamps: true } ); @@ -35,32 +56,49 @@ const rollupConfigs = [ { name: 'draft', filter: { 'state.type': 'draft' }, - rollups: [{ name: 'draft', property: 'state.type', operation: 'count' }], + rollups: [ + { name: 'draftCount', property: 'state.type', operation: 'count' }, + { name: 'draftGrandTotalAmount', property: 'grandTotalAmount', operation: 'sum' }, + ], }, { name: 'sent', filter: { 'state.type': 'sent' }, - rollups: [{ name: 'sent', property: 'state.type', operation: 'count' }], + rollups: [ + { name: 'sentCount', property: 'state.type', operation: 'count' }, + { name: 'sentGrandTotalAmount', property: 'grandTotalAmount', operation: 'sum' }, + ], + }, + { + name: 'acknowledged', + filter: { 'state.type': 'acknowledged' }, + rollups: [ + { name: 'acknowledgedCount', property: 'state.type', operation: 'count' }, + { name: 'acknowledgedGrandTotalAmount', property: 'grandTotalAmount', operation: 'sum' }, + ], }, { name: 'partiallyPaid', filter: { 'state.type': 'partiallyPaid' }, - rollups: [{ name: 'partiallyPaid', property: 'state.type', operation: 'count' }], + rollups: [ + { name: 'partiallyPaidCount', property: 'state.type', operation: 'count' }, + { name: 'partiallyPaidGrandTotalAmount', property: 'grandTotalAmount', operation: 'sum' }, + ], }, { name: 'paid', filter: { 'state.type': 'paid' }, - rollups: [{ name: 'paid', property: 'state.type', operation: 'count' }], + rollups: [{ name: 'paidCount', property: 'state.type', operation: 'count' }], }, { name: 'overdue', filter: { 'state.type': 'overdue' }, - rollups: [{ name: 'overdue', property: 'state.type', operation: 'count' }], + rollups: [{ name: 'overdueCount', property: 'state.type', operation: 'count' }], }, { name: 'cancelled', filter: { 'state.type': 'cancelled' }, - rollups: [{ name: 'cancelled', property: 'state.type', operation: 'count' }], + rollups: [{ name: 'cancelledCount', property: 'state.type', operation: 'count' }], }, ]; @@ -86,6 +124,57 @@ invoiceSchema.statics.history = async function (from, to) { return results; }; +invoiceSchema.statics.recalculate = async function (invoice, user) { + const invoiceId = invoice._id || invoice; + if (!invoiceId) { + return; + } + + // Calculate totals from invoiceOrderItems + let totalAmount = 0; + for (const item of invoice.invoiceOrderItems || []) { + totalAmount += Number.parseFloat(item.invoiceAmount) || 0; + } + let totalAmountWithTax = 0; + for (const item of invoice.invoiceOrderItems || []) { + totalAmountWithTax += Number.parseFloat(item.invoiceAmountWithTax) || 0; + } + + // Calculate shipping totals from invoiceShipments + let shippingAmount = 0; + for (const item of invoice.invoiceShipments || []) { + shippingAmount += Number.parseFloat(item.invoiceAmount) || 0; + } + let shippingAmountWithTax = 0; + for (const item of invoice.invoiceShipments || []) { + shippingAmountWithTax += Number.parseFloat(item.invoiceAmountWithTax) || 0; + } + + // Calculate grand total and tax amount + const grandTotalAmount = parseFloat(totalAmountWithTax) + parseFloat(shippingAmountWithTax); + const totalTaxAmount = + parseFloat(totalAmountWithTax) - + parseFloat(totalAmount) + + (parseFloat(shippingAmountWithTax) - parseFloat(shippingAmount)); + + const updateData = { + totalAmount: parseFloat(totalAmount).toFixed(2), + totalAmountWithTax: parseFloat(totalAmountWithTax).toFixed(2), + shippingAmount: parseFloat(shippingAmount).toFixed(2), + shippingAmountWithTax: parseFloat(shippingAmountWithTax).toFixed(2), + grandTotalAmount: parseFloat(grandTotalAmount).toFixed(2), + totalTaxAmount: parseFloat(totalTaxAmount).toFixed(2), + }; + + await editObject({ + model: this, + id: invoiceId, + updateData, + user, + recalculate: false, + }); +}; + // Add virtual id getter invoiceSchema.virtual('id').get(function () { return this._id; diff --git a/src/database/schemas/finance/payment.schema.js b/src/database/schemas/finance/payment.schema.js new file mode 100644 index 0000000..437583d --- /dev/null +++ b/src/database/schemas/finance/payment.schema.js @@ -0,0 +1,105 @@ +import mongoose from 'mongoose'; +import { generateId } from '../../utils.js'; +const { Schema } = mongoose; +import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js'; + +const paymentSchema = new Schema( + { + _reference: { type: String, default: () => generateId()() }, + amount: { type: Number, required: true, default: 0 }, + vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: false }, + client: { type: Schema.Types.ObjectId, ref: 'client', required: false }, + invoice: { type: Schema.Types.ObjectId, ref: 'invoice', required: true }, + state: { + type: { type: String, required: true, default: 'draft' }, + }, + paymentDate: { type: Date, required: false }, + postedAt: { type: Date, required: false }, + cancelledAt: { type: Date, required: false }, + paymentMethod: { type: String, required: false }, + notes: { type: String, required: false }, + }, + { timestamps: true } +); + +const rollupConfigs = [ + { + name: 'draft', + filter: { 'state.type': 'draft' }, + rollups: [ + { name: 'draftCount', property: 'state.type', operation: 'count' }, + { name: 'draftAmount', property: 'amount', operation: 'sum' }, + ], + }, + { + name: 'posted', + filter: { 'state.type': 'posted' }, + rollups: [ + { name: 'postedCount', property: 'state.type', operation: 'count' }, + { name: 'postedAmount', property: 'amount', operation: 'sum' }, + ], + }, + { + name: 'cancelled', + filter: { 'state.type': 'cancelled' }, + rollups: [ + { name: 'cancelledCount', property: 'state.type', operation: 'count' }, + { name: 'cancelledAmount', property: 'amount', operation: 'sum' }, + ], + }, +]; + +paymentSchema.statics.stats = async function () { + const results = await aggregateRollups({ + model: this, + rollupConfigs: rollupConfigs, + }); + + // Transform the results to match the expected format + return results; +}; + +paymentSchema.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; +}; + +paymentSchema.statics.recalculate = async function (payment, user) { + const paymentId = payment._id || payment; + if (!paymentId) { + return; + } + + // For payments, the amount is set directly + const amount = payment.amount || 0; + + const updateData = { + amount: parseFloat(amount).toFixed(2), + }; + + await editObject({ + model: this, + id: paymentId, + updateData, + user, + recalculate: false, + }); +}; + +// Add virtual id getter +paymentSchema.virtual('id').get(function () { + return this._id; +}); + +// Configure JSON serialization to include virtuals +paymentSchema.set('toJSON', { virtuals: true }); + +// Create and export the model +export const paymentModel = mongoose.model('payment', paymentSchema); diff --git a/src/database/schemas/inventory/orderitem.schema.js b/src/database/schemas/inventory/orderitem.schema.js index d783080..3cb62ff 100644 --- a/src/database/schemas/inventory/orderitem.schema.js +++ b/src/database/schemas/inventory/orderitem.schema.js @@ -14,6 +14,7 @@ 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' }, }, @@ -26,6 +27,12 @@ const orderItemSchema = new Schema( 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 }, @@ -97,6 +104,9 @@ orderItemSchema.statics.recalculate = async function (orderItem, user) { model: orderItemModel, id: orderItem._id, updateData: { + invoicedAmountRemaining: orderTotalAmount - orderItem.invoicedAmount, + invoicedAmountWithTaxRemaining: orderTotalAmountWithTax - orderItem.invoicedAmountWithTax, + invoicedQuantityRemaining: orderItem.quantity - orderItem.invoicedQuantity, totalAmount: orderTotalAmount, totalAmountWithTax: orderTotalAmountWithTax, }, diff --git a/src/database/schemas/inventory/shipment.schema.js b/src/database/schemas/inventory/shipment.schema.js index 16bf11d..6abb147 100644 --- a/src/database/schemas/inventory/shipment.schema.js +++ b/src/database/schemas/inventory/shipment.schema.js @@ -15,6 +15,10 @@ const shipmentSchema = new Schema( amount: { type: Number, required: true }, amountWithTax: { type: Number, required: true }, taxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false }, + invoicedAmount: { type: Number, required: false, default: 0 }, + invoicedAmountWithTax: { type: Number, required: false, default: 0 }, + invoicedAmountRemaining: { type: Number, required: false, default: 0 }, + invoicedAmountWithTaxRemaining: { type: Number, required: false, default: 0 }, shippedAt: { type: Date, required: false }, expectedAt: { type: Date, required: false }, deliveredAt: { type: Date, required: false }, @@ -50,12 +54,16 @@ shipmentSchema.statics.recalculate = async function (shipment, user) { }); } - const amountWithTax = shipment.amount * (1 + (taxRate?.rate || 0) / 100); + const amountWithTax = parseFloat( + (shipment.amount || 0) * (1 + (taxRate?.rate || 0) / 100) + ).toFixed(2); await editObject({ model: shipmentModel, id: shipment._id, updateData: { amountWithTax: amountWithTax, + invoicedAmountRemaining: shipment.amount - (shipment.invoicedAmount || 0), + invoicedAmountWithTaxRemaining: amountWithTax - (shipment.invoicedAmountWithTax || 0), }, user, recalculate: false, diff --git a/src/database/schemas/models.js b/src/database/schemas/models.js index 215fd7f..21315df 100644 --- a/src/database/schemas/models.js +++ b/src/database/schemas/models.js @@ -28,6 +28,8 @@ 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'; +import { clientModel } from './sales/client.schema.js'; +import { salesOrderModel } from './sales/salesorder.schema.js'; // Map prefixes to models and id fields export const models = { @@ -102,4 +104,6 @@ export const models = { 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' }, + CLI: { model: clientModel, idField: '_id', type: 'client', referenceField: '_reference' }, + SOR: { model: salesOrderModel, idField: '_id', type: 'salesOrder', referenceField: '_reference' }, }; diff --git a/src/database/schemas/sales/client.schema.js b/src/database/schemas/sales/client.schema.js new file mode 100644 index 0000000..711ab6f --- /dev/null +++ b/src/database/schemas/sales/client.schema.js @@ -0,0 +1,34 @@ +import mongoose from 'mongoose'; +import { generateId } from '../../utils.js'; + +const addressSchema = new mongoose.Schema({ + building: { required: false, type: String }, + addressLine1: { required: false, type: String }, + addressLine2: { required: false, type: String }, + city: { required: false, type: String }, + state: { required: false, type: String }, + postcode: { required: false, type: String }, + country: { required: false, type: String }, +}); + +const clientSchema = new mongoose.Schema( + { + _reference: { type: String, default: () => generateId()() }, + name: { required: true, type: String }, + email: { required: false, type: String }, + phone: { required: false, type: String }, + country: { required: false, type: String }, + active: { required: true, type: Boolean, default: true }, + address: { required: false, type: addressSchema }, + tags: [{ required: false, type: String }], + }, + { timestamps: true } +); + +clientSchema.virtual('id').get(function () { + return this._id; +}); + +clientSchema.set('toJSON', { virtuals: true }); + +export const clientModel = mongoose.model('client', clientSchema); diff --git a/src/database/schemas/sales/salesorder.schema.js b/src/database/schemas/sales/salesorder.schema.js index e69de29..2874d69 100644 --- a/src/database/schemas/sales/salesorder.schema.js +++ b/src/database/schemas/sales/salesorder.schema.js @@ -0,0 +1,107 @@ +import mongoose from 'mongoose'; +import { generateId } from '../../utils.js'; +const { Schema } = mongoose; +import { aggregateRollups, aggregateRollupsHistory } from '../../database.js'; + +const salesOrderSchema = 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 }, + client: { type: Schema.Types.ObjectId, ref: 'client', required: true }, + state: { + type: { type: String, required: true, default: 'draft' }, + }, + postedAt: { type: Date, required: false }, + confirmedAt: { 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: 'confirmed', + filter: { 'state.type': 'confirmed' }, + rollups: [{ name: 'confirmed', 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: 'partiallyDelivered', + filter: { 'state.type': 'partiallyDelivered' }, + rollups: [{ name: 'partiallyDelivered', property: 'state.type', operation: 'count' }], + }, + { + name: 'delivered', + filter: { 'state.type': 'delivered' }, + rollups: [{ name: 'delivered', 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' }], + }, +]; + +salesOrderSchema.statics.stats = async function () { + const results = await aggregateRollups({ + model: this, + rollupConfigs: rollupConfigs, + }); + + // Transform the results to match the expected format + return results; +}; + +salesOrderSchema.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 +salesOrderSchema.virtual('id').get(function () { + return this._id; +}); + +// Configure JSON serialization to include virtuals +salesOrderSchema.set('toJSON', { virtuals: true }); + +// Create and export the model +export const salesOrderModel = mongoose.model('salesOrder', salesOrderSchema);