first commit
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s

This commit is contained in:
John
2026-09-23 17:31:02 +05:30
commit 69b4e68baf
51657 changed files with 3864077 additions and 0 deletions
+404
View File
@@ -0,0 +1,404 @@
'use strict';
const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const { getPool } = require('../services/sqlPool');
const costingStore = () => require('../services/costingStore');
// pl_account_map / pl_overrides live on the app's own database now — the
// ledger functions below used to JOIN them with OACT/JDT1 (real SAP data)
// in one query, which only worked when both lived on the same server. Now
// they fetch each side separately (SAP pool for OACT/JDT1, costingStore for
// the app tables) and merge in JS. This helper sums override_val for a set
// of account codes from costingStore.getOverrides()'s result.
function sumOverridesForCodes(overrides, codes) {
const set = new Set(codes);
return overrides
.filter(o => set.has(o.acct_code))
.reduce((s, o) => s + (parseFloat(o.override_val) || 0), 0);
}
const BB_GROUPS = ['FG BLOOD BAG'];
const CAPD_GROUPS = ['FG CAPD', 'FG CAPD ACCESSORIES'];
// Employee P&L accounts to exclude from cost
// 5103030xxx = S&M department accounts; 5240040xxx = other excluded codes
const EXCLUDE_EMP_CODES = [
'5103030001','5103030002','5103030003','5103030004','5103030005',
'5103030006','5103030008','5103030010','5103030012','5103030013',
'5240040001','5240040002','5240040003','5240040004','5240040005',
'5240040006','5240040008','5240040009','5240040010','5240040012',
'5240040013','5240040015',
];
// ── Production: stock transfer receipts into WH01 (TransType=67) via OINM ──
async function fetchProduction(pool, from, to) {
const r = await pool.request()
.input('from', from).input('to', to)
.query(`
SELECT O.ItemCode,
MAX(T0.ItemName) AS ItemDescription,
MAX(T2.ItmsGrpNam) AS ItemGroup,
SUM(O.InQty) AS ProductionQty
FROM OINM O
INNER JOIN OITM T0 ON T0.ItemCode = O.ItemCode
INNER JOIN OITB T2 ON T2.ItmsGrpCod = T0.ItmsGrpCod
WHERE O.TransType = 67
AND O.DocDate >= @from AND O.DocDate <= @to
AND O.Warehouse = '01'
AND T2.ItmsGrpNam IN ('FG BLOOD BAG','FG CAPD','FG CAPD ACCESSORIES')
GROUP BY O.ItemCode
ORDER BY MAX(T2.ItmsGrpNam), O.ItemCode
`);
return r.recordset || [];
}
// ── Sales: invoices minus credit notes ─────────────────────────────────────
async function fetchSales(pool, from, to) {
const r = await pool.request()
.input('from', from).input('to', to)
.query(`
SELECT ItemCode, SUM(SalesQty) AS SalesQty
FROM (
SELECT T0.ItemCode, SUM(T0.Quantity) AS SalesQty
FROM INV1 T0
INNER JOIN OINV T1 ON T1.DocEntry = T0.DocEntry
WHERE T1.DocDate >= @from AND T1.DocDate <= @to
AND T1.CANCELED = 'N' AND T0.ItemCode <> ''
GROUP BY T0.ItemCode
UNION ALL
SELECT T0.ItemCode, -SUM(T0.Quantity) AS SalesQty
FROM RIN1 T0
INNER JOIN ORIN T1 ON T1.DocEntry = T0.DocEntry
WHERE T1.DocDate >= @from AND T1.DocDate <= @to
AND T1.CANCELED = 'N' AND T0.ItemCode <> ''
GROUP BY T0.ItemCode
) S
GROUP BY ItemCode
`);
return r.recordset || [];
}
// ── BOM price per finished item (any price list, Price > 0) ─────────────────
async function fetchBOM(pool) {
const r = await pool.request().query(`
SELECT T1.Code AS ItemCode,
MIN(P.Price) AS BOMPrice
FROM OITT T1
INNER JOIN OITM T0 ON T0.ItemCode = T1.Code
LEFT JOIN ITM1 P ON P.ItemCode = T1.Code
LEFT JOIN OITB T2 ON T2.ItmsGrpCod = T0.ItmsGrpCod
WHERE T2.ItmsGrpNam IN ('FG BLOOD BAG','FG CAPD','FG CAPD ACCESSORIES')
AND P.Price > 0
GROUP BY T1.Code
`);
return r.recordset || [];
}
// ── Employee pool from P&L trial balance ────────────────────────────────────
// Formula: Employee-mapped accounts (excl. S&M names + listed codes)
// + MEDICAL EXPENSES (5190090026) + FESTIVAL EXPENSES (5190090029)
// + NET = period_debit - period_credit + provision (pl_overrides)
async function fetchEmployeePool(pool, from, to) {
try {
// pl_account_map (app DB): which accounts are on the "Employee" sheet.
const mapping = await costingStore().getMapping();
const empCodes = mapping
.filter(m => m.sheet === 'Employee' && !EXCLUDE_EMP_CODES.includes(m.acct_code))
.map(m => m.acct_code);
let net = 0;
if (empCodes.length) {
const req = pool.request().input('from', from).input('to', to);
const inList = empCodes.map((c, i) => { req.input(`e${i}`, c); return `@e${i}`; }).join(',');
const r = await req.query(`
SELECT
SUM(CASE WHEN T1.RefDate BETWEEN @from AND @to
THEN ISNULL(T1.Debit,0) - ISNULL(T1.Credit,0) ELSE 0 END) AS net
FROM OACT T0
LEFT JOIN JDT1 T1 ON T0.AcctCode = T1.Account
WHERE T0.AcctCode IN (${inList})
AND T0.AcctName NOT LIKE '%S&M%'
`);
net += parseFloat(r.recordset[0]?.net || 0);
}
// Medical Expenses + Festival Expenses: gross Debit only (not net)
const rMed = await pool.request()
.input('from', from).input('to', to)
.query(`
SELECT
SUM(CASE WHEN T1.RefDate BETWEEN @from AND @to
THEN ISNULL(T1.Debit,0) ELSE 0 END) AS net
FROM OACT T0
LEFT JOIN JDT1 T1 ON T0.AcctCode = T1.Account
WHERE T0.AcctCode IN ('5190090026', '5190090029')
`);
net += parseFloat(rMed.recordset[0]?.net || 0);
// pl_overrides (app DB): provisions for every matched account this period.
const overrides = await costingStore().getOverrides(from, to);
const prov = sumOverridesForCodes(overrides, [...empCodes, '5190090026', '5190090029']);
return net + prov;
} catch (_) {
return 0;
}
}
// Shared helper for the 4 fixed-account-list ledgers below: SAP debit-credit
// net for a set of account codes over the period, via the SAP pool.
async function fetchLedgerNet(pool, codes, from, to) {
const req = pool.request().input('fromDate', from).input('toDate', to);
const inList = codes.map((c, i) => { req.input(`c${i}`, c); return `@c${i}`; }).join(',');
const r = await req.query(`
SELECT
SUM(CASE WHEN T1.[RefDate] BETWEEN @fromDate AND @toDate
THEN ISNULL(T1.[Debit],0) - ISNULL(T1.[Credit],0) ELSE 0 END) AS net
FROM OACT T0
LEFT JOIN JDT1 T1 ON T0.[AcctCode] = T1.[Account]
WHERE T0.[AcctCode] IN (${inList})
`);
return parseFloat(r.recordset[0]?.net || 0);
}
// ── Boiler cost: mirrors P&L fetchTrialBalance formula exactly ───────────────
// NET = period_debit - period_credit + provision (pl_overrides.override_val)
async function fetchBoilerLedger(pool, from, to) {
try {
const codes = ['5110010010'];
const [net, overrides] = await Promise.all([
fetchLedgerNet(pool, codes, from, to),
costingStore().getOverrides(from, to),
]);
return net + sumOverridesForCodes(overrides, codes);
} catch (_) { return 0; }
}
// ── Power cost: GENSET (5110010008) + ELECTRICITY (5110010009) + provisions ───
async function fetchPowerLedger(pool, from, to) {
try {
const codes = ['5110010008', '5110010009'];
const [net, overrides] = await Promise.all([
fetchLedgerNet(pool, codes, from, to),
costingStore().getOverrides(from, to),
]);
return net + sumOverridesForCodes(overrides, codes);
} catch (_) { return 0; }
}
// ── R&D cost: R&D expense accounts + provisions ───────────────────────────────
async function fetchRdLedger(pool, from, to) {
try {
const codes = ['5240040018', '5240040019', '5240040020', '5240040021'];
const [net, overrides] = await Promise.all([
fetchLedgerNet(pool, codes, from, to),
costingStore().getOverrides(from, to),
]);
return net + sumOverridesForCodes(overrides, codes);
} catch (_) { return 0; }
}
// ── R&M cost: all Repair & Maintenance accounts + provisions ─────────────────
async function fetchRepairLedger(pool, from, to) {
try {
const codes = ['5190090010', '5190090011', '5190090012', '5190090013', '5190090028'];
const [net, overrides] = await Promise.all([
fetchLedgerNet(pool, codes, from, to),
costingStore().getOverrides(from, to),
]);
return net + sumOverridesForCodes(overrides, codes);
} catch (_) { return 0; }
}
// ── Split a pool into BB / CAPD using custom % or RM-based auto ──────────────
function splitPool(total, bbPctStr, bbRM, totalRM) {
const frac = bbPctStr != null
? Math.min(100, Math.max(0, parseFloat(bbPctStr) || 0)) / 100
: totalRM > 0 ? bbRM / totalRM : 0;
return [total * frac, total * (1 - frac)];
}
// ── Main data endpoint ───────────────────────────────────────────────────────
router.get('/data', verifyToken, async (req, res) => {
try {
const {
from, to, salaryPeriod, overrideEmpPool, overrideBoilerLedger, overridePowerLedger, overrideRepairLedger, overrideRdLedger,
bbEmpPct, bbQaPct, bbQcPct, bbBoilerPct, bbPowerPct, bbRepairPct, bbRdPct,
empPowerPct, qaPowerPct, qcPowerPct, boilerPowerPct, repairPowerPct, rdPowerPct,
} = req.query;
if (!from || !to)
return res.status(400).json({ success: false, message: 'from and to dates are required' });
const pool = await getPool();
const [production, sales, bom, liveEmpPool, liveBoilerLedger, livePowerLedger, liveRepairLedger, liveRdLedger] = await Promise.all([
fetchProduction(pool, from, to),
fetchSales(pool, from, to),
fetchBOM(pool),
overrideEmpPool != null ? Promise.resolve(null) : fetchEmployeePool(pool, from, to),
overrideBoilerLedger != null ? Promise.resolve(null) : fetchBoilerLedger(pool, from, to),
overridePowerLedger != null ? Promise.resolve(null) : fetchPowerLedger(pool, from, to),
overrideRepairLedger != null ? Promise.resolve(null) : fetchRepairLedger(pool, from, to),
overrideRdLedger != null ? Promise.resolve(null) : fetchRdLedger(pool, from, to),
]);
const boilerLedger = overrideBoilerLedger != null ? parseFloat(overrideBoilerLedger) : liveBoilerLedger;
const powerLedger = overridePowerLedger != null ? parseFloat(overridePowerLedger) : livePowerLedger;
const repairLedger = overrideRepairLedger != null ? parseFloat(overrideRepairLedger) : liveRepairLedger;
const rdLedger = overrideRdLedger != null ? parseFloat(overrideRdLedger) : liveRdLedger;
const plEmpPool = overrideEmpPool != null ? parseFloat(overrideEmpPool) : liveEmpPool;
const salesMap = {};
sales.forEach(s => { salesMap[s.ItemCode] = parseFloat(s.SalesQty) || 0; });
const bomMap = {};
bom.forEach(b => { bomMap[b.ItemCode] = parseFloat(b.BOMPrice) || 0; });
// Salary module: supports comma-separated multiple periods
let salBB = 0, salCAPD = 0, salQA = 0, salQC = 0, salBoiler = 0, salRD = 0, grandTotalSalary = 0;
if (salaryPeriod) {
try {
const ss = require('../services/salaryStore');
const periods = salaryPeriod.split(',').map(p => p.trim()).filter(Boolean);
for (const sp of periods) {
const summary = await ss.getSummary(sp);
// Case-insensitive lookup for each department
const dept = k => {
const entry = Object.entries(summary.total)
.find(([d]) => d.toLowerCase() === k);
return entry ? parseFloat(entry[1] || 0) : 0;
};
salBB += dept('blood bag');
salCAPD += dept('capd');
salQA += dept('qa');
salQC += dept('qc');
salBoiler += dept('boiler');
salRD += dept('r&d');
grandTotalSalary += Object.values(summary.total)
.reduce((s, v) => s + (parseFloat(v) || 0), 0);
}
} catch (_) {}
}
// Boiler: ledger (5110010010 + provision) + BOILER dept salary from salary module
const boilerSalary = salBoiler;
const totalBoiler = boilerLedger + boilerSalary;
// Power: GENSET (5110010008) + ELECTRICITY (5110010009) — no salary component
const totalPower = powerLedger;
// R&M: all Repair & Maintenance accounts — no salary component
const totalRepair = repairLedger;
// R&D: ledger (5240040018-21 + provisions) + R&D dept salary
const rdSalary = salRD;
const totalRd = rdLedger + rdSalary;
const bbItems = production.filter(p => BB_GROUPS.includes(p.ItemGroup));
const capdItems = production.filter(p => CAPD_GROUPS.includes(p.ItemGroup));
// RM Consumption per item = Production Qty × BOM Price × 115%
const rmOf = p =>
parseFloat(((parseFloat(p.ProductionQty) || 0) * (bomMap[p.ItemCode] || 0) * 1.15).toFixed(2));
// Section RM totals
const bbRM = bbItems.reduce((s, p) => s + rmOf(p), 0);
const capdRM = capdItems.reduce((s, p) => s + rmOf(p), 0);
const totalRM = bbRM + capdRM; // QA & QC use combined RM as base
// ── Distribute Power pool into other cost heads ───────────────────────────
const pctVal = v => Math.min(100, Math.max(0, parseFloat(v) || 0)) / 100;
const distPowerEmp = totalPower * pctVal(empPowerPct);
const distPowerQa = totalPower * pctVal(qaPowerPct);
const distPowerQc = totalPower * pctVal(qcPowerPct);
const distPowerBoiler = totalPower * pctVal(boilerPowerPct);
const distPowerRepair = totalPower * pctVal(repairPowerPct);
const distPowerRd = totalPower * pctVal(rdPowerPct);
const distPowerTotal = distPowerEmp + distPowerQa + distPowerQc + distPowerBoiler + distPowerRepair + distPowerRd;
const standalonePower = Math.max(0, totalPower - distPowerTotal);
// Adjusted pools: base + absorbed power share
const adjEmpPool = plEmpPool + distPowerEmp;
const adjQaPool = salQA + distPowerQa;
const adjQcPool = salQC + distPowerQc;
const adjBoilerPool = totalBoiler + distPowerBoiler;
const adjRepairPool = totalRepair + distPowerRepair;
const adjRdPool = totalRd + distPowerRd;
// ── Section pool allocation ────────────────────────────────────────────────
// Employee: custom % override OR salary-dept-ratio (BB/CAPD dept salary)
let bbEmpPool, capdEmpPool;
if (bbEmpPct != null) {
[bbEmpPool, capdEmpPool] = splitPool(adjEmpPool, bbEmpPct, bbRM, totalRM);
} else {
bbEmpPool = (salBB > 0 && grandTotalSalary > 0) ? adjEmpPool * salBB / grandTotalSalary : adjEmpPool;
capdEmpPool = (salCAPD > 0 && grandTotalSalary > 0) ? adjEmpPool * salCAPD / grandTotalSalary : 0;
}
// QA, QC, Boiler, Power: custom % override OR RM-proportional (auto)
const [bbQaPool, capdQaPool] = splitPool(adjQaPool, bbQaPct, bbRM, totalRM);
const [bbQcPool, capdQcPool] = splitPool(adjQcPool, bbQcPct, bbRM, totalRM);
const [bbBoilerPool, capdBoilerPool] = splitPool(adjBoilerPool, bbBoilerPct, bbRM, totalRM);
const [bbPowerPool, capdPowerPool] = splitPool(standalonePower,bbPowerPct, bbRM, totalRM);
const [bbRepairPool, capdRepairPool] = splitPool(adjRepairPool, bbRepairPct, bbRM, totalRM);
const [bbRdPool, capdRdPool] = splitPool(adjRdPool, bbRdPct, bbRM, totalRM);
// ── Build rows: per-item cost = sectionPool × itemRM / sectionRM ──────────
const buildRows = (items, sectionRM, empPool, qaPool, qcPool, boilerPool, powerPool, repairPool, rdPool) => items.map(p => {
const prodQty = parseFloat(p.ProductionQty) || 0;
const salesQty = salesMap[p.ItemCode] || 0;
const rmCost = rmOf(p);
const calc = pool => sectionRM > 0 ? parseFloat((pool * rmCost / sectionRM).toFixed(2)) : 0;
return {
itemCode: p.ItemCode,
itemDescription: p.ItemDescription,
itemGroup: p.ItemGroup,
productionQty: prodQty,
salesQty,
rmConsumption: rmCost,
employeeCost: calc(empPool),
qaCost: calc(qaPool),
qcCost: calc(qcPool),
boilerCost: calc(boilerPool),
powerCost: calc(powerPool),
repairCost: calc(repairPool),
rdCost: calc(rdPool),
};
});
const bloodbag = buildRows(bbItems, bbRM, bbEmpPool, bbQaPool, bbQcPool, bbBoilerPool, bbPowerPool, bbRepairPool, bbRdPool);
const capd = buildRows(capdItems, capdRM, capdEmpPool, capdQaPool, capdQcPool, capdBoilerPool, capdPowerPool, capdRepairPool, capdRdPool);
// Auto % for reference (returned so frontend can show baseline)
const rmBbPct = totalRM > 0 ? bbRM / totalRM * 100 : 0;
const empBbPct = grandTotalSalary > 0 ? salBB / grandTotalSalary * 100 : (bbRM > 0 ? rmBbPct : 0);
const pct = (part, total) => total > 0 ? part / total * 100 : 0;
const allocation = {
emp: { bbPool: bbEmpPool, capdPool: capdEmpPool, bbPct: pct(bbEmpPool, adjEmpPool), autoPct: empBbPct, powerAbsorbed: distPowerEmp },
qa: { bbPool: bbQaPool, capdPool: capdQaPool, bbPct: pct(bbQaPool, adjQaPool), autoPct: rmBbPct, powerAbsorbed: distPowerQa },
qc: { bbPool: bbQcPool, capdPool: capdQcPool, bbPct: pct(bbQcPool, adjQcPool), autoPct: rmBbPct, powerAbsorbed: distPowerQc },
boiler: { bbPool: bbBoilerPool, capdPool: capdBoilerPool, bbPct: pct(bbBoilerPool, adjBoilerPool), autoPct: rmBbPct, powerAbsorbed: distPowerBoiler },
power: { bbPool: bbPowerPool, capdPool: capdPowerPool, bbPct: pct(bbPowerPool, standalonePower), autoPct: rmBbPct, powerAbsorbed: 0 },
repair: { bbPool: bbRepairPool, capdPool: capdRepairPool, bbPct: pct(bbRepairPool, adjRepairPool), autoPct: rmBbPct, powerAbsorbed: distPowerRepair },
rd: { bbPool: bbRdPool, capdPool: capdRdPool, bbPct: pct(bbRdPool, adjRdPool), autoPct: rmBbPct, powerAbsorbed: distPowerRd },
};
res.json({
success: true,
data: { bloodbag, capd },
boilerInfo: { ledger: boilerLedger, salary: boilerSalary, total: totalBoiler },
powerInfo: { total: totalPower },
repairInfo: { total: totalRepair },
rdInfo: { ledger: rdLedger, salary: rdSalary, total: totalRd },
allocation,
});
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
// ── Salary periods (for the dropdown) ──────────────────────────────────────
router.get('/salary-periods', verifyToken, async (req, res) => {
try {
const periods = await require('../services/salaryStore').getPeriods();
res.json({ success: true, data: periods });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
module.exports = router;