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

147 lines
6.9 KiB
JavaScript

// routes/pwoSourceAudit.js
// "PWO Source Report" — for each Production Order, shows whether each of its
// four lifecycle actions (Create / Release / Issue / Receipt) was done
// through this portal or directly in SAP B1. Pulled entirely from SAP B1
// Service Layer + this portal's own local tracking table.
//
// How each stage is told apart:
// - CREATE: SAP UDF U_ERP_SO_NO on the Production Order itself — stamped
// with this portal's own local record ID whenever it creates
// one (see routes/productionOrders.js). Blank = created
// directly in SAP.
// - RELEASE: no UDF exists for this — instead, compares SAP's live
// ProductionOrderStatus against this portal's own local
// tracking record's workflow log. A "Release" / "completed"
// entry only ever gets written by the portal's own Release
// route, so its presence (or absence, with SAP nonetheless
// showing Released) is what's checked here.
// - ISSUE / RECEIPT: no UDF exists on InventoryGenExits/InventoryGenEntries
// either, and DI API (needed to add one) isn't available on
// this SAP install — so the portal instead tags the Comments
// field of every Issue/Receipt document it posts with
// "[Portal PWO#<id>]" (see routes/sap.js). A document without
// that tag was posted directly in SAP.
//
// NOTE — this only works going forward from when each tagging mechanism was
// added: historical orders/documents posted before that have no tag either
// way and will show as "Direct (SAP)" even if they went through the portal.
'use strict';
const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const poStore = () => require('../services/productionOrderStore');
let _sapSvc = null;
function getSap(){
if(!_sapSvc) _sapSvc = require('../services/sapServiceLayer');
return _sapSvc;
}
const cq = (req) => req.query?.company || req.body?.company || null;
// Follows SAP's nextLink until exhausted or MAX_PAGES hit — no $top on the
// initial request (confirmed elsewhere in this app: SAP treats an explicit
// $top as a TOTAL cap across the whole nextLink chain here, not a per-page
// size, silently truncating a large result set otherwise).
async function fetchAllPaged(sap, co, entity, filter, select, orderByField = 'DocDate') {
const out = [];
let url = `${entity}?$filter=${encodeURIComponent(filter)}&$select=${select}&$orderby=${orderByField} desc`;
const MAX_PAGES = 300;
for (let p = 0; p < MAX_PAGES && url; p++) {
const r = await sap.sapRequest('GET', url, null, co);
out.push(...(r?.value || []));
url = r?.['odata.nextLink'] || r?.['@odata.nextLink'] || null;
if (!r?.value?.length) break;
}
return out;
}
router.get('/report', verifyToken, async (req, res) => {
const co = cq(req);
const { from, to } = req.query;
if (!from || !to) return res.status(400).json({ success: false, message: 'from and to dates are required' });
try {
const sap = getSap();
// ProductionOrders has no "DocDate" field (confirmed live — filtering on
// it 502s instead of a clean error) — PostingDate is its equivalent
// "when created" field.
const orders = await fetchAllPaged(
sap, co, 'ProductionOrders',
`PostingDate ge '${from}' and PostingDate le '${to}'`,
'AbsoluteEntry,DocumentNumber,ItemNo,ProductDescription,ProductionOrderStatus,PostingDate,U_ERP_SO_NO',
'PostingDate'
);
if (!orders.length) return res.json({ success: true, data: [] });
// Issue/Receipt can, in principle, happen any time after an order is
// created — but scanning with no upper bound turned out to be far too
// heavy in practice (2000+ Goods Issue documents for a single month on
// this install, ~100+ SAP pages, occasionally 502-ing SAP's own gateway
// under the load). Scoped to the SAME [from,to] window as the orders
// themselves instead — the known trade-off: an order created near the
// END of the selected range whose Issue/Receipt happens AFTER `to` will
// show that stage as "not reached yet" here even though it has been.
// Widen `to` (or re-run with a later range) to see those.
const genFilter = `DocDate ge '${from}' and DocDate le '${to}'`;
const [genExits, genEntries] = await Promise.all([
fetchAllPaged(sap, co, 'InventoryGenExits', genFilter, 'DocEntry,DocNum,DocumentLines'),
fetchAllPaged(sap, co, 'InventoryGenEntries', genFilter, 'DocEntry,DocNum,DocumentLines'),
]);
const localRecs = await poStore().listProductionOrders({ company: co });
const localByEntry = {};
localRecs.forEach(p => { if (p.sapAbsEntry != null) localByEntry[p.sapAbsEntry] = p; });
// Portal-origin marker is now a genuine per-LINE UDF (IGE1/IGN1's own
// U_ERP_SO_NO — set directly in routes/sap.js's creation payload, holding
// the same local Production Order tracking ID as OWOR.U_ERP_SO_NO).
// Checked per matching LINE, not per whole document, since one document
// could in principle carry lines for more than one Production Order.
function groupByBaseEntry(docs) {
const map = {};
docs.forEach(d => {
(d.DocumentLines || []).forEach(l => {
if (l.BaseType === 202 && l.BaseEntry != null) (map[l.BaseEntry] = map[l.BaseEntry] || []).push(l);
});
});
return map;
}
const issueByEntry = groupByBaseEntry(genExits);
const receiptByEntry = groupByBaseEntry(genEntries);
function sourceLabel(lines) {
if (!lines || !lines.length) return null; // stage not reached yet
const tagged = lines.filter(l => !!l.U_ERP_SO_NO).length;
if (tagged === lines.length) return 'Portal';
if (tagged === 0) return 'Direct (SAP)';
return 'Mixed';
}
const rows = orders.map(o => {
const local = localByEntry[o.AbsoluteEntry];
const released = o.ProductionOrderStatus === 'boposReleased' || o.ProductionOrderStatus === 'boposClosed';
let releaseSource = null;
if (released) {
const hasPortalRelease = !!(local && Array.isArray(local.workflowLog) && local.workflowLog.some(l => l.step === 'Release' && l.action === 'completed'));
releaseSource = hasPortalRelease ? 'Portal' : 'Direct (SAP)';
}
return {
absoluteEntry: o.AbsoluteEntry,
docNum: o.DocumentNumber,
itemCode: o.ItemNo,
itemName: o.ProductDescription,
status: (o.ProductionOrderStatus || '').replace('bopos', ''),
creationSource: o.U_ERP_SO_NO ? 'Portal' : 'Direct (SAP)',
releaseSource,
issueSource: sourceLabel(issueByEntry[o.AbsoluteEntry]),
receiptSource: sourceLabel(receiptByEntry[o.AbsoluteEntry]),
};
});
res.json({ success: true, data: rows });
} catch (err) {
res.status(400).json({ success: false, message: err.message });
}
});
module.exports = router;