Added purchase orders and couriers.

This commit is contained in:
Tom Butcher 2025-12-03 03:40:06 +00:00
parent 82d47e08a2
commit 944ad34f89
16 changed files with 721 additions and 29 deletions

View File

@ -20,6 +20,7 @@ import {
materialRoutes,
partStockRoutes,
filamentStockRoutes,
purchaseOrderRoutes,
stockAuditRoutes,
stockEventRoutes,
auditLogRoutes,
@ -30,6 +31,8 @@ import {
documentTemplatesRoutes,
documentPrintersRoutes,
documentJobsRoutes,
courierRoutes,
courierServiceRoutes,
} from './routes/index.js';
import path from 'path';
import * as fs from 'fs';
@ -123,6 +126,7 @@ app.use('/vendors', vendorRoutes);
app.use('/materials', materialRoutes);
app.use('/partstocks', partStockRoutes);
app.use('/filamentstocks', filamentStockRoutes);
app.use('/purchaseorders', purchaseOrderRoutes);
app.use('/stockevents', stockEventRoutes);
app.use('/stockaudits', stockAuditRoutes);
app.use('/auditlogs', auditLogRoutes);
@ -131,6 +135,8 @@ app.use('/documentsizes', documentSizesRoutes);
app.use('/documenttemplates', documentTemplatesRoutes);
app.use('/documentprinters', documentPrintersRoutes);
app.use('/documentjobs', documentJobsRoutes);
app.use('/couriers', courierRoutes);
app.use('/courierservices', courierServiceRoutes);
app.use('/notes', noteRoutes);
if (process.env.SCHEDULE_HOUR) {

View File

@ -14,6 +14,7 @@ import vendorRoutes from './management/vendors.js';
import materialRoutes from './management/materials.js';
import partStockRoutes from './inventory/partstocks.js';
import filamentStockRoutes from './inventory/filamentstocks.js';
import purchaseOrderRoutes from './inventory/purchaseorders.js';
import stockEventRoutes from './inventory/stockevents.js';
import stockAuditRoutes from './inventory/stockaudits.js';
import auditLogRoutes from './management/auditlogs.js';
@ -22,6 +23,8 @@ import documentSizesRoutes from './management/documentsizes.js';
import documentTemplatesRoutes from './management/documenttemplates.js';
import documentPrintersRoutes from './management/documentprinters.js';
import documentJobsRoutes from './management/documentjobs.js';
import courierRoutes from './management/courier.js';
import courierServiceRoutes from './management/courierservice.js';
import noteRoutes from './misc/notes.js';
export {
@ -41,6 +44,7 @@ export {
materialRoutes,
partStockRoutes,
filamentStockRoutes,
purchaseOrderRoutes,
stockEventRoutes,
stockAuditRoutes,
auditLogRoutes,
@ -50,4 +54,6 @@ export {
documentTemplatesRoutes,
documentPrintersRoutes,
documentJobsRoutes,
courierRoutes,
courierServiceRoutes,
};

View File

@ -0,0 +1,50 @@
import express from 'express';
import { isAuthenticated } from '../../keycloak.js';
import { getFilter, convertPropertiesString } from '../../utils.js';
const router = express.Router();
import {
listPurchaseOrdersRouteHandler,
getPurchaseOrderRouteHandler,
editPurchaseOrderRouteHandler,
newPurchaseOrderRouteHandler,
deletePurchaseOrderRouteHandler,
listPurchaseOrdersByPropertiesRouteHandler,
} from '../../services/inventory/purchaseorders.js';
// list of purchase orders
router.get('/', isAuthenticated, (req, res) => {
const { page, limit, property, search, sort, order } = req.query;
const allowedFilters = ['vendor', 'state', 'value', 'vendor._id'];
const filter = getFilter(req.query, allowedFilters);
listPurchaseOrdersRouteHandler(req, res, page, limit, property, filter, search, sort, order);
});
router.get('/properties', isAuthenticated, (req, res) => {
let properties = convertPropertiesString(req.query.properties);
const allowedFilters = ['vendor', 'state.type', 'value', 'vendor._id'];
const filter = getFilter(req.query, allowedFilters, false);
var masterFilter = {};
if (req.query.masterFilter) {
masterFilter = JSON.parse(req.query.masterFilter);
}
listPurchaseOrdersByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
});
router.post('/', isAuthenticated, (req, res) => {
newPurchaseOrderRouteHandler(req, res);
});
router.get('/:id', isAuthenticated, (req, res) => {
getPurchaseOrderRouteHandler(req, res);
});
router.put('/:id', isAuthenticated, async (req, res) => {
editPurchaseOrderRouteHandler(req, res);
});
router.delete('/:id', isAuthenticated, async (req, res) => {
deletePurchaseOrderRouteHandler(req, res);
});
export default router;

View File

@ -0,0 +1,46 @@
import express from 'express';
import { isAuthenticated } from '../../keycloak.js';
import { getFilter, convertPropertiesString } from '../../utils.js';
const router = express.Router();
import {
listCouriersRouteHandler,
getCourierRouteHandler,
editCourierRouteHandler,
newCourierRouteHandler,
deleteCourierRouteHandler,
listCouriersByPropertiesRouteHandler,
} from '../../services/management/courier.js';
// list of couriers
router.get('/', isAuthenticated, (req, res) => {
const { page, limit, property, search, sort, order } = req.query;
const allowedFilters = ['name', 'website', 'email', 'phone', 'contact', 'country'];
const filter = getFilter(req.query, allowedFilters);
listCouriersRouteHandler(req, res, page, limit, property, filter, search, sort, order);
});
router.get('/properties', isAuthenticated, (req, res) => {
let properties = convertPropertiesString(req.query.properties);
const allowedFilters = ['name', 'website', 'email', 'phone', 'contact', 'country'];
const filter = getFilter(req.query, allowedFilters, false);
listCouriersByPropertiesRouteHandler(req, res, properties, filter);
});
router.post('/', isAuthenticated, (req, res) => {
newCourierRouteHandler(req, res);
});
router.get('/:id', isAuthenticated, (req, res) => {
getCourierRouteHandler(req, res);
});
router.put('/:id', isAuthenticated, async (req, res) => {
editCourierRouteHandler(req, res);
});
router.delete('/:id', isAuthenticated, async (req, res) => {
deleteCourierRouteHandler(req, res);
});
export default router;

View File

@ -0,0 +1,46 @@
import express from 'express';
import { isAuthenticated } from '../../keycloak.js';
import { getFilter, convertPropertiesString } from '../../utils.js';
const router = express.Router();
import {
listCourierServicesRouteHandler,
getCourierServiceRouteHandler,
editCourierServiceRouteHandler,
newCourierServiceRouteHandler,
deleteCourierServiceRouteHandler,
listCourierServicesByPropertiesRouteHandler,
} from '../../services/management/courierservice.js';
// list of courier services
router.get('/', isAuthenticated, (req, res) => {
const { page, limit, property, search, sort, order } = req.query;
const allowedFilters = ['courier._id', 'name', 'active', 'deliveryTime'];
const filter = getFilter(req.query, allowedFilters);
listCourierServicesRouteHandler(req, res, page, limit, property, filter, search, sort, order);
});
router.get('/properties', isAuthenticated, (req, res) => {
let properties = convertPropertiesString(req.query.properties);
const allowedFilters = ['courier._id', 'name', 'active', 'deliveryTime', 'courier'];
const filter = getFilter(req.query, allowedFilters, false);
listCourierServicesByPropertiesRouteHandler(req, res, properties, filter);
});
router.post('/', isAuthenticated, (req, res) => {
newCourierServiceRouteHandler(req, res);
});
router.get('/:id', isAuthenticated, (req, res) => {
getCourierServiceRouteHandler(req, res);
});
router.put('/:id', isAuthenticated, async (req, res) => {
editCourierServiceRouteHandler(req, res);
});
router.delete('/:id', isAuthenticated, async (req, res) => {
deleteCourierServiceRouteHandler(req, res);
});
export default router;

View File

@ -15,7 +15,7 @@ import {
// list of parts
router.get('/', isAuthenticated, (req, res) => {
const { page, limit, property, search, sort, order } = req.query;
const allowedFilters = ['product._id', '_id', 'name', 'globalPrice'];
const allowedFilters = ['product._id', '_id', 'name'];
const filter = getFilter(req.query, allowedFilters);
listPartsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
});

View File

@ -2,32 +2,23 @@ import mongoose from 'mongoose';
import { generateId } from '../../utils.js';
const { Schema } = mongoose;
const itemSchema = new Schema({
itemType: { type: String, required: true },
item: { type: Schema.Types.ObjectId, refPath: 'itemType', required: true },
quantity: { type: Number, required: true },
price: { type: Number, required: true },
});
const purchaseOrderSchema = new Schema(
{
_reference: { type: String, default: () => generateId()() },
value: { type: Number, required: true },
unit: { type: String, required: true },
parent: {
type: Schema.Types.ObjectId,
refPath: 'parentType',
required: true,
},
parentType: {
type: String,
required: true,
enum: ['filamentStock', 'partStock', 'productStock'], // Add other models as needed
},
owner: {
type: Schema.Types.ObjectId,
refPath: 'ownerType',
required: true,
},
ownerType: {
type: String,
required: true,
enum: ['user', 'subJob', 'stockAudit'],
},
cost: { net: { type: Number, required: true }, gross: { type: Number, required: true } },
items: [itemSchema],
timestamp: { type: Date, default: Date.now },
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true },
state: {
type: { type: String, required: true, default: 'draft' },
},
},
{ timestamps: true }
);

View File

@ -0,0 +1,23 @@
import mongoose from 'mongoose';
import { generateId } from '../../utils.js';
const courierSchema = new mongoose.Schema(
{
_reference: { type: String, default: () => generateId()() },
name: { required: true, type: String },
website: { required: false, type: String },
email: { required: false, type: String },
phone: { required: false, type: String },
contact: { required: false, type: String },
country: { required: false, type: String },
},
{ timestamps: true }
);
courierSchema.virtual('id').get(function () {
return this._id;
});
courierSchema.set('toJSON', { virtuals: true });
export const courierModel = mongoose.model('courier', courierSchema);

View File

@ -0,0 +1,24 @@
import mongoose from 'mongoose';
import { generateId } from '../../utils.js';
const { Schema } = mongoose;
const courierServiceSchema = new mongoose.Schema(
{
_reference: { type: String, default: () => generateId()() },
name: { required: true, type: String },
courier: { type: Schema.Types.ObjectId, ref: 'courier', required: true },
active: { required: true, type: Boolean },
tracked: { required: true, type: Boolean },
deliveryTime: { required: true, type: Number },
website: { required: false, type: String },
},
{ timestamps: true }
);
courierServiceSchema.virtual('id').get(function () {
return this._id;
});
courierServiceSchema.set('toJSON', { virtuals: true });
export const courierServiceModel = mongoose.model('courierService', courierServiceSchema);

View File

@ -8,9 +8,9 @@ const partSchema = new Schema(
_reference: { type: String, default: () => generateId()() },
name: { type: String, required: true },
fileName: { type: String, required: false },
globalPricing: { type: Boolean, default: true },
priceMode: { type: String, default: 'margin' },
amount: { type: Number, required: false },
price: { type: Number, required: true },
cost: { type: Number, required: true },
margin: { type: Number, required: false },
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true },
file: { type: mongoose.SchemaTypes.ObjectId, ref: 'file', required: false },

View File

View File

@ -0,0 +1,163 @@
import dotenv from 'dotenv';
import { purchaseOrderModel } from '../../schemas/inventory/purchaseorder.schema.js';
import log4js from 'log4js';
import mongoose from 'mongoose';
import {
deleteObject,
listObjects,
getObject,
editObject,
newObject,
listObjectsByProperties,
} from '../../database/database.js';
dotenv.config();
const logger = log4js.getLogger('Purchase Orders');
logger.level = process.env.LOG_LEVEL;
export const listPurchaseOrdersRouteHandler = async (
req,
res,
page = 1,
limit = 25,
property = '',
filter = {},
search = '',
sort = '',
order = 'ascend'
) => {
const result = await listObjects({
model: purchaseOrderModel,
page,
limit,
property,
filter,
search,
sort,
order,
populate: ['vendor'],
});
if (result?.error) {
logger.error('Error listing purchase orders.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of purchase orders (Page ${page}, Limit ${limit}). Count: ${result.length}`);
res.send(result);
};
export const listPurchaseOrdersByPropertiesRouteHandler = async (
req,
res,
properties = '',
filter = {},
masterFilter = {}
) => {
const result = await listObjectsByProperties({
model: purchaseOrderModel,
properties,
filter,
populate: ['vendor'],
masterFilter,
});
if (result?.error) {
logger.error('Error listing purchase orders.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of purchase orders. Count: ${result.length}`);
res.send(result);
};
export const getPurchaseOrderRouteHandler = async (req, res) => {
const id = req.params.id;
const result = await getObject({
model: purchaseOrderModel,
id,
populate: ['vendor', 'items.item'],
});
if (result?.error) {
logger.warn(`Purchase Order not found with supplied id.`);
return res.status(result.code).send(result);
}
logger.debug(`Retreived purchase order with ID: ${id}`);
res.send(result);
};
export const editPurchaseOrderRouteHandler = async (req, res) => {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Purchase Order with ID: ${id}`);
const updateData = {
updatedAt: new Date(),
vendor: req.body.vendor,
items: req.body.items,
cost: req.body.cost,
};
// Create audit log before updating
const result = await editObject({
model: purchaseOrderModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error editing purchase order:', result.error);
res.status(result).send(result);
return;
}
logger.debug(`Edited purchase order with ID: ${id}`);
res.send(result);
};
export const newPurchaseOrderRouteHandler = async (req, res) => {
const newData = {
updatedAt: new Date(),
vendor: req.body.vendor,
items: req.body.items,
cost: req.body.cost,
};
const result = await newObject({
model: purchaseOrderModel,
newData,
user: req.user,
});
if (result.error) {
logger.error('No purchase order created:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`New purchase order with ID: ${result._id}`);
res.send(result);
};
export const deletePurchaseOrderRouteHandler = async (req, res) => {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Purchase Order with ID: ${id}`);
const result = await deleteObject({
model: purchaseOrderModel,
id,
user: req.user,
});
if (result.error) {
logger.error('No purchase order deleted:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`Deleted purchase order with ID: ${result._id}`);
res.send(result);
};

View File

@ -0,0 +1,164 @@
import dotenv from 'dotenv';
import { courierModel } from '../../schemas/management/courier.schema.js';
import log4js from 'log4js';
import mongoose from 'mongoose';
import {
deleteObject,
listObjects,
getObject,
editObject,
newObject,
listObjectsByProperties,
} from '../../database/database.js';
dotenv.config();
const logger = log4js.getLogger('Couriers');
logger.level = process.env.LOG_LEVEL;
export const listCouriersRouteHandler = async (
req,
res,
page = 1,
limit = 25,
property = '',
filter = {},
search = '',
sort = '',
order = 'ascend'
) => {
const result = await listObjects({
model: courierModel,
page,
limit,
property,
filter,
search,
sort,
order,
});
if (result?.error) {
logger.error('Error listing couriers.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of couriers (Page ${page}, Limit ${limit}). Count: ${result.length}.`);
res.send(result);
};
export const listCouriersByPropertiesRouteHandler = async (
req,
res,
properties = '',
filter = {}
) => {
const result = await listObjectsByProperties({
model: courierModel,
properties,
filter,
});
if (result?.error) {
logger.error('Error listing couriers.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of couriers. Count: ${result.length}`);
res.send(result);
};
export const getCourierRouteHandler = async (req, res) => {
const id = req.params.id;
const result = await getObject({
model: courierModel,
id,
});
if (result?.error) {
logger.warn(`Courier not found with supplied id.`);
return res.status(result.code).send(result);
}
logger.debug(`Retreived courier with ID: ${id}`);
res.send(result);
};
export const editCourierRouteHandler = async (req, res) => {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Courier with ID: ${id}`);
const updateData = {
updatedAt: new Date(),
contact: req.body?.contact,
country: req.body?.country,
name: req.body?.name,
website: req.body?.website,
phone: req.body?.phone,
email: req.body?.email,
};
// Create audit log before updating
const result = await editObject({
model: courierModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error editing courier:', result.error);
res.status(result).send(result);
return;
}
logger.debug(`Edited courier with ID: ${id}`);
res.send(result);
};
export const newCourierRouteHandler = async (req, res) => {
const newData = {
updatedAt: new Date(),
contact: req.body?.contact,
country: req.body?.country,
name: req.body?.name,
website: req.body?.website,
phone: req.body?.phone,
email: req.body?.email,
};
const result = await newObject({
model: courierModel,
newData,
user: req.user,
});
if (result.error) {
logger.error('No courier created:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`New courier with ID: ${result._id}`);
res.send(result);
};
export const deleteCourierRouteHandler = async (req, res) => {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Courier with ID: ${id}`);
const result = await deleteObject({
model: courierModel,
id,
user: req.user,
});
if (result.error) {
logger.error('No courier deleted:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`Deleted courier with ID: ${result._id}`);
res.send(result);
};

View File

@ -0,0 +1,167 @@
import dotenv from 'dotenv';
import { courierServiceModel } from '../../schemas/management/courierservice.schema.js';
import log4js from 'log4js';
import mongoose from 'mongoose';
import {
deleteObject,
listObjects,
getObject,
editObject,
newObject,
listObjectsByProperties,
} from '../../database/database.js';
dotenv.config();
const logger = log4js.getLogger('CourierServices');
logger.level = process.env.LOG_LEVEL;
export const listCourierServicesRouteHandler = async (
req,
res,
page = 1,
limit = 25,
property = '',
filter = {},
search = '',
sort = '',
order = 'ascend'
) => {
const result = await listObjects({
model: courierServiceModel,
page,
limit,
property,
filter,
search,
sort,
order,
populate: ['courier'],
});
if (result?.error) {
logger.error('Error listing courier services.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of courier services (Page ${page}, Limit ${limit}). Count: ${result.length}.`);
res.send(result);
};
export const listCourierServicesByPropertiesRouteHandler = async (
req,
res,
properties = '',
filter = {}
) => {
const result = await listObjectsByProperties({
model: courierServiceModel,
properties,
filter,
populate: ['courier'],
});
if (result?.error) {
logger.error('Error listing courier services.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of courier services. Count: ${result.length}`);
res.send(result);
};
export const getCourierServiceRouteHandler = async (req, res) => {
const id = req.params.id;
const result = await getObject({
model: courierServiceModel,
id,
populate: ['courier'],
});
if (result?.error) {
logger.warn(`Courier service not found with supplied id.`);
return res.status(result.code).send(result);
}
logger.debug(`Retreived courier service with ID: ${id}`);
res.send(result);
};
export const editCourierServiceRouteHandler = async (req, res) => {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Courier service with ID: ${id}`);
const updateData = {
updatedAt: new Date(),
courier: req.body?.courier,
website: req.body?.website,
deliveryTime: req.body?.deliveryTime,
active: req.body?.active,
tracked: req.body?.tracked,
name: req.body?.name,
};
// Create audit log before updating
const result = await editObject({
model: courierServiceModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error editing courier service:', result.error);
res.status(result).send(result);
return;
}
logger.debug(`Edited courier service with ID: ${id}`);
res.send(result);
};
export const newCourierServiceRouteHandler = async (req, res) => {
const newData = {
updatedAt: new Date(),
courier: req.body?.courier,
website: req.body?.website,
deliveryTime: req.body?.deliveryTime,
active: req.body?.active,
tracked: req.body?.tracked,
name: req.body?.name,
};
const result = await newObject({
model: courierServiceModel,
newData,
user: req.user,
});
if (result.error) {
logger.error('No courier service created:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`New courier service with ID: ${result._id}`);
res.send(result);
};
export const deleteCourierServiceRouteHandler = async (req, res) => {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Courier service with ID: ${id}`);
const result = await deleteObject({
model: courierServiceModel,
id,
user: req.user,
});
if (result.error) {
logger.error('No courier service deleted:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`Deleted courier service with ID: ${result._id}`);
res.send(result);
};

View File

@ -90,11 +90,11 @@ export const editPartRouteHandler = async (req, res) => {
const updateData = {
updatedAt: new Date(),
name: req.body?.name,
globalPricing: req.body.globalPricing,
file: req.body?.file,
vendor: req.body?.vendor,
margin: req.body?.margin,
amount: req.body?.amount,
price: req.body?.price,
cost: req.body?.cost,
priceMode: req.body?.priceMode,
};
// Create audit log before updating
@ -120,11 +120,11 @@ export const newPartRouteHandler = async (req, res) => {
const newData = {
updatedAt: new Date(),
name: req.body?.name,
globalPricing: req.body.globalPricing,
file: req.body?.file,
vendor: req.body?.vendor,
margin: req.body?.margin,
amount: req.body?.amount,
price: req.body?.price,
cost: req.body?.cost,
priceMode: req.body?.priceMode,
};

View File

@ -7,6 +7,7 @@ import { partModel } from '../../schemas/management/part.schema.js';
import { productModel } from '../../schemas/management/product.schema.js';
import { vendorModel } from '../../schemas/management/vendor.schema.js';
import { filamentStockModel } from '../../schemas/inventory/filamentstock.schema.js';
import { purchaseOrderModel } from '../../schemas/inventory/purchaseorder.schema.js';
import { stockEventModel } from '../../schemas/inventory/stockevent.schema.js';
import { stockAuditModel } from '../../schemas/inventory/stockaudit.schema.js';
import { partStockModel } from '../../schemas/inventory/partstock.schema.js';
@ -20,6 +21,8 @@ import { hostModel } from '../../schemas/management/host.schema.js';
import { documentPrinterModel } from '../../schemas/management/documentprinter.schema.js';
import { documentJobModel } from '../../schemas/management/documentjob.schema.js';
import { fileModel } from '../../schemas/management/file.schema.js';
import { courierServiceModel } from '../../schemas/management/courierservice.schema.js';
import { courierModel } from '../../schemas/management/courier.schema.js';
// Map prefixes to models and id fields
const PREFIX_MODEL_MAP = {
@ -46,6 +49,9 @@ const PREFIX_MODEL_MAP = {
DJB: { model: documentJobModel, idField: '_id', type: 'documentJob' },
HST: { model: hostModel, idField: '_id', type: 'host' },
FLE: { model: fileModel, idField: '_id', type: 'file' },
POR: { model: purchaseOrderModel, idField: '_id', type: 'purchaseOrder' },
COS: { model: courierServiceModel, idField: '_id', type: 'courierService' },
COR: { model: courierModel, idField: '_id', type: 'courier' },
};
/**