Compare commits

..

No commits in common. "a7e35c279eb1071b37c137e706120098c823b7e5" and "dea6a90b68bdcf3d144e18ebe59a5b618e19eeb5" have entirely different histories.

19 changed files with 94 additions and 1903 deletions

View File

@ -1,28 +1,7 @@
import mongoose from 'mongoose';
import { generateId } from '../../utils.js';
const { Schema } = mongoose;
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 }
);
import { aggregateRollups, aggregateRollupsHistory } from '../../database.js';
const invoiceSchema = new Schema(
{
@ -33,21 +12,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 },
from: { type: Schema.Types.ObjectId, ref: 'vendor', required: false },
to: { type: Schema.Types.ObjectId, ref: 'client', required: false },
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' },
},
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 },
sentAt: { type: Date, required: false },
paidAt: { type: Date, required: false },
cancelledAt: { type: Date, required: false },
invoiceOrderItems: [invoiceOrderItemSchema],
invoiceShipments: [invoiceShipmentSchema],
overdueAt: { type: Date, required: false },
},
{ timestamps: true }
);
@ -56,49 +35,32 @@ const rollupConfigs = [
{
name: 'draft',
filter: { 'state.type': 'draft' },
rollups: [
{ name: 'draftCount', property: 'state.type', operation: 'count' },
{ name: 'draftGrandTotalAmount', property: 'grandTotalAmount', operation: 'sum' },
],
rollups: [{ name: 'draft', property: 'state.type', operation: 'count' }],
},
{
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' },
],
rollups: [{ name: 'sent', property: 'state.type', operation: 'count' }],
},
{
name: 'partiallyPaid',
filter: { 'state.type': 'partiallyPaid' },
rollups: [
{ name: 'partiallyPaidCount', property: 'state.type', operation: 'count' },
{ name: 'partiallyPaidGrandTotalAmount', property: 'grandTotalAmount', operation: 'sum' },
],
rollups: [{ name: 'partiallyPaid', property: 'state.type', operation: 'count' }],
},
{
name: 'paid',
filter: { 'state.type': 'paid' },
rollups: [{ name: 'paidCount', property: 'state.type', operation: 'count' }],
rollups: [{ name: 'paid', property: 'state.type', operation: 'count' }],
},
{
name: 'overdue',
filter: { 'state.type': 'overdue' },
rollups: [{ name: 'overdueCount', property: 'state.type', operation: 'count' }],
rollups: [{ name: 'overdue', property: 'state.type', operation: 'count' }],
},
{
name: 'cancelled',
filter: { 'state.type': 'cancelled' },
rollups: [{ name: 'cancelledCount', property: 'state.type', operation: 'count' }],
rollups: [{ name: 'cancelled', property: 'state.type', operation: 'count' }],
},
];
@ -124,57 +86,6 @@ 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;

View File

@ -1,105 +0,0 @@
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);

View File

@ -14,7 +14,6 @@ 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' },
},
@ -27,12 +26,6 @@ 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 },
@ -104,9 +97,6 @@ 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,
},

View File

@ -15,10 +15,6 @@ 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 },
@ -54,16 +50,12 @@ shipmentSchema.statics.recalculate = async function (shipment, user) {
});
}
const amountWithTax = parseFloat(
(shipment.amount || 0) * (1 + (taxRate?.rate || 0) / 100)
).toFixed(2);
const amountWithTax = shipment.amount * (1 + (taxRate?.rate || 0) / 100);
await editObject({
model: shipmentModel,
id: shipment._id,
updateData: {
amountWithTax: amountWithTax,
invoicedAmountRemaining: shipment.amount - (shipment.invoicedAmount || 0),
invoicedAmountWithTaxRemaining: amountWithTax - (shipment.invoicedAmountWithTax || 0),
},
user,
recalculate: false,

View File

@ -28,8 +28,6 @@ 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 = {
@ -104,6 +102,4 @@ 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' },
};

View File

@ -1,34 +0,0 @@
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);

View File

@ -1,107 +0,0 @@
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);

View File

@ -38,9 +38,6 @@ import {
taxRateRoutes,
taxRecordRoutes,
invoiceRoutes,
paymentRoutes,
clientRoutes,
salesOrderRoutes,
} from './routes/index.js';
import path from 'path';
import * as fs from 'fs';
@ -144,9 +141,6 @@ app.use('/courierservices', courierServiceRoutes);
app.use('/taxrates', taxRateRoutes);
app.use('/taxrecords', taxRecordRoutes);
app.use('/invoices', invoiceRoutes);
app.use('/payments', paymentRoutes);
app.use('/clients', clientRoutes);
app.use('/salesorders', salesOrderRoutes);
app.use('/notes', noteRoutes);
// Start the application

View File

