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

405 lines
19 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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;