Added stock location and stock transfer features, including new routes, database schemas, and service updates. Enhanced existing stock-related functionalities to support filtering and population of stock location data.
This commit is contained in:
parent
1b858d8814
commit
d3c662a9ec
@ -20,6 +20,11 @@ const filamentStockSchema = new Schema(
|
|||||||
gross: { type: Number, required: true },
|
gross: { type: Number, required: true },
|
||||||
},
|
},
|
||||||
filamentSku: { type: mongoose.Schema.Types.ObjectId, ref: 'filamentSku', required: true },
|
filamentSku: { type: mongoose.Schema.Types.ObjectId, ref: 'filamentSku', required: true },
|
||||||
|
stockLocation: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'stockLocation',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
|
|||||||
@ -12,6 +12,11 @@ const partStockSchema = new Schema(
|
|||||||
progress: { type: Number, required: false },
|
progress: { type: Number, required: false },
|
||||||
},
|
},
|
||||||
partSku: { type: mongoose.Schema.Types.ObjectId, ref: 'partSku', required: true },
|
partSku: { type: mongoose.Schema.Types.ObjectId, ref: 'partSku', required: true },
|
||||||
|
stockLocation: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'stockLocation',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
currentQuantity: { type: Number, required: true },
|
currentQuantity: { type: Number, required: true },
|
||||||
sourceType: { type: String, required: true },
|
sourceType: { type: String, required: true },
|
||||||
source: { type: Schema.Types.ObjectId, refPath: 'sourceType', required: true },
|
source: { type: Schema.Types.ObjectId, refPath: 'sourceType', required: true },
|
||||||
|
|||||||
@ -19,6 +19,11 @@ const productStockSchema = new Schema(
|
|||||||
},
|
},
|
||||||
postedAt: { type: Date, required: false },
|
postedAt: { type: Date, required: false },
|
||||||
productSku: { type: mongoose.Schema.Types.ObjectId, ref: 'productSku', required: true },
|
productSku: { type: mongoose.Schema.Types.ObjectId, ref: 'productSku', required: true },
|
||||||
|
stockLocation: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'stockLocation',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
currentQuantity: { type: Number, required: true },
|
currentQuantity: { type: Number, required: true },
|
||||||
partStocks: [partStockUsageSchema],
|
partStocks: [partStockUsageSchema],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -25,7 +25,7 @@ const stockEventSchema = new Schema(
|
|||||||
ownerType: {
|
ownerType: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
enum: ['user', 'subJob', 'stockAudit'],
|
enum: ['user', 'subJob', 'stockAudit', 'stockTransfer'],
|
||||||
},
|
},
|
||||||
timestamp: { type: Date, default: Date.now },
|
timestamp: { type: Date, default: Date.now },
|
||||||
},
|
},
|
||||||
|
|||||||
29
src/database/schemas/inventory/stocklocation.schema.js
Normal file
29
src/database/schemas/inventory/stocklocation.schema.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import { generateId } from '../../utils.js';
|
||||||
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
|
const stockLocationSchema = new Schema(
|
||||||
|
{
|
||||||
|
_reference: { type: String, default: () => generateId()() },
|
||||||
|
name: { type: String, required: true },
|
||||||
|
notes: { type: String, required: false },
|
||||||
|
},
|
||||||
|
{ timestamps: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
stockLocationSchema.statics.stats = async function () {
|
||||||
|
const total = await this.countDocuments({});
|
||||||
|
return { total: { count: total } };
|
||||||
|
};
|
||||||
|
|
||||||
|
stockLocationSchema.statics.history = async function () {
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
stockLocationSchema.virtual('id').get(function () {
|
||||||
|
return this._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
stockLocationSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
|
export const stockLocationModel = mongoose.model('stockLocation', stockLocationSchema);
|
||||||
71
src/database/schemas/inventory/stocktransfer.schema.js
Normal file
71
src/database/schemas/inventory/stocktransfer.schema.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import { generateId } from '../../utils.js';
|
||||||
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
|
const stockTransferLineSchema = new Schema(
|
||||||
|
{
|
||||||
|
fromStockType: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
enum: ['filamentStock', 'partStock', 'productStock'],
|
||||||
|
},
|
||||||
|
fromStock: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
refPath: 'fromStockType',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
quantity: { type: Number, required: true },
|
||||||
|
toStockLocation: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'stockLocation',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
toStockType: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
enum: ['filamentStock', 'partStock', 'productStock'],
|
||||||
|
},
|
||||||
|
toStock: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
refPath: 'toStockType',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ _id: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const stockTransferSchema = new Schema(
|
||||||
|
{
|
||||||
|
_reference: { type: String, default: () => generateId()() },
|
||||||
|
state: {
|
||||||
|
type: { type: String, required: true, default: 'draft' },
|
||||||
|
progress: { type: Number, required: false },
|
||||||
|
},
|
||||||
|
postedAt: { type: Date, required: false },
|
||||||
|
lines: { type: [stockTransferLineSchema], default: [] },
|
||||||
|
},
|
||||||
|
{ timestamps: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
stockTransferSchema.statics.stats = async function () {
|
||||||
|
const [draft, posted] = await Promise.all([
|
||||||
|
this.countDocuments({ 'state.type': 'draft' }),
|
||||||
|
this.countDocuments({ 'state.type': 'posted' }),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
draft: { count: draft },
|
||||||
|
posted: { count: posted },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
stockTransferSchema.statics.history = async function () {
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
stockTransferSchema.virtual('id').get(function () {
|
||||||
|
return this._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
stockTransferSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
|
export const stockTransferModel = mongoose.model('stockTransfer', stockTransferSchema);
|
||||||
@ -31,19 +31,10 @@ partSchema.virtual('id').get(function () {
|
|||||||
partSchema.set('toJSON', { virtuals: true });
|
partSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
partSchema.statics.recalculate = async function (part, user) {
|
partSchema.statics.recalculate = async function (part, user) {
|
||||||
const orderItemModel = mongoose.model('orderItem');
|
const partSkuModel = mongoose.model('partSku');
|
||||||
const itemId = part._id;
|
const skus = await partSkuModel.find({ part: part._id }).select('_id').lean();
|
||||||
const draftOrderItems = await orderItemModel
|
for (const sku of skus) {
|
||||||
.find({
|
await partSkuModel.recalculate(sku, user);
|
||||||
'state.type': 'draft',
|
|
||||||
itemType: 'part',
|
|
||||||
item: itemId,
|
|
||||||
})
|
|
||||||
.populate('order')
|
|
||||||
.lean();
|
|
||||||
|
|
||||||
for (const orderItem of draftOrderItems) {
|
|
||||||
await orderItemModel.recalculate(orderItem, user);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,8 @@ import { stockEventModel } from './inventory/stockevent.schema.js';
|
|||||||
import { stockAuditModel } from './inventory/stockaudit.schema.js';
|
import { stockAuditModel } from './inventory/stockaudit.schema.js';
|
||||||
import { partStockModel } from './inventory/partstock.schema.js';
|
import { partStockModel } from './inventory/partstock.schema.js';
|
||||||
import { productStockModel } from './inventory/productstock.schema.js';
|
import { productStockModel } from './inventory/productstock.schema.js';
|
||||||
|
import { stockLocationModel } from './inventory/stocklocation.schema.js';
|
||||||
|
import { stockTransferModel } from './inventory/stocktransfer.schema.js';
|
||||||
import { auditLogModel } from './management/auditlog.schema.js';
|
import { auditLogModel } from './management/auditlog.schema.js';
|
||||||
import { userModel } from './management/user.schema.js';
|
import { userModel } from './management/user.schema.js';
|
||||||
import { appPasswordModel } from './management/apppassword.schema.js';
|
import { appPasswordModel } from './management/apppassword.schema.js';
|
||||||
@ -157,6 +159,20 @@ export const models = {
|
|||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
label: 'Product Stock',
|
label: 'Product Stock',
|
||||||
},
|
},
|
||||||
|
SLN: {
|
||||||
|
model: stockLocationModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'stockLocation',
|
||||||
|
referenceField: '_reference',
|
||||||
|
label: 'Stock Location',
|
||||||
|
},
|
||||||
|
STT: {
|
||||||
|
model: stockTransferModel,
|
||||||
|
idField: '_id',
|
||||||
|
type: 'stockTransfer',
|
||||||
|
referenceField: '_reference',
|
||||||
|
label: 'Stock Transfer',
|
||||||
|
},
|
||||||
ADL: {
|
ADL: {
|
||||||
model: auditLogModel,
|
model: auditLogModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
|
|||||||
@ -29,6 +29,8 @@ import {
|
|||||||
orderItemRoutes,
|
orderItemRoutes,
|
||||||
shipmentRoutes,
|
shipmentRoutes,
|
||||||
stockAuditRoutes,
|
stockAuditRoutes,
|
||||||
|
stockLocationRoutes,
|
||||||
|
stockTransferRoutes,
|
||||||
stockEventRoutes,
|
stockEventRoutes,
|
||||||
auditLogRoutes,
|
auditLogRoutes,
|
||||||
noteTypeRoutes,
|
noteTypeRoutes,
|
||||||
@ -152,6 +154,8 @@ app.use('/orderitems', orderItemRoutes);
|
|||||||
app.use('/shipments', shipmentRoutes);
|
app.use('/shipments', shipmentRoutes);
|
||||||
app.use('/stockevents', stockEventRoutes);
|
app.use('/stockevents', stockEventRoutes);
|
||||||
app.use('/stockaudits', stockAuditRoutes);
|
app.use('/stockaudits', stockAuditRoutes);
|
||||||
|
app.use('/stocklocations', stockLocationRoutes);
|
||||||
|
app.use('/stocktransfers', stockTransferRoutes);
|
||||||
app.use('/auditlogs', auditLogRoutes);
|
app.use('/auditlogs', auditLogRoutes);
|
||||||
app.use('/notetypes', noteTypeRoutes);
|
app.use('/notetypes', noteTypeRoutes);
|
||||||
app.use('/documentsizes', documentSizesRoutes);
|
app.use('/documentsizes', documentSizesRoutes);
|
||||||
|
|||||||
@ -24,6 +24,8 @@ import orderItemRoutes from './inventory/orderitems.js';
|
|||||||
import shipmentRoutes from './inventory/shipments.js';
|
import shipmentRoutes from './inventory/shipments.js';
|
||||||
import stockEventRoutes from './inventory/stockevents.js';
|
import stockEventRoutes from './inventory/stockevents.js';
|
||||||
import stockAuditRoutes from './inventory/stockaudits.js';
|
import stockAuditRoutes from './inventory/stockaudits.js';
|
||||||
|
import stockLocationRoutes from './inventory/stocklocations.js';
|
||||||
|
import stockTransferRoutes from './inventory/stocktransfers.js';
|
||||||
import auditLogRoutes from './management/auditlogs.js';
|
import auditLogRoutes from './management/auditlogs.js';
|
||||||
import noteTypeRoutes from './management/notetypes.js';
|
import noteTypeRoutes from './management/notetypes.js';
|
||||||
import documentSizesRoutes from './management/documentsizes.js';
|
import documentSizesRoutes from './management/documentsizes.js';
|
||||||
@ -75,6 +77,8 @@ export {
|
|||||||
shipmentRoutes,
|
shipmentRoutes,
|
||||||
stockEventRoutes,
|
stockEventRoutes,
|
||||||
stockAuditRoutes,
|
stockAuditRoutes,
|
||||||
|
stockLocationRoutes,
|
||||||
|
stockTransferRoutes,
|
||||||
auditLogRoutes,
|
auditLogRoutes,
|
||||||
noteTypeRoutes,
|
noteTypeRoutes,
|
||||||
noteRoutes,
|
noteRoutes,
|
||||||
|
|||||||
@ -18,7 +18,15 @@ import {
|
|||||||
// list of filament stocks
|
// list of filament stocks
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
router.get('/', isAuthenticated, (req, res) => {
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
const { page, limit, property, search, sort, order } = req.query;
|
||||||
const allowedFilters = ['filamentSku', 'state', 'startingWeight', 'currentWeight', 'filamentSku._id'];
|
const allowedFilters = [
|
||||||
|
'filamentSku',
|
||||||
|
'state',
|
||||||
|
'startingWeight',
|
||||||
|
'currentWeight',
|
||||||
|
'filamentSku._id',
|
||||||
|
'stockLocation',
|
||||||
|
'stockLocation._id',
|
||||||
|
];
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
const filter = getFilter(req.query, allowedFilters);
|
||||||
listFilamentStocksRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
listFilamentStocksRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -18,7 +18,15 @@ import {
|
|||||||
// list of part stocks
|
// list of part stocks
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
router.get('/', isAuthenticated, (req, res) => {
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
const { page, limit, property, search, sort, order } = req.query;
|
||||||
const allowedFilters = ['partSku', 'state', 'startingQuantity', 'currentQuantity', 'partSku._id'];
|
const allowedFilters = [
|
||||||
|
'partSku',
|
||||||
|
'state',
|
||||||
|
'startingQuantity',
|
||||||
|
'currentQuantity',
|
||||||
|
'partSku._id',
|
||||||
|
'stockLocation',
|
||||||
|
'stockLocation._id',
|
||||||
|
];
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
const filter = getFilter(req.query, allowedFilters);
|
||||||
listPartStocksRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
listPartStocksRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -18,7 +18,14 @@ import {
|
|||||||
|
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
router.get('/', isAuthenticated, (req, res) => {
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
const { page, limit, property, search, sort, order } = req.query;
|
||||||
const allowedFilters = ['productSku', 'state', 'currentQuantity', 'productSku._id'];
|
const allowedFilters = [
|
||||||
|
'productSku',
|
||||||
|
'state',
|
||||||
|
'currentQuantity',
|
||||||
|
'productSku._id',
|
||||||
|
'stockLocation',
|
||||||
|
'stockLocation._id',
|
||||||
|
];
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
const filter = getFilter(req.query, allowedFilters);
|
||||||
listProductStocksRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
listProductStocksRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
||||||
});
|
});
|
||||||
|
|||||||
64
src/routes/inventory/stocklocations.js
Normal file
64
src/routes/inventory/stocklocations.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { isAuthenticated } from '../../keycloak.js';
|
||||||
|
import { getFilter, convertPropertiesString } from '../../utils.js';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
import {
|
||||||
|
listStockLocationsRouteHandler,
|
||||||
|
getStockLocationRouteHandler,
|
||||||
|
editStockLocationRouteHandler,
|
||||||
|
editMultipleStockLocationsRouteHandler,
|
||||||
|
newStockLocationRouteHandler,
|
||||||
|
deleteStockLocationRouteHandler,
|
||||||
|
listStockLocationsByPropertiesRouteHandler,
|
||||||
|
getStockLocationStatsRouteHandler,
|
||||||
|
getStockLocationHistoryRouteHandler,
|
||||||
|
} from '../../services/inventory/stocklocations.js';
|
||||||
|
|
||||||
|
router.get('/', isAuthenticated, (req, res) => {
|
||||||
|
const { page, limit, property, search, sort, order } = req.query;
|
||||||
|
const allowedFilters = ['name', 'notes'];
|
||||||
|
const filter = getFilter(req.query, allowedFilters);
|
||||||
|
listStockLocationsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/properties', isAuthenticated, (req, res) => {
|
||||||
|
let properties = convertPropertiesString(req.query.properties);
|
||||||
|
const allowedFilters = ['name'];
|
||||||
|
const filter = getFilter(req.query, allowedFilters, false);
|
||||||
|
var masterFilter = {};
|
||||||
|
if (req.query.masterFilter) {
|
||||||
|
masterFilter = JSON.parse(req.query.masterFilter);
|
||||||
|
}
|
||||||
|
listStockLocationsByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/', isAuthenticated, (req, res) => {
|
||||||
|
newStockLocationRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/stats', isAuthenticated, (req, res) => {
|
||||||
|
getStockLocationStatsRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/history', isAuthenticated, (req, res) => {
|
||||||
|
getStockLocationHistoryRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:id', isAuthenticated, (req, res) => {
|
||||||
|
getStockLocationRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/', isAuthenticated, async (req, res) => {
|
||||||
|
editMultipleStockLocationsRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/:id', isAuthenticated, async (req, res) => {
|
||||||
|
editStockLocationRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/:id', isAuthenticated, async (req, res) => {
|
||||||
|
deleteStockLocationRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
69
src/routes/inventory/stocktransfers.js
Normal file
69
src/routes/inventory/stocktransfers.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { isAuthenticated } from '../../keycloak.js';
|
||||||
|
import { getFilter, convertPropertiesString } from '../../utils.js';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
import {
|
||||||
|
listStockTransfersRouteHandler,
|
||||||
|
getStockTransferRouteHandler,
|
||||||
|
editStockTransferRouteHandler,
|
||||||
|
editMultipleStockTransfersRouteHandler,
|
||||||
|
newStockTransferRouteHandler,
|
||||||
|
deleteStockTransferRouteHandler,
|
||||||
|
postStockTransferRouteHandler,
|
||||||
|
listStockTransfersByPropertiesRouteHandler,
|
||||||
|
getStockTransferStatsRouteHandler,
|
||||||
|
getStockTransferHistoryRouteHandler,
|
||||||
|
} from '../../services/inventory/stocktransfers.js';
|
||||||
|
|
||||||
|
router.get('/', isAuthenticated, (req, res) => {
|
||||||
|
const { page, limit, property, search, sort, order } = req.query;
|
||||||
|
const allowedFilters = ['state', 'state.type', 'postedAt'];
|
||||||
|
const filter = getFilter(req.query, allowedFilters);
|
||||||
|
listStockTransfersRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/properties', isAuthenticated, (req, res) => {
|
||||||
|
let properties = convertPropertiesString(req.query.properties);
|
||||||
|
const allowedFilters = ['state.type'];
|
||||||
|
const filter = getFilter(req.query, allowedFilters, false);
|
||||||
|
var masterFilter = {};
|
||||||
|
if (req.query.masterFilter) {
|
||||||
|
masterFilter = JSON.parse(req.query.masterFilter);
|
||||||
|
}
|
||||||
|
listStockTransfersByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/', isAuthenticated, (req, res) => {
|
||||||
|
newStockTransferRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/stats', isAuthenticated, (req, res) => {
|
||||||
|
getStockTransferStatsRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/history', isAuthenticated, (req, res) => {
|
||||||
|
getStockTransferHistoryRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:id', isAuthenticated, (req, res) => {
|
||||||
|
getStockTransferRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/', isAuthenticated, async (req, res) => {
|
||||||
|
editMultipleStockTransfersRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/:id', isAuthenticated, async (req, res) => {
|
||||||
|
editStockTransferRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/:id', isAuthenticated, async (req, res) => {
|
||||||
|
deleteStockTransferRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/:id/post', isAuthenticated, async (req, res) => {
|
||||||
|
postStockTransferRouteHandler(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@ -36,7 +36,7 @@ export const listFilamentStocksRouteHandler = async (
|
|||||||
search,
|
search,
|
||||||
sort,
|
sort,
|
||||||
order,
|
order,
|
||||||
populate: [{ path: 'filamentSku' }],
|
populate: [{ path: 'filamentSku' }, { path: 'stockLocation' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@ -60,7 +60,7 @@ export const listFilamentStocksByPropertiesRouteHandler = async (
|
|||||||
model: filamentStockModel,
|
model: filamentStockModel,
|
||||||
properties,
|
properties,
|
||||||
filter,
|
filter,
|
||||||
populate: ['filamentSku'],
|
populate: ['filamentSku', 'stockLocation'],
|
||||||
masterFilter,
|
masterFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ export const getFilamentStockRouteHandler = async (req, res) => {
|
|||||||
const result = await getObject({
|
const result = await getObject({
|
||||||
model: filamentStockModel,
|
model: filamentStockModel,
|
||||||
id,
|
id,
|
||||||
populate: [{ path: 'filamentSku' }],
|
populate: [{ path: 'filamentSku' }, { path: 'stockLocation' }],
|
||||||
});
|
});
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
logger.warn(`Filament Stock not found with supplied id.`);
|
logger.warn(`Filament Stock not found with supplied id.`);
|
||||||
@ -95,8 +95,9 @@ export const editFilamentStockRouteHandler = async (req, res) => {
|
|||||||
|
|
||||||
logger.trace(`Filament Stock with ID: ${id}`);
|
logger.trace(`Filament Stock with ID: ${id}`);
|
||||||
|
|
||||||
const updateData = {};
|
const updateData = {
|
||||||
// Create audit log before updating
|
stockLocation: req.body.stockLocation,
|
||||||
|
};
|
||||||
const result = await editObject({
|
const result = await editObject({
|
||||||
model: filamentStockModel,
|
model: filamentStockModel,
|
||||||
id,
|
id,
|
||||||
@ -148,6 +149,7 @@ export const newFilamentStockRouteHandler = async (req, res) => {
|
|||||||
currentWeight: req.body.currentWeight,
|
currentWeight: req.body.currentWeight,
|
||||||
filamentSku: req.body.filamentSku,
|
filamentSku: req.body.filamentSku,
|
||||||
state: req.body.state,
|
state: req.body.state,
|
||||||
|
stockLocation: req.body.stockLocation,
|
||||||
};
|
};
|
||||||
const result = await newObject({
|
const result = await newObject({
|
||||||
model: filamentStockModel,
|
model: filamentStockModel,
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export const listPartStocksRouteHandler = async (
|
|||||||
search,
|
search,
|
||||||
sort,
|
sort,
|
||||||
order,
|
order,
|
||||||
populate: [{ path: 'partSku' }],
|
populate: [{ path: 'partSku' }, { path: 'stockLocation' }, { path: 'source' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@ -60,7 +60,7 @@ export const listPartStocksByPropertiesRouteHandler = async (
|
|||||||
model: partStockModel,
|
model: partStockModel,
|
||||||
properties,
|
properties,
|
||||||
filter,
|
filter,
|
||||||
populate: ['partSku'],
|
populate: ['partSku', 'stockLocation', 'source'],
|
||||||
masterFilter,
|
masterFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ export const getPartStockRouteHandler = async (req, res) => {
|
|||||||
const result = await getObject({
|
const result = await getObject({
|
||||||
model: partStockModel,
|
model: partStockModel,
|
||||||
id,
|
id,
|
||||||
populate: [{ path: 'partSku' }],
|
populate: [{ path: 'partSku' }, { path: 'stockLocation' }, { path: 'source' }],
|
||||||
});
|
});
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
logger.warn(`Part Stock not found with supplied id.`);
|
logger.warn(`Part Stock not found with supplied id.`);
|
||||||
@ -96,7 +96,6 @@ export const editPartStockRouteHandler = async (req, res) => {
|
|||||||
logger.trace(`Part Stock with ID: ${id}`);
|
logger.trace(`Part Stock with ID: ${id}`);
|
||||||
|
|
||||||
const updateData = {};
|
const updateData = {};
|
||||||
// Create audit log before updating
|
|
||||||
const result = await editObject({
|
const result = await editObject({
|
||||||
model: partStockModel,
|
model: partStockModel,
|
||||||
id,
|
id,
|
||||||
@ -148,6 +147,9 @@ export const newPartStockRouteHandler = async (req, res) => {
|
|||||||
currentQuantity: req.body.currentQuantity,
|
currentQuantity: req.body.currentQuantity,
|
||||||
partSku: req.body.partSku,
|
partSku: req.body.partSku,
|
||||||
state: req.body.state,
|
state: req.body.state,
|
||||||
|
sourceType: req.body.sourceType,
|
||||||
|
source: req.body.source,
|
||||||
|
stockLocation: req.body.stockLocation,
|
||||||
};
|
};
|
||||||
const result = await newObject({
|
const result = await newObject({
|
||||||
model: partStockModel,
|
model: partStockModel,
|
||||||
|
|||||||
@ -38,7 +38,11 @@ export const listProductStocksRouteHandler = async (
|
|||||||
search,
|
search,
|
||||||
sort,
|
sort,
|
||||||
order,
|
order,
|
||||||
populate: [{ path: 'productSku' }, { path: 'partStocks.partStock' }],
|
populate: [
|
||||||
|
{ path: 'productSku' },
|
||||||
|
{ path: 'partStocks.partStock' },
|
||||||
|
{ path: 'stockLocation' },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@ -62,7 +66,7 @@ export const listProductStocksByPropertiesRouteHandler = async (
|
|||||||
model: productStockModel,
|
model: productStockModel,
|
||||||
properties,
|
properties,
|
||||||
filter,
|
filter,
|
||||||
populate: ['productSku', 'partStocks.partStock'],
|
populate: ['productSku', 'partStocks.partStock', 'stockLocation'],
|
||||||
masterFilter,
|
masterFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -81,7 +85,12 @@ export const getProductStockRouteHandler = async (req, res) => {
|
|||||||
const result = await getObject({
|
const result = await getObject({
|
||||||
model: productStockModel,
|
model: productStockModel,
|
||||||
id,
|
id,
|
||||||
populate: [{ path: 'partStocks.partSku' }, { path: 'partStocks.partStock' }, { path: 'productSku' }],
|
populate: [
|
||||||
|
{ path: 'partStocks.partSku' },
|
||||||
|
{ path: 'partStocks.partStock' },
|
||||||
|
{ path: 'productSku' },
|
||||||
|
{ path: 'stockLocation' },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
logger.warn(`Product Stock not found with supplied id.`);
|
logger.warn(`Product Stock not found with supplied id.`);
|
||||||
@ -111,6 +120,7 @@ export const editProductStockRouteHandler = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
|
stockLocation: req.body?.stockLocation,
|
||||||
partStocks: req.body?.partStocks?.map((partStock) => ({
|
partStocks: req.body?.partStocks?.map((partStock) => ({
|
||||||
quantity: partStock.quantity,
|
quantity: partStock.quantity,
|
||||||
partStock: partStock.partStock,
|
partStock: partStock.partStock,
|
||||||
@ -173,6 +183,7 @@ export const newProductStockRouteHandler = async (req, res) => {
|
|||||||
currentQuantity: req.body.currentQuantity,
|
currentQuantity: req.body.currentQuantity,
|
||||||
productSku: req.body.productSku,
|
productSku: req.body.productSku,
|
||||||
state: req.body.state ?? { type: 'draft' },
|
state: req.body.state ?? { type: 'draft' },
|
||||||
|
stockLocation: req.body.stockLocation,
|
||||||
partStocks: (productSku.parts || []).map((part) => ({
|
partStocks: (productSku.parts || []).map((part) => ({
|
||||||
partSku: part.partSku,
|
partSku: part.partSku,
|
||||||
quantity: part.quantity,
|
quantity: part.quantity,
|
||||||
|
|||||||
196
src/services/inventory/stocklocations.js
Normal file
196
src/services/inventory/stocklocations.js
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import config from '../../config.js';
|
||||||
|
import { stockLocationModel } from '../../database/schemas/inventory/stocklocation.schema.js';
|
||||||
|
import log4js from 'log4js';
|
||||||
|
import mongoose from 'mongoose';
|
||||||
|
import {
|
||||||
|
deleteObject,
|
||||||
|
listObjects,
|
||||||
|
getObject,
|
||||||
|
editObject,
|
||||||
|
editObjects,
|
||||||
|
newObject,
|
||||||
|
listObjectsByProperties,
|
||||||
|
getModelStats,
|
||||||
|
getModelHistory,
|
||||||
|
} from '../../database/database.js';
|
||||||
|
|
||||||
|
const logger = log4js.getLogger('Stock Locations');
|
||||||
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
|
export const listStockLocationsRouteHandler = async (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
page = 1,
|
||||||
|
limit = 25,
|
||||||
|
property = '',
|
||||||
|
filter = {},
|
||||||
|
search = '',
|
||||||
|
sort = '',
|
||||||
|
order = 'ascend'
|
||||||
|
) => {
|
||||||
|
const result = await listObjects({
|
||||||
|
model: stockLocationModel,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
property,
|
||||||
|
filter,
|
||||||
|
search,
|
||||||
|
sort,
|
||||||
|
order,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error listing stock locations.');
|
||||||
|
res.status(result.code).send(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`List of stock locations (Page ${page}, Limit ${limit}). Count: ${result.length}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listStockLocationsByPropertiesRouteHandler = async (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
properties = '',
|
||||||
|
filter = {},
|
||||||
|
masterFilter = {}
|
||||||
|
) => {
|
||||||
|
const result = await listObjectsByProperties({
|
||||||
|
model: stockLocationModel,
|
||||||
|
properties,
|
||||||
|
filter,
|
||||||
|
masterFilter,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error listing stock locations.');
|
||||||
|
res.status(result.code).send(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`List of stock locations. Count: ${result.length}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStockLocationRouteHandler = async (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
const result = await getObject({
|
||||||
|
model: stockLocationModel,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
if (result?.error) {
|
||||||
|
logger.warn(`Stock location not found with supplied id.`);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
logger.debug(`Retrieved stock location with ID: ${id}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const editStockLocationRouteHandler = async (req, res) => {
|
||||||
|
const id = new mongoose.Types.ObjectId(req.params.id);
|
||||||
|
|
||||||
|
const updateData = {
|
||||||
|
name: req.body.name,
|
||||||
|
notes: req.body.notes,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await editObject({
|
||||||
|
model: stockLocationModel,
|
||||||
|
id,
|
||||||
|
updateData,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
logger.error('Error editing stock location:', result.error);
|
||||||
|
res.status(result.code).send(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Edited stock location with ID: ${id}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const editMultipleStockLocationsRouteHandler = 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: stockLocationModel,
|
||||||
|
updates,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
logger.error('Error editing stock locations:', result.error);
|
||||||
|
res.status(result.code || 500).send(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Edited ${updates.length} stock locations`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const newStockLocationRouteHandler = async (req, res) => {
|
||||||
|
const newData = {
|
||||||
|
name: req.body.name,
|
||||||
|
notes: req.body.notes,
|
||||||
|
};
|
||||||
|
const result = await newObject({
|
||||||
|
model: stockLocationModel,
|
||||||
|
newData,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
if (result.error) {
|
||||||
|
logger.error('No stock location created:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`New stock location with ID: ${result._id}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteStockLocationRouteHandler = async (req, res) => {
|
||||||
|
const id = new mongoose.Types.ObjectId(req.params.id);
|
||||||
|
|
||||||
|
const result = await deleteObject({
|
||||||
|
model: stockLocationModel,
|
||||||
|
id,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
if (result.error) {
|
||||||
|
logger.error('No stock location deleted:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Deleted stock location with ID: ${result._id}`);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStockLocationStatsRouteHandler = async (req, res) => {
|
||||||
|
const result = await getModelStats({ model: stockLocationModel });
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error fetching stock location stats:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
logger.trace('Stock location stats:', result);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStockLocationHistoryRouteHandler = async (req, res) => {
|
||||||
|
const from = req.query.from;
|
||||||
|
const to = req.query.to;
|
||||||
|
const result = await getModelHistory({ model: stockLocationModel, from, to });
|
||||||
|
if (result?.error) {
|
||||||
|
logger.error('Error fetching stock location history:', result.error);
|
||||||
|
return res.status(result.code).send(result);
|
||||||
|
}
|
||||||
|
logger.trace('Stock location history:', result);
|
||||||
|
res.send(result);
|
||||||
|
};
|
||||||
461
src/services/inventory/stocktransfers.js
Normal file
461
src/services/inventory/stocktransfers.js
Normal file
@ -0,0 +1,461 @@
|
|||||||
|
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,
|
||||||
|
listObjects,
|
||||||
|
getObject,
|
||||||
|
editObject,
|
||||||
|
editObjects,
|
||||||
|
newObject,
|
||||||
|
listObjectsByProperties,
|
||||||
|
getModelStats,
|
||||||
|
getModelHistory,
|
||||||
|
checkStates,
|
||||||
|
} 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),
|
||||||
|
toStockLocation: l.toStockLocation?._id ?? l.toStockLocation,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createStockEventsForLine({ transferId, fromId, fromType, toId, toType, qty, unit }) {
|
||||||
|
const ts = new Date();
|
||||||
|
await stockEventModel.insertMany([
|
||||||
|
{
|
||||||
|
value: -Math.abs(qty),
|
||||||
|
unit,
|
||||||
|
parent: fromId,
|
||||||
|
parentType: fromType,
|
||||||
|
owner: transferId,
|
||||||
|
ownerType: 'stockTransfer',
|
||||||
|
timestamp: ts,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: Math.abs(qty),
|
||||||
|
unit,
|
||||||
|
parent: toId,
|
||||||
|
parentType: toType,
|
||||||
|
owner: transferId,
|
||||||
|
ownerType: 'stockTransfer',
|
||||||
|
timestamp: ts,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executePostedLine(transferId, line) {
|
||||||
|
const toLocId = line.toStockLocation;
|
||||||
|
const loc = await stockLocationModel.findById(toLocId).lean();
|
||||||
|
if (!loc) {
|
||||||
|
throw new Error(`Unknown stock location: ${toLocId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(line.quantity > 0)) {
|
||||||
|
throw new Error('Line quantity must be positive');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.fromStockType === 'filamentStock') {
|
||||||
|
const src = await filamentStockModel.findById(line.fromStock);
|
||||||
|
if (!src) throw new Error('From filament stock not found');
|
||||||
|
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 newNet = netAvail - line.quantity;
|
||||||
|
const ratio = netAvail > 0 ? newNet / netAvail : 0;
|
||||||
|
const newGross = (src.currentWeight?.gross ?? 0) * ratio;
|
||||||
|
await filamentStockModel.findByIdAndUpdate(src._id, {
|
||||||
|
$set: { 'currentWeight.net': newNet, 'currentWeight.gross': newGross },
|
||||||
|
});
|
||||||
|
|
||||||
|
const destWeight = {
|
||||||
|
net: line.quantity,
|
||||||
|
gross: line.quantity + tareBefore,
|
||||||
|
};
|
||||||
|
const dest = await filamentStockModel.create({
|
||||||
|
state: src.state,
|
||||||
|
startingWeight: destWeight,
|
||||||
|
currentWeight: destWeight,
|
||||||
|
filamentSku: src.filamentSku,
|
||||||
|
stockLocation: toLocId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createStockEventsForLine({
|
||||||
|
transferId,
|
||||||
|
fromId: src._id,
|
||||||
|
fromType: 'filamentStock',
|
||||||
|
toId: dest._id,
|
||||||
|
toType: 'filamentStock',
|
||||||
|
qty: line.quantity,
|
||||||
|
unit: 'g',
|
||||||
|
});
|
||||||
|
|
||||||
|
return { toStockType: 'filamentStock', toStock: dest._id };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.fromStockType === 'partStock') {
|
||||||
|
const src = await partStockModel.findById(line.fromStock);
|
||||||
|
console.log(src);
|
||||||
|
if (!src) throw new Error('From part stock not found');
|
||||||
|
const currentQuantity = src.state.type === 'new' ? src.startingQuantity : src.currentQuantity;
|
||||||
|
if (line.quantity > currentQuantity) {
|
||||||
|
throw new Error('Part transfer quantity exceeds current quantity');
|
||||||
|
}
|
||||||
|
await partStockModel.findByIdAndUpdate(src._id, {
|
||||||
|
$inc: { currentQuantity: -line.quantity },
|
||||||
|
});
|
||||||
|
|
||||||
|
const dest = await partStockModel.create({
|
||||||
|
partSku: src.partSku,
|
||||||
|
currentQuantity: line.quantity,
|
||||||
|
state: { type: 'new' },
|
||||||
|
sourceType: 'stockTransfer',
|
||||||
|
source: transferId,
|
||||||
|
stockLocation: toLocId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createStockEventsForLine({
|
||||||
|
transferId,
|
||||||
|
fromId: src._id,
|
||||||
|
fromType: 'partStock',
|
||||||
|
toId: dest._id,
|
||||||
|
toType: 'partStock',
|
||||||
|
qty: line.quantity,
|
||||||
|
unit: 'each',
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
if (line.quantity > src.currentQuantity) {
|
||||||
|
throw new Error('Product transfer quantity exceeds current quantity');
|
||||||
|
}
|
||||||
|
await productStockModel.findByIdAndUpdate(src._id, {
|
||||||
|
$inc: { currentQuantity: -line.quantity },
|
||||||
|
});
|
||||||
|
|
||||||
|
const dest = await productStockModel.create({
|
||||||
|
productSku: src.productSku,
|
||||||
|
currentQuantity: line.quantity,
|
||||||
|
state: { type: 'posted' },
|
||||||
|
postedAt: new Date(),
|
||||||
|
partStocks: [],
|
||||||
|
stockLocation: toLocId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createStockEventsForLine({
|
||||||
|
transferId,
|
||||||
|
fromId: src._id,
|
||||||
|
fromType: 'productStock',
|
||||||
|
toId: dest._id,
|
||||||
|
toType: 'productStock',
|
||||||
|
qty: line.quantity,
|
||||||
|
unit: 'each',
|
||||||
|
});
|
||||||
|
|
||||||
|
return { toStockType: 'productStock', toStock: dest._id };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unsupported from stock type: ${line.fromStockType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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: [
|
||||||
|
{ path: 'lines.fromStock' },
|
||||||
|
{ path: 'lines.toStockLocation' },
|
||||||
|
{ path: 'lines.toStock' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
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: [
|
||||||
|
{ path: 'lines.fromStock' },
|
||||||
|
{ path: 'lines.toStockLocation' },
|
||||||
|
{ path: 'lines.toStock' },
|
||||||
|
],
|
||||||
|
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 getStockTransferRouteHandler = async (req, res) => {
|
||||||
|
const id = req.params.id;
|
||||||
|
const result = await getObject({
|
||||||
|
model: stockTransferModel,
|
||||||
|
id,
|
||||||
|
populate: [
|
||||||
|
{ path: 'lines.fromStock' },
|
||||||
|
{ path: 'lines.toStockLocation' },
|
||||||
|
{ path: 'lines.toStock' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
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)),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await editObject({
|
||||||
|
model: stockTransferModel,
|
||||||
|
id,
|
||||||
|
updateData,
|
||||||
|
user: req.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
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' },
|
||||||
|
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 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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedLines = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const line of doc.lines) {
|
||||||
|
const plain = line.toObject();
|
||||||
|
const { toStockType, toStock } = await executePostedLine(doc._id, plain);
|
||||||
|
updatedLines.push({
|
||||||
|
...plain,
|
||||||
|
toStockType,
|
||||||
|
toStock,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const posted = await stockTransferModel
|
||||||
|
.findByIdAndUpdate(
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
state: { type: 'posted' },
|
||||||
|
postedAt: new Date(),
|
||||||
|
lines: updatedLines,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ new: true }
|
||||||
|
)
|
||||||
|
.populate([
|
||||||
|
{ path: 'lines.fromStock' },
|
||||||
|
{ path: 'lines.toStockLocation' },
|
||||||
|
{ path: 'lines.toStock' },
|
||||||
|
])
|
||||||
|
.lean();
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
@ -23,6 +23,8 @@ export const EXPORT_FILTER_BY_TYPE = {
|
|||||||
orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'sku._id', 'shipment._id'],
|
orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'sku._id', 'shipment._id'],
|
||||||
shipment: ['order._id', 'orderType', 'courierService._id'],
|
shipment: ['order._id', 'orderType', 'courierService._id'],
|
||||||
stockEvent: ['parent._id', 'parentType', 'owner._id', 'ownerType'],
|
stockEvent: ['parent._id', 'parentType', 'owner._id', 'ownerType'],
|
||||||
|
stockLocation: ['name'],
|
||||||
|
stockTransfer: ['state.type', 'postedAt'],
|
||||||
stockAudit: ['filamentStock._id', 'partStock._id'],
|
stockAudit: ['filamentStock._id', 'partStock._id'],
|
||||||
documentJob: ['documentTemplate', 'documentPrinter', 'object._id', 'objectType'],
|
documentJob: ['documentTemplate', 'documentPrinter', 'object._id', 'objectType'],
|
||||||
documentTemplate: ['parent._id', 'documentSize._id'],
|
documentTemplate: ['parent._id', 'documentSize._id'],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user