All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good
This commit introduces a new `deleteObjects` function in the database module, allowing for bulk deletion of objects based on specified filters and states. Additionally, new route handlers for deleting invoices, payment policies, payments, tax records, filament stocks, order items, part stocks, product stocks, purchase orders, shipments, stock audits, stock locations, stock transfers, app passwords, couriers, courier services, document printers, document sizes, document templates, email accounts, email templates, filaments, filament SKUs, files, hosts, materials, note types, parts, part SKUs, permission settings, product categories, products, and vendor entities are added. Each route supports deletion based on filters, enhancing the application's data management capabilities.
620 lines
16 KiB
JavaScript
620 lines
16 KiB
JavaScript
import config from '../../config.js';
|
|
import { stockTransferModel } from '../../database/schemas/inventory/stocktransfer.schema.js';
|
|
import { stockLocationModel } from '../../database/schemas/inventory/stocklocation.schema.js';
|
|
import { filamentStockModel } from '../../database/schemas/inventory/filamentstock.schema.js';
|
|
import { partStockModel } from '../../database/schemas/inventory/partstock.schema.js';
|
|
import { productStockModel } from '../../database/schemas/inventory/productstock.schema.js';
|
|
import { stockEventModel } from '../../database/schemas/inventory/stockevent.schema.js';
|
|
import log4js from 'log4js';
|
|
import mongoose from 'mongoose';
|
|
import {
|
|
deleteObject,
|
|
deleteObjects,
|
|
listObjects,
|
|
getObject,
|
|
editObject,
|
|
editObjects,
|
|
newObject,
|
|
listObjectsByProperties,
|
|
getModelStats,
|
|
getModelHistory,
|
|
checkStates,
|
|
searchObjects,
|
|
getPropertyValues,
|
|
getObjectNeighbors,
|
|
} from '../../database/database.js';
|
|
|
|
const logger = log4js.getLogger('Stock Transfers');
|
|
logger.level = config.server.logLevel;
|
|
|
|
const normalizeLineInput = (l) => ({
|
|
fromStockType: l.fromStockType,
|
|
fromStock: l.fromStock?._id ?? l.fromStock,
|
|
quantity: Number(l.quantity),
|
|
});
|
|
|
|
const toId = (value) => {
|
|
if (value == null) return null;
|
|
if (typeof value === 'object' && value._id) return String(value._id);
|
|
return String(value);
|
|
};
|
|
|
|
async function createStockEvent(newData, user) {
|
|
const result = await newObject({
|
|
model: stockEventModel,
|
|
newData,
|
|
user,
|
|
});
|
|
|
|
if (result?.error) {
|
|
throw new Error(result.error);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
async function createStockEventsForLine({ transferId, fromId, fromType, toId, toType, qty, unit, user }) {
|
|
const ts = new Date();
|
|
await Promise.all([
|
|
createStockEvent(
|
|
{
|
|
value: -Math.abs(qty),
|
|
unit,
|
|
parent: fromId,
|
|
parentType: fromType,
|
|
owner: transferId,
|
|
ownerType: 'stockTransfer',
|
|
timestamp: ts,
|
|
},
|
|
user
|
|
),
|
|
createStockEvent(
|
|
{
|
|
value: Math.abs(qty),
|
|
unit,
|
|
parent: toId,
|
|
parentType: toType,
|
|
owner: transferId,
|
|
ownerType: 'stockTransfer',
|
|
timestamp: ts,
|
|
},
|
|
user
|
|
),
|
|
]);
|
|
}
|
|
|
|
async function createStock(model, newData, user) {
|
|
const result = await newObject({
|
|
model,
|
|
newData,
|
|
user,
|
|
});
|
|
|
|
if (result?.error) {
|
|
throw new Error(result.error);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
async function executePostedLine(transfer, line, user) {
|
|
const fromLocId = transfer.fromLocation;
|
|
const toLocId = transfer.toLocation;
|
|
|
|
const [fromLoc, toLoc] = await Promise.all([
|
|
stockLocationModel.findById(fromLocId).lean(),
|
|
stockLocationModel.findById(toLocId).lean(),
|
|
]);
|
|
|
|
if (!fromLoc) {
|
|
throw new Error(`Unknown from location: ${fromLocId}`);
|
|
}
|
|
if (!toLoc) {
|
|
throw new Error(`Unknown to location: ${toLocId}`);
|
|
}
|
|
|
|
if (!(line.quantity > 0)) {
|
|
throw new Error('Line quantity must be positive');
|
|
}
|
|
|
|
const assertStockAtFromLocation = (stock) => {
|
|
if (toId(stock?.stockLocation) !== toId(fromLocId)) {
|
|
throw new Error('From stock must be at the transfer from location');
|
|
}
|
|
};
|
|
|
|
if (line.fromStockType === 'filamentStock') {
|
|
const src = await filamentStockModel.findById(line.fromStock);
|
|
if (!src) throw new Error('From filament stock not found');
|
|
assertStockAtFromLocation(src);
|
|
const netAvail = src.currentWeight?.net ?? 0;
|
|
if (line.quantity > netAvail) {
|
|
throw new Error('Filament transfer quantity exceeds available net weight');
|
|
}
|
|
const tareBefore = Math.max(0, (src.currentWeight?.gross ?? 0) - (src.currentWeight?.net ?? 0));
|
|
|
|
const destWeight = {
|
|
net: line.quantity,
|
|
gross: line.quantity + tareBefore,
|
|
};
|
|
const dest = await createStock(
|
|
filamentStockModel,
|
|
{
|
|
state: src.state,
|
|
startingWeight: destWeight,
|
|
currentWeight: destWeight,
|
|
filament: src.filament,
|
|
filamentSku: src.filamentSku,
|
|
stockLocation: toLocId,
|
|
},
|
|
user
|
|
);
|
|
|
|
await createStockEventsForLine({
|
|
transferId: transfer._id,
|
|
fromId: src._id,
|
|
fromType: 'filamentStock',
|
|
toId: dest._id,
|
|
toType: 'filamentStock',
|
|
qty: line.quantity,
|
|
unit: 'g',
|
|
user,
|
|
});
|
|
|
|
return { toStockType: 'filamentStock', toStock: dest._id };
|
|
}
|
|
|
|
if (line.fromStockType === 'partStock') {
|
|
const src = await partStockModel.findById(line.fromStock);
|
|
if (!src) throw new Error('From part stock not found');
|
|
assertStockAtFromLocation(src);
|
|
const currentQuantity = src.currentQuantity;
|
|
if (line.quantity > currentQuantity) {
|
|
throw new Error('Part transfer quantity exceeds current quantity');
|
|
}
|
|
|
|
const dest = await createStock(
|
|
partStockModel,
|
|
{
|
|
part: src.part,
|
|
partSku: src.partSku,
|
|
currentQuantity: line.quantity,
|
|
state: { type: 'new' },
|
|
postedAt: new Date(),
|
|
stockLocation: toLocId,
|
|
},
|
|
user
|
|
);
|
|
|
|
await createStockEventsForLine({
|
|
transferId: transfer._id,
|
|
fromId: src._id,
|
|
fromType: 'partStock',
|
|
toId: dest._id,
|
|
toType: 'partStock',
|
|
qty: line.quantity,
|
|
unit: 'each',
|
|
user,
|
|
});
|
|
|
|
return { toStockType: 'partStock', toStock: dest._id };
|
|
}
|
|
|
|
if (line.fromStockType === 'productStock') {
|
|
const src = await productStockModel.findById(line.fromStock);
|
|
if (!src) throw new Error('From product stock not found');
|
|
assertStockAtFromLocation(src);
|
|
if (line.quantity > src.currentQuantity) {
|
|
throw new Error('Product transfer quantity exceeds current quantity');
|
|
}
|
|
|
|
const dest = await createStock(
|
|
productStockModel,
|
|
{
|
|
product: src.product,
|
|
productSku: src.productSku,
|
|
currentQuantity: line.quantity,
|
|
state: { type: 'new' },
|
|
postedAt: new Date(),
|
|
partStockList: [],
|
|
stockLocation: toLocId,
|
|
},
|
|
user
|
|
);
|
|
|
|
await createStockEventsForLine({
|
|
transferId: transfer._id,
|
|
fromId: src._id,
|
|
fromType: 'productStock',
|
|
toId: dest._id,
|
|
toType: 'productStock',
|
|
qty: line.quantity,
|
|
unit: 'each',
|
|
user,
|
|
});
|
|
|
|
return { toStockType: 'productStock', toStock: dest._id };
|
|
}
|
|
|
|
throw new Error(`Unsupported from stock type: ${line.fromStockType}`);
|
|
}
|
|
|
|
const stockTransferPopulate = [
|
|
{ path: 'fromLocation' },
|
|
{ path: 'toLocation' },
|
|
{ path: 'lines.fromStock' },
|
|
{ path: 'lines.toStock' },
|
|
];
|
|
|
|
export const listStockTransfersRouteHandler = async (
|
|
req,
|
|
res,
|
|
page = 1,
|
|
limit = 25,
|
|
property = '',
|
|
filter = {},
|
|
search = '',
|
|
sort = '',
|
|
order = 'ascend'
|
|
) => {
|
|
const result = await listObjects({
|
|
model: stockTransferModel,
|
|
page,
|
|
limit,
|
|
property,
|
|
filter,
|
|
search,
|
|
sort,
|
|
order,
|
|
populate: stockTransferPopulate,
|
|
});
|
|
|
|
if (result?.error) {
|
|
logger.error('Error listing stock transfers.');
|
|
res.status(result.code).send(result);
|
|
return;
|
|
}
|
|
|
|
logger.debug(`List of stock transfers (Page ${page}, Limit ${limit}). Count: ${result.length}`);
|
|
res.send(result);
|
|
};
|
|
|
|
export const listStockTransfersByPropertiesRouteHandler = async (
|
|
req,
|
|
res,
|
|
properties = '',
|
|
filter = {},
|
|
masterFilter = {}
|
|
) => {
|
|
const result = await listObjectsByProperties({
|
|
model: stockTransferModel,
|
|
properties,
|
|
filter,
|
|
populate: stockTransferPopulate,
|
|
masterFilter,
|
|
});
|
|
|
|
if (result?.error) {
|
|
logger.error('Error listing stock transfers.');
|
|
res.status(result.code).send(result);
|
|
return;
|
|
}
|
|
|
|
logger.debug(`List of stock transfers. Count: ${result.length}`);
|
|
res.send(result);
|
|
};
|
|
|
|
export const getStockTransferPropertyValuesRouteHandler = async (
|
|
req,
|
|
res,
|
|
property,
|
|
filter,
|
|
masterFilter
|
|
) => {
|
|
const result = await getPropertyValues({
|
|
model: stockTransferModel,
|
|
property,
|
|
filter: { ...filter, ...masterFilter },
|
|
});
|
|
res.send(result);
|
|
};
|
|
|
|
export const searchStockTransfersRouteHandler = async (req, res, search) => {
|
|
const result = await searchObjects({
|
|
model: stockTransferModel,
|
|
search,
|
|
});
|
|
res.send(result);
|
|
};
|
|
|
|
export const getStockTransferRouteHandler = async (req, res) => {
|
|
const id = req.params.id;
|
|
const result = await getObject({
|
|
model: stockTransferModel,
|
|
id,
|
|
populate: stockTransferPopulate,
|
|
});
|
|
if (result?.error) {
|
|
logger.warn(`Stock transfer not found with supplied id.`);
|
|
return res.status(result.code).send(result);
|
|
}
|
|
logger.debug(`Retrieved stock transfer with ID: ${id}`);
|
|
res.send(result);
|
|
};
|
|
|
|
export const editStockTransferRouteHandler = async (req, res) => {
|
|
const id = new mongoose.Types.ObjectId(req.params.id);
|
|
|
|
const checkStatesResult = await checkStates({ model: stockTransferModel, id, states: ['draft'] });
|
|
|
|
if (checkStatesResult.error) {
|
|
logger.error('Error checking stock transfer state:', checkStatesResult.error);
|
|
res.status(checkStatesResult.code).send(checkStatesResult);
|
|
return;
|
|
}
|
|
|
|
if (checkStatesResult === false) {
|
|
logger.error('Stock transfer is not in draft state.');
|
|
res.status(400).send({ error: 'Stock transfer is not in draft state.', code: 400 });
|
|
return;
|
|
}
|
|
|
|
const updateData = {
|
|
lines: (req.body.lines || []).map((l) => normalizeLineInput(l)),
|
|
};
|
|
if (req.body.fromLocation !== undefined) {
|
|
updateData.fromLocation = req.body.fromLocation?._id ?? req.body.fromLocation;
|
|
}
|
|
if (req.body.toLocation !== undefined) {
|
|
updateData.toLocation = req.body.toLocation?._id ?? req.body.toLocation;
|
|
}
|
|
|
|
const result = await editObject({
|
|
model: stockTransferModel,
|
|
id,
|
|
updateData,
|
|
user: req.user,
|
|
populate: stockTransferPopulate,
|
|
});
|
|
|
|
if (result.error) {
|
|
logger.error('Error editing stock transfer:', result.error);
|
|
res.status(result.code).send(result);
|
|
return;
|
|
}
|
|
|
|
logger.debug(`Edited stock transfer with ID: ${id}`);
|
|
res.send(result);
|
|
};
|
|
|
|
export const editMultipleStockTransfersRouteHandler = async (req, res) => {
|
|
const updates = req.body.map((update) => ({
|
|
_id: update._id,
|
|
}));
|
|
|
|
if (!Array.isArray(updates)) {
|
|
return res.status(400).send({ error: 'Body must be an array of updates.', code: 400 });
|
|
}
|
|
|
|
const result = await editObjects({
|
|
model: stockTransferModel,
|
|
updates,
|
|
user: req.user,
|
|
});
|
|
|
|
if (result.error) {
|
|
logger.error('Error editing stock transfers:', result.error);
|
|
res.status(result.code || 500).send(result);
|
|
return;
|
|
}
|
|
|
|
logger.debug(`Edited ${updates.length} stock transfers`);
|
|
res.send(result);
|
|
};
|
|
|
|
export const newStockTransferRouteHandler = async (req, res) => {
|
|
const newData = {
|
|
state: req.body.state ?? { type: 'draft' },
|
|
fromLocation: req.body.fromLocation?._id ?? req.body.fromLocation,
|
|
toLocation: req.body.toLocation?._id ?? req.body.toLocation,
|
|
lines: (req.body.lines || []).map((l) => normalizeLineInput(l)),
|
|
};
|
|
const result = await newObject({
|
|
model: stockTransferModel,
|
|
newData,
|
|
user: req.user,
|
|
});
|
|
if (result.error) {
|
|
logger.error('No stock transfer created:', result.error);
|
|
return res.status(result.code).send(result);
|
|
}
|
|
|
|
logger.debug(`New stock transfer with ID: ${result._id}`);
|
|
res.send(result);
|
|
};
|
|
|
|
export const deleteStockTransferByFilterRouteHandler = async (
|
|
req,
|
|
res,
|
|
filter = {},
|
|
masterFilter = {}
|
|
) => {
|
|
const result = await deleteObjects({
|
|
model: stockTransferModel,
|
|
filter: { ...masterFilter, ...filter },
|
|
user: req.user,
|
|
states: ['draft'],
|
|
});
|
|
|
|
if (result.error) {
|
|
logger.error('Failed to delete filtered StockTransfer:', result.error);
|
|
return res.status(result.code || 500).send(result);
|
|
}
|
|
|
|
res.send(result);
|
|
};
|
|
|
|
export const deleteStockTransferRouteHandler = async (req, res) => {
|
|
const id = new mongoose.Types.ObjectId(req.params.id);
|
|
|
|
const checkStatesResult = await checkStates({ model: stockTransferModel, id, states: ['draft'] });
|
|
|
|
if (checkStatesResult.error) {
|
|
logger.error('Error checking stock transfer state:', checkStatesResult.error);
|
|
res.status(checkStatesResult.code).send(checkStatesResult);
|
|
return;
|
|
}
|
|
|
|
if (checkStatesResult === false) {
|
|
logger.error('Stock transfer is not in draft state.');
|
|
res.status(400).send({ error: 'Stock transfer is not in draft state.', code: 400 });
|
|
return;
|
|
}
|
|
|
|
const result = await deleteObject({
|
|
model: stockTransferModel,
|
|
id,
|
|
user: req.user,
|
|
});
|
|
if (result.error) {
|
|
logger.error('No stock transfer deleted:', result.error);
|
|
return res.status(result.code).send(result);
|
|
}
|
|
|
|
logger.debug(`Deleted stock transfer with ID: ${result._id}`);
|
|
res.send(result);
|
|
};
|
|
|
|
export const postStockTransferRouteHandler = async (req, res) => {
|
|
const id = new mongoose.Types.ObjectId(req.params.id);
|
|
|
|
const checkStatesResult = await checkStates({ model: stockTransferModel, id, states: ['draft'] });
|
|
|
|
if (checkStatesResult.error) {
|
|
logger.error('Error checking stock transfer state:', checkStatesResult.error);
|
|
res.status(checkStatesResult.code).send(checkStatesResult);
|
|
return;
|
|
}
|
|
|
|
if (checkStatesResult === false) {
|
|
logger.error('Stock transfer is not in draft state.');
|
|
res.status(400).send({ error: 'Stock transfer is not in draft state.', code: 400 });
|
|
return;
|
|
}
|
|
|
|
const doc = await stockTransferModel.findById(id);
|
|
if (!doc) {
|
|
return res.status(404).send({ error: 'Stock transfer not found.', code: 404 });
|
|
}
|
|
|
|
if (!doc.lines?.length) {
|
|
return res.status(400).send({ error: 'Stock transfer has no lines.', code: 400 });
|
|
}
|
|
|
|
if (!doc.fromLocation || !doc.toLocation) {
|
|
return res.status(400).send({
|
|
error: 'Stock transfer must have from and to locations.',
|
|
code: 400,
|
|
});
|
|
}
|
|
|
|
const updatedLines = [];
|
|
|
|
try {
|
|
for (const line of doc.lines) {
|
|
const plain = line.toObject();
|
|
const { toStockType, toStock } = await executePostedLine(doc, plain, req.user);
|
|
updatedLines.push({
|
|
...plain,
|
|
toStockType,
|
|
toStock,
|
|
});
|
|
}
|
|
|
|
const postedResult = await editObject({
|
|
model: stockTransferModel,
|
|
id,
|
|
updateData: {
|
|
state: { type: 'posted' },
|
|
postedAt: new Date(),
|
|
lines: updatedLines,
|
|
},
|
|
user: req.user,
|
|
});
|
|
|
|
if (postedResult?.error) {
|
|
throw new Error(postedResult.error);
|
|
}
|
|
|
|
const posted = await getObject({
|
|
model: stockTransferModel,
|
|
id,
|
|
populate: stockTransferPopulate,
|
|
});
|
|
|
|
if (posted?.error) {
|
|
throw new Error(posted.error);
|
|
}
|
|
|
|
logger.debug(`Posted stock transfer with ID: ${id}`);
|
|
res.send(posted);
|
|
} catch (err) {
|
|
logger.error('Error posting stock transfer:', err);
|
|
res.status(400).send({ error: err.message || 'Failed to post stock transfer', code: 400 });
|
|
}
|
|
};
|
|
|
|
export const getStockTransferStatsRouteHandler = async (req, res) => {
|
|
const result = await getModelStats({ model: stockTransferModel });
|
|
if (result?.error) {
|
|
logger.error('Error fetching stock transfer stats:', result.error);
|
|
return res.status(result.code).send(result);
|
|
}
|
|
logger.trace('Stock transfer stats:', result);
|
|
res.send(result);
|
|
};
|
|
|
|
export const getStockTransferHistoryRouteHandler = async (req, res) => {
|
|
const from = req.query.from;
|
|
const to = req.query.to;
|
|
const result = await getModelHistory({ model: stockTransferModel, from, to });
|
|
if (result?.error) {
|
|
logger.error('Error fetching stock transfer history:', result.error);
|
|
return res.status(result.code).send(result);
|
|
}
|
|
logger.trace('Stock transfer history:', result);
|
|
res.send(result);
|
|
};
|
|
export const getStockTransferNeighborsRouteHandler = async (
|
|
req,
|
|
res,
|
|
property = '',
|
|
filter = {},
|
|
search = '',
|
|
sort = '',
|
|
order = 'ascend',
|
|
id
|
|
) => {
|
|
if (!id) {
|
|
return res.status(400).send({ error: 'Missing id parameter', code: 400 });
|
|
}
|
|
|
|
const result = await getObjectNeighbors({
|
|
model: stockEventModel,
|
|
id,
|
|
filter,
|
|
search,
|
|
sort,
|
|
order,
|
|
});
|
|
|
|
if (result?.error) {
|
|
logger.error('Error fetching stockTransfer neighbors.');
|
|
return res.status(result.code).send(result);
|
|
}
|
|
|
|
logger.debug(`Retrieved stockTransfer neighbors for ID: ${id}`);
|
|
res.send(result);
|
|
};
|
|
|