All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good
This commit introduces a new function, `syncModelsWithScheduler`, to synchronize database schemas with the farm control scheduler. It also adds scheduled properties and event handling for invoices, allowing for automatic state updates based on due dates. Additionally, minor refactoring is performed in the sales listings routes and services to improve code readability and maintainability. The changes enhance the application's data synchronization and invoice management capabilities.
252 lines
7.9 KiB
JavaScript
252 lines
7.9 KiB
JavaScript
import mongoose from 'mongoose';
|
|
import { generateId } from '../../utils.js';
|
|
const { Schema } = mongoose;
|
|
import {
|
|
aggregateRollups,
|
|
aggregateRollupsHistory,
|
|
editObject,
|
|
getObject,
|
|
} from '../../database.js';
|
|
import { taxRateModel } from '../management/taxrate.schema.js';
|
|
import { amountWithTax, resolveTaxRate } from '../../tax.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(
|
|
{
|
|
_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 },
|
|
from: { type: Schema.Types.ObjectId, refPath: 'fromType', required: false },
|
|
fromType: { type: String, required: false },
|
|
to: { type: Schema.Types.ObjectId, refPath: 'toType', required: false },
|
|
toType: { type: String, required: false },
|
|
state: {
|
|
type: { type: String, required: true, default: 'draft' },
|
|
},
|
|
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 },
|
|
invoiceOrderItems: [invoiceOrderItemSchema],
|
|
invoiceShipments: [invoiceShipmentSchema],
|
|
},
|
|
{ timestamps: true }
|
|
);
|
|
|
|
invoiceSchema.index({ orderType: 'text', fromType: 'text', toType: 'text' });
|
|
|
|
const rollupConfigs = [
|
|
{
|
|
name: 'draft',
|
|
filter: { 'state.type': 'draft' },
|
|
rollups: [
|
|
{ name: 'draftCount', property: 'state.type', operation: 'count' },
|
|
{ name: 'draftGrandTotalAmount', property: 'grandTotalAmount', operation: 'sum' },
|
|
],
|
|
},
|
|
{
|
|
name: 'sent',
|
|
filter: { 'state.type': 'sent' },
|
|
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: 'partiallyPaidCount', property: 'state.type', operation: 'count' },
|
|
{ name: 'partiallyPaidGrandTotalAmount', property: 'grandTotalAmount', operation: 'sum' },
|
|
],
|
|
},
|
|
{
|
|
name: 'paid',
|
|
filter: { 'state.type': 'paid' },
|
|
rollups: [
|
|
{ name: 'paidCount', property: 'state.type', operation: 'count' },
|
|
{ name: 'paidGrandTotalAmount', property: 'grandTotalAmount', operation: 'sum' },
|
|
],
|
|
},
|
|
{
|
|
name: 'overdue',
|
|
filter: { 'state.type': 'overdue' },
|
|
rollups: [
|
|
{ name: 'overdueCount', property: 'state.type', operation: 'count' },
|
|
{ name: 'overdueGrandTotalAmount', property: 'grandTotalAmount', operation: 'sum' },
|
|
],
|
|
},
|
|
{
|
|
name: 'cancelled',
|
|
filter: { 'state.type': 'cancelled' },
|
|
rollups: [{ name: 'cancelledCount', 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;
|
|
};
|
|
|
|
invoiceSchema.statics.scheduledProperties = [
|
|
{
|
|
name: 'dueAt',
|
|
filter: { state: 'due|sent|acknowledged' },
|
|
type: 'dateTime',
|
|
},
|
|
];
|
|
|
|
invoiceSchema.statics.onSchedulerEvent = async function (invoice, property) {
|
|
const invoiceId = invoice._id || invoice;
|
|
if (!invoiceId) {
|
|
return;
|
|
}
|
|
|
|
if (property === 'dueAt' && invoice.dueAt && invoice.dueAt < new Date()) {
|
|
await editObject({
|
|
model: this,
|
|
id: invoiceId,
|
|
updateData: {
|
|
state: { type: 'overdue' },
|
|
},
|
|
user: 'system',
|
|
recalculate: false,
|
|
});
|
|
}
|
|
};
|
|
|
|
invoiceSchema.statics.recalculate = async function (invoice, user) {
|
|
const invoiceId = invoice._id || invoice;
|
|
if (!invoiceId) {
|
|
return;
|
|
}
|
|
|
|
const invoiceOrderItems = [];
|
|
for (const item of invoice.invoiceOrderItems || []) {
|
|
const taxRate = await resolveTaxRate(item.taxRate, getObject, taxRateModel);
|
|
invoiceOrderItems.push({
|
|
...item,
|
|
invoiceAmountWithTax: amountWithTax(item.invoiceAmount, taxRate),
|
|
});
|
|
}
|
|
|
|
const invoiceShipments = [];
|
|
for (const item of invoice.invoiceShipments || []) {
|
|
const taxRate = await resolveTaxRate(item.taxRate, getObject, taxRateModel);
|
|
invoiceShipments.push({
|
|
...item,
|
|
invoiceAmountWithTax: amountWithTax(item.invoiceAmount, taxRate),
|
|
});
|
|
}
|
|
|
|
// Calculate totals from invoiceOrderItems
|
|
let totalAmount = 0;
|
|
for (const item of invoiceOrderItems) {
|
|
totalAmount += Number.parseFloat(item.invoiceAmount) || 0;
|
|
}
|
|
let totalAmountWithTax = 0;
|
|
for (const item of invoiceOrderItems) {
|
|
totalAmountWithTax += Number.parseFloat(item.invoiceAmountWithTax) || 0;
|
|
}
|
|
|
|
// Calculate shipping totals from invoiceShipments
|
|
let shippingAmount = 0;
|
|
for (const item of invoiceShipments) {
|
|
shippingAmount += Number.parseFloat(item.invoiceAmount) || 0;
|
|
}
|
|
let shippingAmountWithTax = 0;
|
|
for (const item of 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 = {
|
|
invoiceOrderItems,
|
|
invoiceShipments,
|
|
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;
|
|
});
|
|
|
|
// Configure JSON serialization to include virtuals
|
|
invoiceSchema.set('toJSON', { virtuals: true });
|
|
|
|
// Create and export the model
|
|
export const invoiceModel = mongoose.model('invoice', invoiceSchema);
|