@ -13,9 +13,9 @@ import {
listInvoicesByPropertiesRouteHandler,
getInvoiceStatsRouteHandler,
getInvoiceHistoryRouteHandler,
acknowledgeInvoiceRouteHandler,
sendInvoiceRouteHandler,
markInvoicePaidRouteHandler,
cancelInvoiceRouteHandler,
postInvoiceRouteHandler,
} from '../../services/finance/invoices.js';
// list of invoices
@ -23,13 +23,12 @@ router.get('/', isAuthenticated, (req, res) => {
const { page, limit, property, search, sort, order } = req.query;
const allowedFilters = [
'vendor',
'client',
'customer',
'invoiceType',
'state',
'value',
'vendor._id',
'client._id',
'order',
'order._id',
'orderType',
'customer._id',
];
const filter = getFilter(req.query, allowedFilters);
listInvoicesRouteHandler(req, res, page, limit, property, filter, search, sort, order);
@ -39,13 +38,12 @@ router.get('/properties', isAuthenticated, (req, res) => {
let properties = convertPropertiesString(req.query.properties);
const allowedFilters = [
'vendor',
'client',
'orderType',
'order',
'customer',
'invoiceType',
'state.type',
'value',
'vendor._id',
'client._id',
'customer._id',
];
const filter = getFilter(req.query, allowedFilters, false);
var masterFilter = {};
@ -86,12 +84,12 @@ router.delete('/:id', isAuthenticated, async (req, res) => {
deleteInvoiceRouteHandler(req, res);
});
router.post('/:id/post', isAuthenticated, async (req, res) => {
postInvoiceRouteHandler(req, res);
router.post('/:id/send', isAuthenticated, async (req, res) => {
sendInvoiceRouteHandler(req, res);
});
router.post('/:id/acknowledge', isAuthenticated, async (req, res) => {
acknowledgeInvoiceRouteHandler(req, res);
router.post('/:id/markpaid', isAuthenticated, async (req, res) => {
markInvoicePaidRouteHandler(req, res);
});
router.post('/:id/cancel', isAuthenticated, async (req, res) => {

View File

@ -1,96 +0,0 @@
import express from 'express';
import { isAuthenticated } from '../../keycloak.js';
import { getFilter, convertPropertiesString } from '../../utils.js';
const router = express.Router();
import {
listPaymentsRouteHandler,
getPaymentRouteHandler,
editPaymentRouteHandler,
editMultiplePaymentsRouteHandler,
newPaymentRouteHandler,
deletePaymentRouteHandler,
listPaymentsByPropertiesRouteHandler,
getPaymentStatsRouteHandler,
getPaymentHistoryRouteHandler,
postPaymentRouteHandler,
cancelPaymentRouteHandler,
} from '../../services/finance/payments.js';
// list of payments
router.get('/', isAuthenticated, (req, res) => {
const { page, limit, property, search, sort, order } = req.query;
const allowedFilters = [
'vendor',
'client',
'state',
'vendor._id',
'client._id',
'invoice',
'invoice._id',
];
const filter = getFilter(req.query, allowedFilters);
listPaymentsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
});
router.get('/properties', isAuthenticated, (req, res) => {
let properties = convertPropertiesString(req.query.properties);
const allowedFilters = [
'vendor',
'client',
'invoice',
'state.type',
'value',
'vendor._id',
'client._id',
'invoice._id',
];
const filter = getFilter(req.query, allowedFilters, false);
var masterFilter = {};
if (req.query.masterFilter) {
masterFilter = JSON.parse(req.query.masterFilter);
}
listPaymentsByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
});
router.post('/', isAuthenticated, (req, res) => {
newPaymentRouteHandler(req, res);
});
// get payment stats
router.get('/stats', isAuthenticated, (req, res) => {
getPaymentStatsRouteHandler(req, res);
});
// get payment history
router.get('/history', isAuthenticated, (req, res) => {
getPaymentHistoryRouteHandler(req, res);
});
router.get('/:id', isAuthenticated, (req, res) => {
getPaymentRouteHandler(req, res);
});
// update multiple payments
router.put('/', isAuthenticated, async (req, res) => {
editMultiplePaymentsRouteHandler(req, res);
});
router.put('/:id', isAuthenticated, async (req, res) => {
editPaymentRouteHandler(req, res);
});
router.delete('/:id', isAuthenticated, async (req, res) => {
deletePaymentRouteHandler(req, res);
});
router.post('/:id/post', isAuthenticated, async (req, res) => {
postPaymentRouteHandler(req, res);
});
router.post('/:id/cancel', isAuthenticated, async (req, res) => {
cancelPaymentRouteHandler(req, res);
});
export default router;

View File

@ -30,9 +30,6 @@ import courierServiceRoutes from './management/courierservice.js';
import taxRateRoutes from './management/taxrates.js';
import taxRecordRoutes from './management/taxrecords.js';
import invoiceRoutes from './finance/invoices.js';
import paymentRoutes from './finance/payments.js';
import clientRoutes from './sales/clients.js';
import salesOrderRoutes from './sales/salesorders.js';
import noteRoutes from './misc/notes.js';
export {
@ -69,7 +66,4 @@ export {
taxRateRoutes,
taxRecordRoutes,
invoiceRoutes,
paymentRoutes,
clientRoutes,
salesOrderRoutes,
};

View File

@ -19,7 +19,6 @@ import {
router.get('/', isAuthenticated, (req, res) => {
const { page, limit, property, search, sort, order } = req.query;
const allowedFilters = [
'name',
'itemType',
'item',
'item._id',
@ -36,7 +35,6 @@ router.get('/', isAuthenticated, (req, res) => {
router.get('/properties', isAuthenticated, (req, res) => {
let properties = convertPropertiesString(req.query.properties);
const allowedFilters = [
'name',
'itemType',
'item',
'item._id',

View File

@ -1,59 +0,0 @@
import express from 'express';
import { isAuthenticated } from '../../keycloak.js';
import { getFilter, convertPropertiesString } from '../../utils.js';
const router = express.Router();
import {
listClientsRouteHandler,
getClientRouteHandler,
editClientRouteHandler,
newClientRouteHandler,
deleteClientRouteHandler,
listClientsByPropertiesRouteHandler,
getClientStatsRouteHandler,
getClientHistoryRouteHandler,
} from '../../services/sales/clients.js';
// list of clients
router.get('/', isAuthenticated, (req, res) => {
const { page, limit, property, search, sort, order } = req.query;
const allowedFilters = ['country', 'active', 'createdAt', 'updatedAt'];
const filter = getFilter(req.query, allowedFilters);
listClientsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
});
router.get('/properties', isAuthenticated, (req, res) => {
let properties = convertPropertiesString(req.query.properties);
const allowedFilters = ['country', 'active', 'createdAt', 'updatedAt'];
const filter = getFilter(req.query, allowedFilters, false);
listClientsByPropertiesRouteHandler(req, res, properties, filter);
});
router.post('/', isAuthenticated, (req, res) => {
newClientRouteHandler(req, res);
});
// get client stats
router.get('/stats', isAuthenticated, (req, res) => {
getClientStatsRouteHandler(req, res);
});
// get clients history
router.get('/history', isAuthenticated, (req, res) => {
getClientHistoryRouteHandler(req, res);
});
router.get('/:id', isAuthenticated, (req, res) => {
getClientRouteHandler(req, res);
});
router.put('/:id', isAuthenticated, async (req, res) => {
editClientRouteHandler(req, res);
});
router.delete('/:id', isAuthenticated, async (req, res) => {
deleteClientRouteHandler(req, res);
});
export default router;

View File

@ -1,84 +0,0 @@
import express from 'express';
import { isAuthenticated } from '../../keycloak.js';
import { getFilter, convertPropertiesString } from '../../utils.js';
const router = express.Router();
import {
listSalesOrdersRouteHandler,
getSalesOrderRouteHandler,
editSalesOrderRouteHandler,
editMultipleSalesOrdersRouteHandler,
newSalesOrderRouteHandler,
deleteSalesOrderRouteHandler,
listSalesOrdersByPropertiesRouteHandler,
getSalesOrderStatsRouteHandler,
getSalesOrderHistoryRouteHandler,
postSalesOrderRouteHandler,
confirmSalesOrderRouteHandler,
cancelSalesOrderRouteHandler,
} from '../../services/sales/salesorders.js';
// list of sales orders
router.get('/', isAuthenticated, (req, res) => {
const { page, limit, property, search, sort, order } = req.query;
const allowedFilters = ['client', 'state', 'value', 'client._id'];
const filter = getFilter(req.query, allowedFilters);
listSalesOrdersRouteHandler(req, res, page, limit, property, filter, search, sort, order);
});
router.get('/properties', isAuthenticated, (req, res) => {
let properties = convertPropertiesString(req.query.properties);
const allowedFilters = ['client', 'state.type', 'value', 'client._id'];
const filter = getFilter(req.query, allowedFilters, false);
var masterFilter = {};
if (req.query.masterFilter) {
masterFilter = getFilter(JSON.parse(req.query.masterFilter), allowedFilters, true);
}
listSalesOrdersByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
});
router.post('/', isAuthenticated, (req, res) => {
newSalesOrderRouteHandler(req, res);
});
// get sales order stats
router.get('/stats', isAuthenticated, (req, res) => {
getSalesOrderStatsRouteHandler(req, res);
});
// get sales orders history
router.get('/history', isAuthenticated, (req, res) => {
getSalesOrderHistoryRouteHandler(req, res);
});
router.get('/:id', isAuthenticated, (req, res) => {
getSalesOrderRouteHandler(req, res);
});
// update multiple sales orders
router.put('/', isAuthenticated, async (req, res) => {
editMultipleSalesOrdersRouteHandler(req, res);
});
router.put('/:id', isAuthenticated, async (req, res) => {
editSalesOrderRouteHandler(req, res);
});
router.delete('/:id', isAuthenticated, async (req, res) => {
deleteSalesOrderRouteHandler(req, res);
});
router.post('/:id/post', isAuthenticated, async (req, res) => {
postSalesOrderRouteHandler(req, res);
});
router.post('/:id/confirm', isAuthenticated, async (req, res) => {
confirmSalesOrderRouteHandler(req, res);
});
router.post('/:id/cancel', isAuthenticated, async (req, res) => {
cancelSalesOrderRouteHandler(req, res);
});
export default router;

View File

@ -14,8 +14,6 @@ import {
getModelHistory,
checkStates,
} from '../../database/database.js';
import { orderItemModel } from '../../database/schemas/inventory/orderitem.schema.js';
import { shipmentModel } from '../../database/schemas/inventory/shipment.schema.js';
const logger = log4js.getLogger('Invoices');
logger.level = config.server.logLevel;
@ -31,11 +29,7 @@ export const listInvoicesRouteHandler = async (
sort = '',
order = 'ascend'
) => {
const populateFields = [
{ path: 'to', strictPopulate: false, ref: 'client' },
{ path: 'from', strictPopulate: false, ref: 'vendor' },
{ path: 'order' },
];
const populateFields = ['vendor', 'customer', 'relatedOrder'];
const result = await listObjects({
model: invoiceModel,
page,
@ -65,7 +59,7 @@ export const listInvoicesByPropertiesRouteHandler = async (
filter = {},
masterFilter = {}
) => {
const populateFields = ['to', 'from', 'order'];
const populateFields = ['vendor', 'customer', 'relatedOrder'];
const result = await listObjectsByProperties({
model: invoiceModel,
properties,
@ -86,15 +80,7 @@ export const listInvoicesByPropertiesRouteHandler = async (
export const getInvoiceRouteHandler = async (req, res) => {
const id = req.params.id;
const populateFields = [
{ path: 'to', strictPopulate: false },
{ path: 'from', strictPopulate: false },
{ path: 'order' },
{ path: 'invoiceOrderItems.taxRate' },
{ path: 'invoiceShipments.taxRate' },
{ path: 'invoiceOrderItems.orderItem' },
{ path: 'invoiceShipments.shipment' },
];
const populateFields = ['vendor', 'customer', 'relatedOrder'];
const result = await getObject({
model: invoiceModel,
id,
@ -131,17 +117,12 @@ export const editInvoiceRouteHandler = async (req, res) => {
const updateData = {
updatedAt: new Date(),
vendor: req.body.vendor,
client: req.body.client,
customer: req.body.customer,
invoiceType: req.body.invoiceType,
invoiceDate: req.body.invoiceDate,
dueAt: req.body.dueDate,
issuedAt: req.body.issuedAt,
orderType: req.body.orderType,
order: req.body.order,
invoiceOrderItems: req.body.invoiceOrderItems,
invoiceShipments: req.body.invoiceShipments,
from: req.body.from,
to: req.body.to,
dueDate: req.body.dueDate,
relatedOrderType: req.body.relatedOrderType,
relatedOrder: req.body.relatedOrder,
};
// Create audit log before updating
const result = await editObject({
@ -166,7 +147,7 @@ export const editMultipleInvoicesRouteHandler = async (req, res) => {
const updates = req.body.map((update) => ({
_id: update._id,
vendor: update.vendor,
client: update.client,
customer: update.customer,
invoiceType: update.invoiceType,
}));
@ -192,90 +173,21 @@ export const editMultipleInvoicesRouteHandler = async (req, res) => {
};
export const newInvoiceRouteHandler = async (req, res) => {
const orderItems = await listObjects({
model: orderItemModel,
filter: { order: req.body.order, orderType: req.body.orderType },
});
const shipments = await listObjects({
model: shipmentModel,
filter: { order: req.body.order, orderType: req.body.orderType },
});
if (orderItems.error) {
logger.error('Error getting order items:', orderItems.error);
return res.status(orderItems.code).send(orderItems);
}
if (shipments.error) {
logger.error('Error getting shipments:', shipments.error);
return res.status(shipments.code).send(shipments);
}
const invoiceOrderItems = orderItems
.map((orderItem) => {
const invoicedAmountWithTax = orderItem.invoicedAmountWithTax || 0;
const totalAmountWithTax = orderItem.totalAmountWithTax || 0;
const invoicedAmount = orderItem.invoicedAmount || 0;
const totalAmount = orderItem.totalAmount || 0;
const quantity = (orderItem.quantity || 0) - (orderItem.invoicedQuantity || 0);
const taxRate = orderItem?.taxRate?._id;
if (invoicedAmountWithTax >= totalAmountWithTax || invoicedAmount >= totalAmount) {
return null;
}
var finalQuantity = quantity;
if (finalQuantity <= 0) {
finalQuantity = 1;
}
return {
orderItem: orderItem._id,
taxRate: taxRate,
invoiceAmountWithTax: totalAmountWithTax - invoicedAmountWithTax,
invoiceAmount: totalAmount - invoicedAmount,
invoiceQuantity: finalQuantity,
};
})
.filter((item) => item !== null);
const invoiceShipments = shipments
.map((shipment) => {
const invoicedAmount = shipment.invoicedAmount || 0;
const amountWithTax = shipment.amountWithTax || 0;
const invoicedAmountWithTax = shipment.invoicedAmountWithTax || 0;
const amount = shipment.amount || 0;
const taxRate = shipment?.taxRate || null;
if (invoicedAmountWithTax >= amountWithTax || invoicedAmount >= amount) {
return null;
}
return {
shipment: shipment._id,
taxRate: taxRate,
invoiceAmountWithTax: amountWithTax - invoicedAmountWithTax,
invoiceAmount: amount - invoicedAmount,
};
})
.filter((item) => item !== null);
const newData = {
updatedAt: new Date(),
vendor: req.body.vendor,
client: req.body.client,
issuedAt: req.body.issuedAt || new Date(),
dueAt: req.body.dueAt || new Date(),
orderType: req.body.orderType,
order: req.body.order,
customer: req.body.customer,
invoiceType: req.body.invoiceType || 'sales',
invoiceDate: req.body.invoiceDate || new Date(),
dueDate: req.body.dueDate,
relatedOrderType: req.body.relatedOrderType,
relatedOrder: req.body.relatedOrder,
totalAmount: 0,
totalAmountWithTax: 0,
totalTaxAmount: 0,
grandTotalAmount: 0,
shippingAmount: 0,
shippingAmountWithTax: 0,
invoiceOrderItems: invoiceOrderItems,
invoiceShipments: invoiceShipments,
from: req.body.from,
to: req.body.to,
};
const result = await newObject({
model: invoiceModel,
@ -335,48 +247,7 @@ export const getInvoiceHistoryRouteHandler = async (req, res) => {
res.send(result);
};
export const acknowledgeInvoiceRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Invoice with ID: ${id}`);
const checkStatesResult = await checkStates({ model: invoiceModel, id, states: ['sent'] });
if (checkStatesResult.error) {
logger.error('Error checking invoice states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Invoice is not in sent state.');
res.status(400).send({ error: 'Invoice is not in sent state.', code: 400 });
return;
}
const updateData = {
updatedAt: new Date(),
state: { type: 'acknowledged' },
acknowledgedAt: new Date(),
};
const result = await editObject({
model: invoiceModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error acknowledging invoice:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Acknowledged invoice with ID: ${id}`);
res.send(result);
};
export const postInvoiceRouteHandler = async (req, res) => {
export const sendInvoiceRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Invoice with ID: ${id}`);
@ -395,98 +266,10 @@ export const postInvoiceRouteHandler = async (req, res) => {
return;
}
const invoice = await getObject({
model: invoiceModel,
id,
});
const invoiceOrderItems = invoice.invoiceOrderItems;
const invoiceShipments = invoice.invoiceShipments;
for (const invoiceOrderItem of invoiceOrderItems) {
const orderItem = await getObject({
model: orderItemModel,
id: invoiceOrderItem.orderItem._id,
});
if (orderItem.error) {
logger.error('Error getting order item:', orderItem.error);
return res.status(orderItem.code).send(orderItem);
}
const invoiceQuantity = invoiceOrderItem.invoiceQuantity || 0;
const invoiceAmount = invoiceOrderItem.invoiceAmount || 0;
const invoiceAmountWithTax = invoiceOrderItem.invoiceAmountWithTax || 0;
const invoicedQuantity = orderItem.invoicedQuantity || 0;
const invoicedAmount = orderItem.invoicedAmount || 0;
const invoicedAmountWithTax = orderItem.invoicedAmountWithTax || 0;
var quantity = (orderItem.invoiceQuantity || 0) + invoiceQuantity;
if (quantity <= orderItem.quantity) {
quantity = orderItem.quantity;
}
const updateData = {
updatedAt: new Date(),
invoicedQuantity: invoicedQuantity + invoiceQuantity,
invoicedAmount: invoicedAmount + invoiceAmount,
invoicedAmountWithTax: invoicedAmountWithTax + invoiceAmountWithTax,
};
const result = await editObject({
model: orderItemModel,
id: orderItem._id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error updating order item:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`Updated order item with ID: ${orderItem._id}`);
}
for (const invoiceShipment of invoiceShipments) {
const shipmentId = invoiceShipment.shipment._id || invoiceShipment.shipment;
const shipment = await getObject({
model: shipmentModel,
id: shipmentId,
});
if (shipment.error) {
logger.error('Error getting shipment:', shipment.error);
return res.status(shipment.code).send(shipment);
}
const invoiceAmount = invoiceShipment.invoiceAmount || 0;
const invoiceAmountWithTax = invoiceShipment.invoiceAmountWithTax || 0;
const invoicedAmount = shipment.invoicedAmount || 0;
const invoicedAmountWithTax = shipment.invoicedAmountWithTax || 0;
const updateData = {
updatedAt: new Date(),
invoicedAmount: invoicedAmount + invoiceAmount,
invoicedAmountWithTax: invoicedAmountWithTax + invoiceAmountWithTax,
};
const result = await editObject({
model: shipmentModel,
id: shipment._id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error updating shipment:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`Updated shipment with ID: ${shipment._id}`);
}
if (invoiceOrderItems.error) {
logger.error('Error getting invoice order items:', invoiceOrderItems.error);
return res.status(invoiceOrderItems.code).send(invoiceOrderItems);
}
if (invoiceShipments.error) {
logger.error('Error getting invoice shipments:', invoiceShipments.error);
return res.status(invoiceShipments.code).send(invoiceShipments);
}
const updateData = {
updatedAt: new Date(),
state: { type: 'sent' },
postedAt: new Date(),
sentAt: new Date(),
};
const result = await editObject({
model: invoiceModel,
@ -496,12 +279,57 @@ export const postInvoiceRouteHandler = async (req, res) => {
});
if (result.error) {
logger.error('Error posting invoice:', result.error);
logger.error('Error sending invoice:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Posted invoice with ID: ${id}`);
logger.debug(`Sent invoice with ID: ${id}`);
res.send(result);
};
export const markInvoicePaidRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Invoice with ID: ${id}`);
const checkStatesResult = await checkStates({
model: invoiceModel,
id,
states: ['sent', 'partiallyPaid'],
});
if (checkStatesResult.error) {
logger.error('Error checking invoice states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Invoice is not in sent or partially paid state.');
res.status(400).send({ error: 'Invoice is not in sent or partially paid state.', code: 400 });
return;
}
const updateData = {
updatedAt: new Date(),
state: { type: 'paid' },
paidAt: new Date(),
};
const result = await editObject({
model: invoiceModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error marking invoice as paid:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Marked invoice as paid with ID: ${id}`);
res.send(result);
};

View File

@ -1,373 +0,0 @@
import config from '../../config.js';
import { paymentModel } from '../../database/schemas/finance/payment.schema.js';
import log4js from 'log4js';
import mongoose from 'mongoose';
import {
deleteObject,
listObjects,
getObject,
editObject,
editObjects,
newObject,
listObjectsByProperties,
getModelStats,
getModelHistory,
checkStates,
} from '../../database/database.js';
import { invoiceModel } from '../../database/schemas/finance/invoice.schema.js';
const logger = log4js.getLogger('Payments');
logger.level = config.server.logLevel;
export const listPaymentsRouteHandler = async (
req,
res,
page = 1,
limit = 25,
property = '',
filter = {},
search = '',
sort = '',
order = 'ascend'
) => {
const populateFields = ['vendor', 'client', 'invoice'];
const result = await listObjects({
model: paymentModel,
page,
limit,
property,
filter,
search,
sort,
order,
populate: populateFields,
});
if (result?.error) {
logger.error('Error listing payments.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of payments (Page ${page}, Limit ${limit}). Count: ${result.length}`);
res.send(result);
};
export const listPaymentsByPropertiesRouteHandler = async (
req,
res,
properties = '',
filter = {},
masterFilter = {}
) => {
const populateFields = ['vendor', 'client', 'invoice'];
const result = await listObjectsByProperties({
model: paymentModel,
properties,
filter,
populate: populateFields,
masterFilter,
});
if (result?.error) {
logger.error('Error listing payments.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of payments. Count: ${result.length}`);
res.send(result);
};
export const getPaymentRouteHandler = async (req, res) => {
const id = req.params.id;
const populateFields = [
{ path: 'vendor' },
{ path: 'client' },
{ path: 'invoice' },
];
const result = await getObject({
model: paymentModel,
id,
populate: populateFields,
});
if (result?.error) {
logger.warn(`Payment not found with supplied id.`);
return res.status(result.code).send(result);
}
logger.debug(`Retrieved payment with ID: ${id}`);
res.send(result);
};
export const editPaymentRouteHandler = async (req, res) => {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Payment with ID: ${id}`);
const checkStatesResult = await checkStates({ model: paymentModel, id, states: ['draft'] });
if (checkStatesResult.error) {
logger.error('Error checking payment states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Payment is not in draft state.');
res.status(400).send({ error: 'Payment is not in draft state.', code: 400 });
return;
}
const updateData = {
updatedAt: new Date(),
vendor: req.body.vendor,
client: req.body.client,
invoice: req.body.invoice,
amount: req.body.amount,
paymentDate: req.body.paymentDate,
paymentMethod: req.body.paymentMethod,
notes: req.body.notes,
};
// Create audit log before updating
const result = await editObject({
model: paymentModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error editing payment:', result.error);
res.status(result).send(result);
return;
}
logger.debug(`Edited payment with ID: ${id}`);
res.send(result);
};
export const editMultiplePaymentsRouteHandler = async (req, res) => {
const updates = req.body.map((update) => ({
_id: update._id,
vendor: update.vendor,
client: update.client,
invoice: update.invoice,
}));
if (!Array.isArray(updates)) {
return res.status(400).send({ error: 'Body must be an array of updates.', code: 400 });
}
const result = await editObjects({
model: paymentModel,
updates,
user: req.user,
});
if (result.error) {
logger.error('Error editing payments:', result.error);
res.status(result.code || 500).send(result);
return;
}
logger.debug(`Edited ${updates.length} payments`);
res.send(result);
};
export const newPaymentRouteHandler = async (req, res) => {
// Get invoice to populate vendor/client
const invoice = await getObject({
model: invoiceModel,
id: req.body.invoice,
populate: [{ path: 'vendor' }, { path: 'client' }],
});
if (invoice.error) {
logger.error('Error getting invoice:', invoice.error);
return res.status(invoice.code).send(invoice);
}
const newData = {
updatedAt: new Date(),
vendor: invoice.vendor?._id || req.body.vendor,
client: invoice.client?._id || req.body.client,
invoice: req.body.invoice,
amount: req.body.amount || 0,
paymentDate: req.body.paymentDate || new Date(),
paymentMethod: req.body.paymentMethod,
notes: req.body.notes,
};
const result = await newObject({
model: paymentModel,
newData,
user: req.user,
});
if (result.error) {
logger.error('No payment created:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`New payment with ID: ${result._id}`);
res.send(result);
};
export const deletePaymentRouteHandler = async (req, res) => {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Payment with ID: ${id}`);
const result = await deleteObject({
model: paymentModel,
id,
user: req.user,
});
if (result.error) {
logger.error('No payment deleted:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`Deleted payment with ID: ${result._id}`);
res.send(result);
};
export const getPaymentStatsRouteHandler = async (req, res) => {
const result = await getModelStats({ model: paymentModel });
if (result?.error) {
logger.error('Error fetching payment stats:', result.error);
return res.status(result.code).send(result);
}
logger.trace('Payment stats:', result);
res.send(result);
};
export const getPaymentHistoryRouteHandler = async (req, res) => {
const from = req.query.from;
const to = req.query.to;
const result = await getModelHistory({ model: paymentModel, from, to });
if (result?.error) {
logger.error('Error fetching payment history:', result.error);
return res.status(result.code).send(result);
}
logger.trace('Payment history:', result);
res.send(result);
};
export const postPaymentRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Payment with ID: ${id}`);
const checkStatesResult = await checkStates({ model: paymentModel, id, states: ['draft'] });
if (checkStatesResult.error) {
logger.error('Error checking payment states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Payment is not in draft state.');
res.status(400).send({ error: 'Payment is not in draft state.', code: 400 });
return;
}
const payment = await getObject({
model: paymentModel,
id,
populate: [{ path: 'invoice' }],
});
if (payment.error) {
logger.error('Error getting payment:', payment.error);
return res.status(payment.code).send(payment);
}
// Update invoice paid amounts if needed
if (payment.invoice) {
const invoice = await getObject({
model: invoiceModel,
id: payment.invoice._id || payment.invoice,
});
if (!invoice.error) {
// You can add logic here to update invoice paid amounts
// This is a simplified version - adjust based on your business logic
}
}
const updateData = {
updatedAt: new Date(),
state: { type: 'posted' },
postedAt: new Date(),
};
const result = await editObject({
model: paymentModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error posting payment:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Posted payment with ID: ${id}`);
res.send(result);
};
export const cancelPaymentRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Payment with ID: ${id}`);
const checkStatesResult = await checkStates({
model: paymentModel,
id,
states: ['draft', 'posted'],
});
if (checkStatesResult.error) {
logger.error('Error checking payment states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Payment is not in a cancellable state.');
res.status(400).send({
error: 'Payment is not in a cancellable state (must be draft or posted).',
code: 400,
});
return;
}
const updateData = {
updatedAt: new Date(),
state: { type: 'cancelled' },
cancelledAt: new Date(),
};
const result = await editObject({
model: paymentModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error cancelling payment:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Cancelled payment with ID: ${id}`);
res.send(result);
};

View File

@ -135,7 +135,6 @@ export const editOrderItemRouteHandler = async (req, res) => {
const updateData = {
updatedAt: new Date(),
name: req.body.name,
itemType: req.body.itemType,
item: req.body.item,
orderType: req.body.orderType,
@ -170,7 +169,6 @@ export const editOrderItemRouteHandler = async (req, res) => {
export const editMultipleOrderItemsRouteHandler = async (req, res) => {
const updates = req.body.map((update) => ({
_id: update._id,
name: update.name,
itemType: update.itemType,
item: update.item,
orderType: update.orderType,
@ -208,7 +206,6 @@ export const editMultipleOrderItemsRouteHandler = async (req, res) => {
export const newOrderItemRouteHandler = async (req, res) => {
const newData = {
updatedAt: new Date(),
name: req.body.name,
purchaseOrder: req.body.purchaseOrder,
state: { type: 'draft' },
itemType: req.body.itemType,

View File

@ -1,189 +0,0 @@
import config from '../../config.js';
import { clientModel } from '../../database/schemas/sales/client.schema.js';
import log4js from 'log4js';
import mongoose from 'mongoose';
import {
deleteObject,
listObjects,
getObject,
editObject,
newObject,
listObjectsByProperties,
getModelStats,
getModelHistory,
} from '../../database/database.js';
const logger = log4js.getLogger('Clients');
logger.level = config.server.logLevel;
export const listClientsRouteHandler = async (
req,
res,
page = 1,
limit = 25,
property = '',
filter = {},
search = '',
sort = '',
order = 'ascend'
) => {
const result = await listObjects({
model: clientModel,
page,
limit,
property,
filter,
search,
sort,
order,
});
if (result?.error) {
logger.error('Error listing clients.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of clients (Page ${page}, Limit ${limit}). Count: ${result.length}.`);
res.send(result);
};
export const listClientsByPropertiesRouteHandler = async (
req,
res,
properties = '',
filter = {}
) => {
const result = await listObjectsByProperties({
model: clientModel,
properties,
filter,
});
if (result?.error) {
logger.error('Error listing clients.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of clients. Count: ${result.length}`);
res.send(result);
};
export const getClientRouteHandler = async (req, res) => {
const id = req.params.id;
const result = await getObject({
model: clientModel,
id,
});
if (result?.error) {
logger.warn(`Client not found with supplied id.`);
return res.status(result.code).send(result);
}
logger.debug(`Retreived client with ID: ${id}`);
res.send(result);
};
export const editClientRouteHandler = async (req, res) => {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Client with ID: ${id}`);
const updateData = {
updatedAt: new Date(),
country: req.body.country,
name: req.body.name,
phone: req.body.phone,
email: req.body.email,
address: req.body.address,
active: req.body.active,
tags: req.body.tags,
};
// Create audit log before updating
const result = await editObject({
model: clientModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error editing client:', result.error);
res.status(result).send(result);
return;
}
logger.debug(`Edited client with ID: ${id}`);
res.send(result);
};
export const newClientRouteHandler = async (req, res) => {
const newData = {
updatedAt: new Date(),
country: req.body.country,
name: req.body.name,
phone: req.body.phone,
email: req.body.email,
address: req.body.address,
active: req.body.active,
tags: req.body.tags,
};
const result = await newObject({
model: clientModel,
newData,
user: req.user,
});
if (result.error) {
logger.error('No client created:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`New client with ID: ${result._id}`);
res.send(result);
};
export const deleteClientRouteHandler = async (req, res) => {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Client with ID: ${id}`);
const result = await deleteObject({
model: clientModel,
id,
user: req.user,
});
if (result.error) {
logger.error('No client deleted:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`Deleted client with ID: ${result._id}`);
res.send(result);
};
export const getClientStatsRouteHandler = async (req, res) => {
const result = await getModelStats({ model: clientModel });
if (result?.error) {
logger.error('Error fetching client stats:', result.error);
return res.status(result.code).send(result);
}
logger.trace('Client stats:', result);
res.send(result);
};
export const getClientHistoryRouteHandler = async (req, res) => {
const from = req.query.from;
const to = req.query.to;
const result = await getModelHistory({ model: clientModel, from, to });
if (result?.error) {
logger.error('Error fetching client history:', result.error);
return res.status(result.code).send(result);
}
logger.trace('Client history:', result);
res.send(result);
};

View File

@ -1,460 +0,0 @@
import config from '../../config.js';
import { salesOrderModel } from '../../database/schemas/sales/salesorder.schema.js';
import log4js from 'log4js';
import mongoose from 'mongoose';
import {
deleteObject,
listObjects,
getObject,
editObject,
editObjects,
newObject,
listObjectsByProperties,
getModelStats,
getModelHistory,
checkStates,
} from '../../database/database.js';
import { orderItemModel } from '../../database/schemas/inventory/orderitem.schema.js';
import { shipmentModel } from '../../database/schemas/inventory/shipment.schema.js';
const logger = log4js.getLogger('Sales Orders');
logger.level = config.server.logLevel;
export const listSalesOrdersRouteHandler = async (
req,
res,
page = 1,
limit = 25,
property = '',
filter = {},
search = '',
sort = '',
order = 'ascend'
) => {
const result = await listObjects({
model: salesOrderModel,
page,
limit,
property,
filter,
search,
sort,
order,
populate: ['client'],
});
if (result?.error) {
logger.error('Error listing sales orders.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of sales orders (Page ${page}, Limit ${limit}). Count: ${result.length}`);
res.send(result);
};
export const listSalesOrdersByPropertiesRouteHandler = async (
req,
res,
properties = '',
filter = {},
masterFilter = {}
) => {
const result = await listObjectsByProperties({
model: salesOrderModel,
properties,
filter,
populate: ['client'],
masterFilter,
});
if (result?.error) {
logger.error('Error listing sales orders.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of sales orders. Count: ${result.length}`);
res.send(result);
};
export const getSalesOrderRouteHandler = async (req, res) => {
const id = req.params.id;
const result = await getObject({
model: salesOrderModel,
id,
populate: ['client'],
});
if (result?.error) {
logger.warn(`Sales Order not found with supplied id.`);
return res.status(result.code).send(result);
}
logger.debug(`Retreived sales order with ID: ${id}`);
res.send(result);
};
export const editSalesOrderRouteHandler = async (req, res) => {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Sales Order with ID: ${id}`);
const checkStatesResult = await checkStates({ model: salesOrderModel, id, states: ['draft'] });
if (checkStatesResult.error) {
logger.error('Error checking sales order states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Sales order is not in draft state.');
res.status(400).send({ error: 'Sales order is not in draft state.', code: 400 });
return;
}
const updateData = {
updatedAt: new Date(),
client: req.body.client,
};
// Create audit log before updating
const result = await editObject({
model: salesOrderModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error editing sales order:', result.error);
res.status(result).send(result);
return;
}
logger.debug(`Edited sales order with ID: ${id}`);
res.send(result);
};
export const editMultipleSalesOrdersRouteHandler = async (req, res) => {
const updates = req.body.map((update) => ({
_id: update._id,
client: update.client,
}));
if (!Array.isArray(updates)) {
return res.status(400).send({ error: 'Body must be an array of updates.', code: 400 });
}
const result = await editObjects({
model: salesOrderModel,
updates,
user: req.user,
});
if (result.error) {
logger.error('Error editing sales orders:', result.error);
res.status(result.code || 500).send(result);
return;
}
logger.debug(`Edited ${updates.length} sales orders`);
res.send(result);
};
export const newSalesOrderRouteHandler = async (req, res) => {
const newData = {
updatedAt: new Date(),
client: req.body.client,
totalAmount: 0,
totalAmountWithTax: 0,
totalTaxAmount: 0,
};
const result = await newObject({
model: salesOrderModel,
newData,
user: req.user,
});
if (result.error) {
logger.error('No sales order created:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`New sales order with ID: ${result._id}`);
res.send(result);
};
export const deleteSalesOrderRouteHandler = async (req, res) => {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Sales Order with ID: ${id}`);
const result = await deleteObject({
model: salesOrderModel,
id,
user: req.user,
});
if (result.error) {
logger.error('No sales order deleted:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`Deleted sales order with ID: ${result._id}`);
res.send(result);
};
export const getSalesOrderStatsRouteHandler = async (req, res) => {
const result = await getModelStats({ model: salesOrderModel });
if (result?.error) {
logger.error('Error fetching sales order stats:', result.error);
return res.status(result.code).send(result);
}
logger.trace('Sales order stats:', result);
res.send(result);
};
export const getSalesOrderHistoryRouteHandler = async (req, res) => {
const from = req.query.from;
const to = req.query.to;
const result = await getModelHistory({ model: salesOrderModel, from, to });
if (result?.error) {
logger.error('Error fetching sales order history:', result.error);
return res.status(result.code).send(result);
}
logger.trace('Sales order history:', result);
res.send(result);
};
export const postSalesOrderRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Sales Order with ID: ${id}`);
const checkStatesResult = await checkStates({ model: salesOrderModel, id, states: ['draft'] });
if (checkStatesResult.error) {
logger.error('Error checking sales order states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Sales order is not in draft state.');
res.status(400).send({ error: 'Sales order is not in draft state.', code: 400 });
return;
}
const orderItemsResult = await listObjects({
model: orderItemModel,
filter: { order: id, orderType: 'salesOrder' },
pagination: false,
});
const shipmentsResult = await listObjects({
model: shipmentModel,
filter: { order: id, orderType: 'salesOrder' },
pagination: false,
});
for (const orderItem of orderItemsResult) {
if (orderItem.state.type != 'draft') {
logger.warn(`Order item ${orderItem._id} is not in draft state.`);
return res
.status(400)
.send({ error: `Order item ${orderItem._reference} not in draft state.`, code: 400 });
}
if (!orderItem?.shipment || orderItem?.shipment == null) {
logger.warn(`Order item ${orderItem._id} does not have a shipment.`);
return res
.status(400)
.send({ error: `Order item ${orderItem._reference} does not have a shipment.`, code: 400 });
}
}
for (const shipment of shipmentsResult) {
if (shipment.state.type != 'draft') {
logger.warn(`Shipment ${shipment._id} is not in draft state.`);
return res
.status(400)
.send({ error: `Shipment ${shipment._reference} not in draft state.`, code: 400 });
}
}
for (const orderItem of orderItemsResult) {
await editObject({
model: orderItemModel,
id: orderItem._id,
updateData: {
state: { type: 'ordered' },
orderedAt: new Date(),
},
user: req.user,
});
}
for (const shipment of shipmentsResult) {
await editObject({
model: shipmentModel,
id: shipment._id,
updateData: {
state: { type: 'planned' },
},
user: req.user,
});
}
const updateData = {
updatedAt: new Date(),
state: { type: 'sent' },
postedAt: new Date(),
};
const result = await editObject({
model: salesOrderModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error posting sales order:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Posted sales order with ID: ${id}`);
res.send(result);
};
export const confirmSalesOrderRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Sales Order with ID: ${id}`);
const checkStatesResult = await checkStates({ model: salesOrderModel, id, states: ['sent'] });
if (checkStatesResult.error) {
logger.error('Error checking sales order states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Sales order is not in sent state.');
res.status(400).send({ error: 'Sales order is not in sent state.', code: 400 });
return;
}
const updateData = {
updatedAt: new Date(),
state: { type: 'confirmed' },
confirmedAt: new Date(),
};
const result = await editObject({
model: salesOrderModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error confirming sales order:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Confirmed sales order with ID: ${id}`);
res.send(result);
};
export const cancelSalesOrderRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Sales Order with ID: ${id}`);
const checkStatesResult = await checkStates({
model: salesOrderModel,
id,
states: ['sent', 'confirmed', 'partiallyShipped', 'shipped', 'partiallyDelivered'],
});
if (checkStatesResult.error) {
logger.error('Error checking sales order states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Sales order is not in a cancellable state.');
res.status(400).send({
error: 'Sales order is not in a cancellable state (must be sent, confirmed, partiallyShipped, shipped, or partiallyDelivered).',
code: 400,
});
return;
}
const orderItemsResult = await listObjects({
model: orderItemModel,
filter: { order: id, orderType: 'salesOrder' },
pagination: false,
});
const shipmentsResult = await listObjects({
model: shipmentModel,
filter: { order: id, orderType: 'salesOrder' },
pagination: false,
});
const allowedOrderItemStates = ['ordered', 'shipped'];
const allowedShipmentStates = ['shipped', 'planned'];
for (const orderItem of orderItemsResult) {
if (allowedOrderItemStates.includes(orderItem.state.type)) {
await editObject({
model: orderItemModel,
id: orderItem._id,
updateData: {
state: { type: 'cancelled' },
},
user: req.user,
});
}
}
for (const shipment of shipmentsResult) {
if (allowedShipmentStates.includes(shipment.state.type)) {
await editObject({
model: shipmentModel,
id: shipment._id,
updateData: {
state: { type: 'cancelled' },
},
user: req.user,
});
}
}
const updateData = {
updatedAt: new Date(),
state: { type: 'cancelled' },
cancelledAt: new Date(),
};
const result = await editObject({
model: salesOrderModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error cancelling sales order:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Cancelled sales order with ID: ${id}`);
res.send(result);
};