Files
John eead8f5ffd
SAP-ERP Portal CI/CD / build (push) Successful in 3m57s
sale order
2026-10-05 18:45:17 +05:30

269 lines
13 KiB
JavaScript

// backend/routes/itemApproval.js
'use strict';
const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
let _store = null;
function getStore() {
if (!_store) _store = require('../services/hanaItemStore');
return _store;
}
let _sapSvc = null;
function getSap() {
if (!_sapSvc) _sapSvc = require('../services/sapServiceLayer');
return _sapSvc;
}
const appSettings = () => require('../services/appSettingsStore');
// admin/sap_adder/system_admin may push directly and approve/reject others'
// submissions. Kept as one helper so the client (public/approvals.html's
// canManageItems()) and server always agree on who this is.
function isItemApprover(user) {
return user?.role === 'admin' || user?.role === 'sap_adder' || user?.role === 'system_admin';
}
const cq = (req) => req.query?.company || req.body?.company || null;
async function pushItemToSap(data, co) {
const sap = getSap();
const payload = buildSapPayload(data);
await sap.sapRequest('POST', 'Items', payload, co);
if (data.DefaultWarehouse) {
try {
await sap.sapRequest('PATCH',
`Items('${encodeURIComponent(payload.ItemCode)}')`,
{ DefaultWarehouse: data.DefaultWarehouse },
co
);
} catch (we) {
console.warn('[ITEM-APPROVAL] DefaultWarehouse PATCH failed (non-fatal):', we.message);
}
}
return payload;
}
// ── Build SAP item payload from stored itemData ────────────────────────────────
function buildSapPayload(data) {
const d = data || {};
const payload = {
ItemCode: (d.ItemCode || '').trim().toUpperCase(),
ItemName: (d.ItemName || '').trim(),
ItemType: d.ItemType || 'itItems',
ItemClass: d.ItemClass || 'itcMaterial',
InventoryItem: d.InventoryItem || 'tYES',
SalesItem: d.SalesItem || 'tYES',
PurchaseItem: d.PurchaseItem || 'tYES',
VatLiable: 'tYES',
Valid: 'tYES',
Frozen: d.Frozen || 'tNO',
ManageSerialNumbers: d.ManageSerialNumbers || 'tNO',
ManageBatchNumbers: d.ManageBatchNumbers || 'tNO',
SRIAndBatchManageMethod: d.SRIAndBatchManageMethod || 'bomm_OnEveryTransaction',
WTLiable: d.WTLiable || 'tNO',
NoDiscounts: d.NoDiscounts || 'tNO',
TreeType: d.TreeType || 'iNotATree',
AssetItem: 'tNO',
GLMethod: d.GLMethod || 'glm_ItemClass',
CostAccountingMethod: d.CostAccountingMethod || 'bis_MovingAverage',
TaxType: 'tt_Yes',
IssueMethod: d.IssueMethod || 'im_Manual',
PlanningSystem: d.PlanningSystem || 'bop_None',
ProcurementMethod: d.ProcurementMethod || 'bom_Buy',
ComponentWarehouse: d.ComponentWarehouse || 'bomcw_BOM',
MaterialType: d.MaterialType || 'mt_FinishedGoods',
ProductSource: d.ProductSource || '0',
ManageStockByWarehouse: 'tNO',
InCostRollup: d.InCostRollup || 'tYES',
};
if (d.ItemsGroupCode) payload.ItemsGroupCode = parseInt(d.ItemsGroupCode);
if (d.UoMGroupEntry) payload.UoMGroupEntry = parseInt(d.UoMGroupEntry);
if (d.InventoryUOM) payload.InventoryUOM = String(d.InventoryUOM).toUpperCase();
if (d.SalesUnit) payload.SalesUnit = String(d.SalesUnit).toUpperCase();
if (d.PurchaseUnit) payload.PurchaseUnit = String(d.PurchaseUnit).toUpperCase();
// accept both form-style keys and frontend SAP-style keys
const gst = d.GSTRelevant || d.GSTRelevnt;
if (gst) payload.GSTRelevnt = gst;
if (d.GSTTaxCategory) payload.GSTTaxCategory = d.GSTTaxCategory;
if (d.Excisable) payload.Excisable = d.Excisable;
if (d.AssessableValue) payload.AssessableValue = Number(d.AssessableValue) || 0;
if (d.HsnCode && /^\d+$/.test(String(d.HsnCode))) payload.ChapterID = parseInt(d.HsnCode);
else if (d.ChapterID) payload.ChapterID = d.ChapterID;
if (d.DefaultWarehouse) payload.DefaultWarehouse = d.DefaultWarehouse;
if (d.DefaultVendor) payload.Mainsupplier = d.DefaultVendor;
else if (d.Mainsupplier) payload.Mainsupplier = d.Mainsupplier;
if (d.LeadTime) payload.LeadTime = parseInt(d.LeadTime);
if (d.UserText) payload.User_Text = d.UserText;
else if (d.User_Text) payload.User_Text = d.User_Text;
if (d.ProductionStandardCost) payload.ProdStdCost = Number(d.ProductionStandardCost) || 0;
else if (d.ProdStdCost) payload.ProdStdCost = Number(d.ProdStdCost) || 0;
if (d.PurchaseQtyPerPackage) payload.PurchaseQtyPerPackUnit = Number(d.PurchaseQtyPerPackage) || 1;
else if (d.PurchaseQtyPerPackUnit) payload.PurchaseQtyPerPackUnit = Number(d.PurchaseQtyPerPackUnit) || 1;
if (d.PurchaseItemsPerUnit) payload.PurchaseItemsPerUnit = Number(d.PurchaseItemsPerUnit) || 1;
if (d.SalesQtyPerPackage) payload.SalesQtyPerPackUnit = Number(d.SalesQtyPerPackage) || 1;
else if (d.SalesQtyPerPackUnit) payload.SalesQtyPerPackUnit = Number(d.SalesQtyPerPackUnit) || 1;
if (d.MinimumOrderQuantity) payload.MinOrderQuantity = Number(d.MinimumOrderQuantity) || 0;
else if (d.MinOrderQuantity) payload.MinOrderQuantity = Number(d.MinOrderQuantity) || 0;
if (d.MinInventory !== undefined) payload.MinInventory = Number(d.MinInventory) || 0;
if (d.MaxInventory !== undefined) payload.MaxInventory = Number(d.MaxInventory) || 0;
if (d.DesiredInventory !== undefined) payload.DesiredInventory = Number(d.DesiredInventory) || 0;
// Remove blank/null keys
Object.keys(payload).forEach(k => {
if (payload[k] === '' || payload[k] === null || payload[k] === undefined) delete payload[k];
});
return payload;
}
// ── POST /api/item-approvals/submit ─────────────────────────────────────────
// admin/sap_adder/system_admin → push directly to SAP (no approval queue)
// all other roles → save as PENDING for admin review
// Admin → System Settings → "Item Approval Workflow": when OFF, the whole
// queue is bypassed and EVERYONE pushes directly (there's no one left who'd
// review a pending item) — same as an approver submitting, regardless of role.
router.post('/submit', verifyToken, async (req, res) => {
try {
const store = getStore();
const b = req.body;
if (!b.ItemCode || !b.ItemName) {
return res.status(400).json({ success: false, message: 'ItemCode and ItemName are required' });
}
const { company, ...itemData } = b;
const itemCode = b.ItemCode.trim().toUpperCase();
const itemName = b.ItemName.trim();
const co = company || null;
const baseItem = {
itemCode, itemName, itemData,
submittedBy: req.user.username,
submittedByName: req.user.name || req.user.username,
company: company || '',
};
if (isItemApprover(req.user) || !appSettings().itemApprovalEnabled()) {
const payload = await pushItemToSap(itemData, co);
const logEntry = {
username: req.user.username, name: req.user.name || req.user.username,
role: req.user.role, action: 'approve', comment: 'Direct push',
timestamp: new Date().toISOString(),
};
const request = await store.insertItem({ ...baseItem, status: 'PENDING', approvalLog: [logEntry] });
await store.updateItem(request.id, {
status: 'SAP_PUSHED',
approvedBy: req.user.username,
approvedAt: new Date().toISOString(),
sapItemCode: payload.ItemCode,
});
console.log(`[ITEM-APPROVAL] ✅ Direct push — Item ${payload.ItemCode} created in SAP`);
return res.json({ success: true, message: `Item ${payload.ItemCode} created in SAP B1!`, id: request.id, status: 'SAP_PUSHED', itemCode: payload.ItemCode });
}
// user / manager / sr_manager — queue for approval
const request = await store.insertItem({ ...baseItem, status: 'PENDING', approvalLog: [] });
res.json({ success: true, message: 'Item submitted for admin approval', id: request.id, status: 'PENDING' });
} catch (err) {
console.error('[ITEM-APPROVAL] submit error:', err.message);
res.status(500).json({ success: false, message: err.message });
}
});
// ── GET /api/item-approvals ──────────────────────────────────────────────────
router.get('/', verifyToken, async (req, res) => {
try {
const store = getStore();
const status = (req.query.status || 'ALL').toUpperCase();
const company = cq(req);
// 'user' role can only see their own submissions
const submittedBy = req.user.role === 'user' ? req.user.username : null;
const data = await store.listByStatus(status, company, submittedBy);
res.json({ success: true, data });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// ── GET /api/item-approvals/:id ──────────────────────────────────────────────
router.get('/:id', verifyToken, async (req, res) => {
try {
const item = await getStore().findById(req.params.id);
if (!item) return res.status(404).json({ success: false, message: 'Not found' });
res.json({ success: true, data: item });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// ── PATCH /api/item-approvals/:id/action ────────────────────────────────────
// action: 'approve' | 'reject'
// Admin can also pass editedData to override item fields before pushing
router.patch('/:id/action', verifyToken, async (req, res) => {
if (!isItemApprover(req.user)) {
return res.status(403).json({ success: false, message: 'Only admin/SAP Adder/system admin can approve items' });
}
try {
const store = getStore();
const item = await store.findById(req.params.id);
if (!item) return res.status(404).json({ success: false, message: 'Not found' });
const { action, comment, editedData } = req.body;
if (!['approve', 'reject'].includes(action)) {
return res.status(400).json({ success: false, message: 'action must be approve or reject' });
}
if (item.status === 'SAP_PUSHED') {
return res.status(400).json({ success: false, message: 'Item already pushed to SAP' });
}
if (item.status === 'REJECTED') {
return res.status(400).json({ success: false, message: 'Item is already rejected. Ask submitter to re-submit.' });
}
if (item.status !== 'PENDING') {
return res.status(400).json({ success: false, message: `Cannot act on item with status: ${item.status}` });
}
const logEntry = {
username: req.user.username,
name: req.user.name || req.user.username,
role: req.user.role,
action,
comment: comment || '',
timestamp: new Date().toISOString(),
};
if (action === 'reject') {
await store.updateItem(item.id, {
status: 'REJECTED',
approvalLog: [...(item.approvalLog || []), logEntry],
rejectedBy: req.user.username,
rejectedAt: new Date().toISOString(),
adminNotes: comment || '',
});
return res.json({ success: true, message: 'Item request rejected', status: 'REJECTED' });
}
// ── APPROVE: merge editedData if admin changed fields, then push to SAP ──
const baseData = item.itemData || {};
const mergedData = editedData ? { ...baseData, ...editedData } : baseData;
const co = cq(req) || item.company || null;
console.log('[ITEM-APPROVAL] Pushing to SAP:', (mergedData.ItemCode || '').trim().toUpperCase());
const payload = await pushItemToSap(mergedData, co);
await store.updateItem(item.id, {
status: 'SAP_PUSHED',
approvalLog: [...(item.approvalLog || []), logEntry],
adminNotes: comment || '',
adminEditedData: editedData || null,
approvedBy: req.user.username,
approvedAt: new Date().toISOString(),
sapItemCode: payload.ItemCode,
});
console.log(`[ITEM-APPROVAL] ✅ Item ${payload.ItemCode} created in SAP`);
res.json({ success: true, message: `Item ${payload.ItemCode} created in SAP B1!`, status: 'SAP_PUSHED', itemCode: payload.ItemCode });
} catch (err) {
console.error('[ITEM-APPROVAL] action error:', err.message);
res.status(500).json({ success: false, message: err.message });
}
});
module.exports = router;