Files
John 69b4e68baf
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s
first commit
2026-09-23 17:31:02 +05:30

177 lines
11 KiB
JavaScript

'use strict';
// routes/prePwo.js — "Pre-PWO" staging (Admin → System Settings → "Pre-PWO —
// Store Review Before SAP"). See services/prePwoStore.js for the full design
// note. This file is ONLY the staging record's CRUD + the Production<->Store
// review round trip. Actually creating the real SAP Production Order still
// goes through the existing, unmodified routes/sap.js + routes/productionOrders.js
// endpoints — the frontend, after a successful SAP creation, calls
// POST /:id/mark-converted here purely to lock the staging row.
const express = require('express');
const router = express.Router();
const { verifyToken, hasStepPerm, hasStepAssigned } = require('../middleware/auth');
const store = () => require('../services/prePwoStore');
const poStore = () => require('../services/productionOrderStore');
const woStore = () => require('../services/workOrderStore');
const appSettings = require('../services/appSettingsStore');
function requireAnyStep(fullKeys, perm) {
return (req, res, next) => {
if (fullKeys.some(k => hasStepPerm(req.user, k, perm))) return next();
res.status(403).json({ success: false, message: `You are not assigned "${perm}" on approval step: ${fullKeys.join(' or ')}` });
};
}
// For viewing (list/get), holding ANY perm on ANY of these steps is enough —
// e.g. a Store user who only has 'add' checked on prepwo_review (no 'view')
// still needs to reach the list to find what's been shared with them.
function requireAnyStepAssigned(fullKeys) {
return (req, res, next) => {
if (fullKeys.some(k => hasStepAssigned(req.user, k))) return next();
res.status(403).json({ success: false, message: `You are not assigned to any of: ${fullKeys.join(', ')}` });
};
}
const VIEW_STEPS = ['production_order:create', 'production_order:prepwo_share', 'production_order:prepwo_review'];
router.get('/', verifyToken, requireAnyStepAssigned(VIEW_STEPS), async (req, res) => {
try {
const { mine, company, workOrderId, status } = req.query;
const data = await store().listPrePwos({ mine: mine === '1' ? req.user.username : undefined, company, workOrderId, status });
res.json({ success: true, data });
} catch (err) { res.status(500).json({ success: false, message: err.message }); }
});
router.get('/:id', verifyToken, requireAnyStepAssigned(VIEW_STEPS), async (req, res) => {
try {
const r = await store().findById(req.params.id);
if (!r || r.isDeleted) return res.status(404).json({ success: false, message: 'Not found' });
res.json({ success: true, data: r });
} catch (err) { res.status(500).json({ success: false, message: err.message }); }
});
// Create the staging record — the app-DB-only substitute for what would
// otherwise be an immediate SAP write. Only from a Work Order (per the
// original ask: "After approval of WO"); manual/standalone Production
// Orders are unaffected by this feature and keep writing straight to SAP.
router.post('/', verifyToken, (req, res, next) => {
if (hasStepPerm(req.user, 'production_order:create', 'add')) return next();
res.status(403).json({ success: false, message: 'You are not assigned "add" on approval step: production_order:create' });
}, async (req, res) => {
try {
if (!appSettings.preWoStoreReviewEnabled())
return res.status(403).json({ success: false, message: 'The Pre-PWO Store Review workflow is disabled in Admin → System Settings' });
const b = req.body || {};
if (!b.workOrderId) return res.status(400).json({ success: false, message: 'workOrderId is required' });
const wo = await woStore().findById(b.workOrderId);
if (!wo || wo.isDeleted) return res.status(404).json({ success: false, message: 'Work order not found' });
// Same "one Production Order per Work Order" guarantee as the direct-to-SAP
// path (routes/sap.js), checked against BOTH a real PO already existing
// AND an open (not-yet-converted) Pre-PWO already staged for this WO.
const existingPo = await poStore().listProductionOrders({ workOrderId: b.workOrderId });
if (existingPo.length)
return res.status(409).json({ success: false, message: `A Production Order (${existingPo[0].sapDocNum || '#' + existingPo[0].id}) has already been generated for this Work Order.` });
const existingPre = await store().findOpenByWorkOrderId(b.workOrderId);
if (existingPre)
return res.status(409).json({ success: false, message: `A Pre-PWO is already staged for this Work Order (${existingPre.reviewStage === 0 ? 'not yet shared' : existingPre.reviewStage === 1 ? 'awaiting Store review' : 'reviewed by Store'}).` });
if (!Array.isArray(b.lines) || !b.lines.length)
return res.status(400).json({ success: false, message: 'At least one component line is required' });
const saved = await store().insertPrePwo({
workOrderId: b.workOrderId,
itemCode: b.itemCode || wo.productCode, itemName: b.itemName || wo.productName,
plannedQty: b.plannedQty || wo.totalUnits, batchNumber: b.batchNumber || wo.batchNumber,
mfgDate: b.mfgDate || wo.mfgDate, expDate: b.expDate || wo.expDate,
warehouse: b.warehouse || '', fgWarehouse: b.fgWarehouse || '',
lines: b.lines, remarks: b.remarks || '', company: b.company || '',
createdBy: req.user.username, createdByName: req.user.name || req.user.username,
});
res.json({ success: true, data: saved });
} catch (err) { res.status(500).json({ success: false, message: err.message }); }
});
// Production edits header/lines directly (with or without ever involving
// Store) — any time before the Pre-PWO is converted to a real SAP order.
router.put('/:id', verifyToken, requireAnyStep(['production_order:create'], 'edit'), async (req, res) => {
try {
const existing = await store().findById(req.params.id);
if (!existing || existing.isDeleted) return res.status(404).json({ success: false, message: 'Not found' });
if (existing.status === 'CONVERTED') return res.status(409).json({ success: false, message: 'Already pushed to SAP — locked' });
const b = req.body || {};
if (!Array.isArray(b.lines) || !b.lines.length)
return res.status(400).json({ success: false, message: 'At least one component line is required' });
const updated = await store().updatePrePwo(req.params.id, {
itemCode: b.itemCode ?? existing.itemCode, itemName: b.itemName ?? existing.itemName,
plannedQty: b.plannedQty ?? existing.plannedQty, batchNumber: b.batchNumber ?? existing.batchNumber,
mfgDate: b.mfgDate ?? existing.mfgDate, expDate: b.expDate ?? existing.expDate,
warehouse: b.warehouse ?? existing.warehouse, fgWarehouse: b.fgWarehouse ?? existing.fgWarehouse,
lines: b.lines, remarks: b.remarks ?? existing.remarks,
});
res.json({ success: true, data: updated });
} catch (err) { res.status(500).json({ success: false, message: err.message }); }
});
// Production → Store (optional — no hard gate anywhere downstream).
router.post('/:id/share-with-store', verifyToken, requireAnyStep(['production_order:prepwo_share'], 'add'), async (req, res) => {
try {
if (!appSettings.preWoStoreReviewEnabled())
return res.status(403).json({ success: false, message: 'The Pre-PWO Store Review workflow is disabled in Admin → System Settings' });
const existing = await store().findById(req.params.id);
if (!existing || existing.isDeleted) return res.status(404).json({ success: false, message: 'Not found' });
if (existing.status === 'CONVERTED') return res.status(409).json({ success: false, message: 'Already pushed to SAP — locked' });
if (existing.reviewStage !== 0)
return res.status(409).json({ success: false, message: `Already ${existing.reviewStage === 1 ? 'shared with Store, awaiting their review' : 'been through Store review'}` });
const updated = await store().shareWithStore(req.params.id, { by: req.user.username, byName: req.user.name || req.user.username });
res.json({ success: true, data: updated });
} catch (err) { res.status(500).json({ success: false, message: err.message }); }
});
// Store → Production: may add/remove/substitute component lines outright —
// single round trip, no further back-and-forth.
router.post('/:id/revert-to-production', verifyToken, requireAnyStep(['production_order:prepwo_review'], 'add'), async (req, res) => {
try {
if (!appSettings.preWoStoreReviewEnabled())
return res.status(403).json({ success: false, message: 'The Pre-PWO Store Review workflow is disabled in Admin → System Settings' });
const existing = await store().findById(req.params.id);
if (!existing || existing.isDeleted) return res.status(404).json({ success: false, message: 'Not found' });
if (existing.status === 'CONVERTED') return res.status(409).json({ success: false, message: 'Already pushed to SAP — locked' });
if (existing.reviewStage !== 1)
return res.status(409).json({ success: false, message: existing.reviewStage === 0 ? 'This Pre-PWO has not been shared by Production yet' : 'Already reverted to Production' });
const b = req.body || {};
if (!Array.isArray(b.lines) || !b.lines.length)
return res.status(400).json({ success: false, message: 'At least one component line is required' });
const updated = await store().revertToProduction(req.params.id, {
by: req.user.username, byName: req.user.name || req.user.username, lines: b.lines, remarks: b.remarks || '',
});
res.json({ success: true, data: updated });
} catch (err) { res.status(500).json({ success: false, message: err.message }); }
});
// Called by the frontend immediately after it has successfully created the
// real SAP Production Order (via the existing /api/sap/production-order +
// /api/production-orders routes, unchanged) from this Pre-PWO's current
// lines. Purely locks the staging row — does not itself touch SAP.
router.post('/:id/mark-converted', verifyToken, requireAnyStep(['production_order:create'], 'add'), async (req, res) => {
try {
const existing = await store().findById(req.params.id);
if (!existing || existing.isDeleted) return res.status(404).json({ success: false, message: 'Not found' });
if (existing.status === 'CONVERTED') return res.json({ success: true, data: existing });
const { convertedPoId } = req.body || {};
if (!convertedPoId) return res.status(400).json({ success: false, message: 'convertedPoId is required' });
const updated = await store().markConverted(req.params.id, { convertedPoId });
res.json({ success: true, data: updated });
} catch (err) { res.status(500).json({ success: false, message: err.message }); }
});
router.delete('/:id', verifyToken, requireAnyStep(['production_order:create'], 'delete'), async (req, res) => {
try {
const existing = await store().findById(req.params.id);
if (!existing) return res.status(404).json({ success: false, message: 'Not found' });
if (existing.status === 'CONVERTED') return res.status(409).json({ success: false, message: 'Already pushed to SAP — cannot delete' });
await store().softDelete(req.params.id);
res.json({ success: true });
} catch (err) { res.status(500).json({ success: false, message: err.message }); }
});
module.exports = router;