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

352 lines
16 KiB
JavaScript
Raw 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';
// bs_config (this store's own config table) lives on the app's own database.
// fetchGroupBalance/fetchReservesSurplus/etc. below all receive their pool
// as a parameter from the caller (routes/balanceSheet.js, which imports the
// SAP pool directly for its OACT/JDT1 queries), so they're unaffected here.
const { getPool } = require('./appSqlPool');
// Balance sheet group structure (fixed, not user-configurable)
const BS_ITEMS = [
// key, label, section (L=Liabilities, A=Assets), subsection, source
{ key:'share_capital', label:'Share Capital', sec:'L', sub:'SF', src:'fixed' },
{ key:'reserves_surplus', label:'Reserves & Surplus', sec:'L', sub:'SF', src:'reserves_formula' },
{ key:'profit_loss', label:'Profit & Loss', sec:'L', sub:'SF', src:'pl_net' },
{ key:'lt_borrowings', label:'Long-term Borrowings', sec:'L', sub:'NCL', src:'ledger' },
{ key:'other_lt_liab', label:'Other Long-term Liabilities', sec:'L', sub:'NCL', src:'ledger' },
{ key:'lt_provisions', label:'Long-term Provisions', sec:'L', sub:'NCL', src:'ledger' },
{ key:'st_borrowings', label:'Short-term Borrowings', sec:'L', sub:'CL', src:'ledger' },
{ key:'trade_payables', label:'Trade Payables', sec:'L', sub:'CL', src:'ledger' },
{ key:'other_current_liab', label:'Other Current Liabilities', sec:'L', sub:'CL', src:'ledger' },
{ key:'st_provisions', label:'Short-term Provisions', sec:'L', sub:'CL', src:'provisions' },
{ key:'fixed_assets', label:'Fixed Assets (Tangible & Intangible)', sec:'A', sub:'NCA', src:'ledger' },
{ key:'non_current_invest', label:'Non-current Investments', sec:'A', sub:'NCA', src:'ledger' },
{ key:'lt_loans_advances', label:'Long-term Loans & Advances', sec:'A', sub:'NCA', src:'ledger' },
{ key:'other_non_current', label:'Other Non-current Assets', sec:'A', sub:'NCA', src:'ledger' },
{ key:'current_invest', label:'Current Investments', sec:'A', sub:'CA', src:'ledger' },
{ key:'inventories', label:'Inventories', sec:'A', sub:'CA', src:'closing_stock' },
{ key:'trade_receivables', label:'Trade Receivables', sec:'A', sub:'CA', src:'ledger' },
{ key:'cash_equivalents', label:'Cash and Cash Equivalents', sec:'A', sub:'CA', src:'ledger' },
{ key:'st_loans_advances', label:'Short-term Loans & Advances', sec:'A', sub:'CA', src:'ledger' },
{ key:'other_current_assets', label:'Other Current Assets', sec:'A', sub:'CA', src:'ledger' },
];
// Default account codes per group
// Default opening account codes for reserves_surplus formula
const DEFAULT_OPENING_ACCOUNTS = {
reserves_surplus: ['5100001001'],
};
const DEFAULT_ACCOUNTS = {
reserves_surplus: ['2120020001'],
trade_payables: ['2140000000'],
other_current_liab: ['2200000000'],
st_provisions: ['2210000000'],
fixed_assets: ['1100000000'],
trade_receivables: ['1250000000'],
cash_equivalents: ['1220000000'],
st_loans_advances: ['1260000000'],
other_current_assets: ['1280000000','1290000000','1330000000'],
};
const DEFAULT_FIXED = { share_capital: 100 }; // in Lakh
async function bootstrap() {
const pool = await getPool();
await pool.request().query(`
IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='bs_config' AND xtype='U')
CREATE TABLE bs_config (
group_key NVARCHAR(50) NOT NULL PRIMARY KEY,
acct_codes NVARCHAR(MAX) NOT NULL DEFAULT '[]',
opening_acct_codes NVARCHAR(MAX) NOT NULL DEFAULT '[]',
fixed_value DECIMAL(18,4) NULL,
notes NVARCHAR(500) NULL,
updated_at DATETIME DEFAULT GETDATE()
)
`);
// Add opening_acct_codes column if upgrading from older schema
await pool.request().query(`
IF NOT EXISTS (
SELECT 1 FROM sys.columns
WHERE name='opening_acct_codes' AND object_id=OBJECT_ID('bs_config')
)
ALTER TABLE bs_config ADD opening_acct_codes NVARCHAR(MAX) NOT NULL DEFAULT '[]'
`);
// Seed defaults if empty
const check = await pool.request().query(`SELECT COUNT(*) AS cnt FROM bs_config`);
if (check.recordset[0].cnt === 0) {
for (const item of BS_ITEMS) {
const codes = JSON.stringify(DEFAULT_ACCOUNTS[item.key] || []);
const ocodes = JSON.stringify(DEFAULT_OPENING_ACCOUNTS[item.key] || []);
const fv = DEFAULT_FIXED[item.key] != null ? DEFAULT_FIXED[item.key] : null;
await pool.request()
.input('k', item.key).input('c', codes).input('o', ocodes).input('f', fv)
.query(`INSERT INTO bs_config (group_key,acct_codes,opening_acct_codes,fixed_value) VALUES (@k,@c,@o,@f)`);
}
} else {
// Migration: fill in opening_acct_codes defaults for items that still have '[]'
for (const [key, defaults] of Object.entries(DEFAULT_OPENING_ACCOUNTS)) {
await pool.request()
.input('k', key)
.input('o', JSON.stringify(defaults))
.query(`
UPDATE bs_config
SET opening_acct_codes = @o, updated_at = GETDATE()
WHERE group_key = @k
AND (opening_acct_codes IS NULL OR opening_acct_codes = '[]')
`);
}
// Migration: fill in main acct_codes defaults if still '[]'
for (const [key, defaults] of Object.entries(DEFAULT_ACCOUNTS)) {
if (!defaults.length) continue;
await pool.request()
.input('k', key)
.input('c', JSON.stringify(defaults))
.query(`
UPDATE bs_config
SET acct_codes = @c, updated_at = GETDATE()
WHERE group_key = @k
AND (acct_codes IS NULL OR acct_codes = '[]')
`);
}
}
}
async function getConfig() {
const pool = await getPool();
const r = await pool.request().query(`SELECT group_key,acct_codes,opening_acct_codes,fixed_value,notes FROM bs_config`);
const map = {};
r.recordset.forEach(row => {
map[row.group_key] = {
acct_codes: JSON.parse(row.acct_codes || '[]'),
opening_acct_codes: JSON.parse(row.opening_acct_codes || '[]'),
fixed_value: row.fixed_value != null ? parseFloat(row.fixed_value) : null,
notes: row.notes || '',
};
});
return map;
}
async function saveConfig(groupKey, acctCodes, openingAcctCodes, fixedValue, notes) {
const pool = await getPool();
const codes = JSON.stringify(Array.isArray(acctCodes) ? acctCodes : []);
const ocodes = JSON.stringify(Array.isArray(openingAcctCodes) ? openingAcctCodes : []);
const fv = fixedValue != null && fixedValue !== '' ? parseFloat(fixedValue) : null;
await pool.request()
.input('k', groupKey).input('c', codes).input('o', ocodes).input('f', fv).input('n', notes || null)
.query(`
MERGE bs_config AS T
USING (SELECT @k AS group_key) AS S ON T.group_key = S.group_key
WHEN MATCHED THEN UPDATE SET acct_codes=@c, opening_acct_codes=@o, fixed_value=@f, notes=@n, updated_at=GETDATE()
WHEN NOT MATCHED THEN INSERT (group_key,acct_codes,opening_acct_codes,fixed_value,notes) VALUES (@k,@c,@o,@f,@n);
`);
}
// Derive a 4-char prefix from a 10-digit parent account code
// e.g. '2140000000' → trim trailing zeros '214' (length<4) → fallback to slice(0,4) = '2140'
function acctPrefix(code) {
const trimmed = String(code).replace(/0+$/, '');
return trimmed.length >= 4 ? trimmed : String(code).slice(0, 4);
}
// Fetch D-C balance for OACT accounts matching any configured prefix.
// from = period start (period balance, matching trial balance) OR null (cumulative up to asOf).
async function fetchGroupBalance(pool, acctCodes, from, asOf) {
if (!acctCodes || !acctCodes.length) return 0;
const req = pool.request().input('asOf', asOf);
if (from) req.input('from', from);
const conditions = acctCodes.map((c, i) => {
req.input(`p${i}`, acctPrefix(c) + '%');
return `T0.[AcctCode] LIKE @p${i}`;
}).join(' OR ');
const dateFilter = from
? `T1.[RefDate] BETWEEN @from AND @asOf`
: `T1.[RefDate] <= @asOf`;
const r = await req.query(`
SELECT ISNULL(SUM(
CASE WHEN ${dateFilter}
THEN ISNULL(T1.[Debit],0) - ISNULL(T1.[Credit],0)
ELSE 0 END
), 0) AS balance
FROM OACT T0
LEFT JOIN JDT1 T1 ON T0.[AcctCode] = T1.[Account]
WHERE ${conditions}
`);
return parseFloat(r.recordset[0]?.balance || 0);
}
// Compute netTax using EXACT same formula as Monthly Accounts P&L Summary frontend.
// stockDetail: { open: { conv }, close: { conv } } — matches renderSummary's convChange.
function calcNetTaxFromAccounts(rows, openingStock, closingStock, stockDetail) {
const S = { Revenue:0, Purchase:0, Employee:0, Factory:0, Admin:0, SND:0, Finance:0, OtherIncome:0 };
rows.forEach(r => {
const sheet = r.sheet || '';
if (S[sheet] === undefined) return;
const prov = r.override_val != null ? Number(r.override_val) : (r.provision != null ? Number(r.provision) : 0);
const net = sheet === 'Revenue'
? (Number(r.period_credit)||0) - (Number(r.period_debit)||0) + prov
: (Number(r.period_debit)||0) - (Number(r.period_credit)||0) + prov;
S[sheet] += net;
});
const matCost = (Number(openingStock)||0) + S.Purchase - (Number(closingStock)||0);
// convChange mirrors renderSummary: openConv - closeConv (change in conversion/WIP cost)
const openConv = Number(stockDetail?.open?.conv) || 0;
const closeConv = Number(stockDetail?.close?.conv) || 0;
const convChange = openConv - closeConv;
const grossP = S.Revenue - (matCost + convChange);
const totalExp = S.Employee + S.Factory + S.Admin + S.SND + S.Finance;
const netOps = grossP - totalExp;
const oiT = -S.OtherIncome;
return netOps + oiT;
}
// Fetch net profit from snapshot: parses the saved JSON using the same formula.
// `pool` (kept for call-site compatibility) is the SAP pool the caller
// already has for its other queries — pl_snapshots itself now lives on the
// app DB, so this reads it via costingStore.getSnapshot() instead.
async function fetchNetProfitFromSnapshot(pool, snapshotId) {
try {
const cs = require('./costingStore');
const snap = await cs.getSnapshot(parseInt(snapshotId));
if (!snap) return 0;
const parsed = JSON.parse(snap.data_json);
const accounts = parsed.accounts || parsed;
const openingStock = parsed.opening_stock != null ? parsed.opening_stock : 0;
const closingStock = parsed.closing_stock != null ? parsed.closing_stock : 0;
const stockDetail = parsed.stock_detail || null;
return calcNetTaxFromAccounts(accounts, openingStock, closingStock, stockDetail);
} catch(e) {
console.error('[fetchNetProfitFromSnapshot]', e.message);
return 0;
}
}
// Fetch P&L net profit from live database using the same formula as Monthly Accounts
async function fetchNetProfit(pool, from, to) {
try {
const cs = require('./costingStore');
const [tb, mapping, overrides] = await Promise.all([
cs.fetchTrialBalance(from, to),
cs.getMapping(),
cs.getOverrides(from, to),
]);
const mapByCode = {}; mapping.forEach(m => { mapByCode[m.acct_code] = m; });
const ovByCode = {}; overrides.forEach(o => { ovByCode[o.acct_code] = o; });
// Build account rows in the same format as the snapshot/frontend
const rows = tb.map(row => ({
sheet: mapByCode[row.acct_code]?.sheet ?? (row.group_mask === 4 ? 'Revenue' : 'Other'),
period_debit: Number(row.period_debit) || 0,
period_credit: Number(row.period_credit) || 0,
override_val: ovByCode[row.acct_code]?.override_val ?? null,
}));
const stockOpen = Number(ovByCode['__STOCK_OPEN__']?.override_val) || 0;
const stockClose = Number(ovByCode['__STOCK_CLOSE__']?.override_val) || 0;
const stockDetail = {
open: { conv: Number(ovByCode['__OPEN_CONV__']?.override_val) || 0 },
close: { conv: Number(ovByCode['__CLOSE_CONV__']?.override_val) || 0 },
};
return calcNetTaxFromAccounts(rows, stockOpen, stockClose, stockDetail);
} catch(_) { return 0; }
}
// Return date string one day before the given date (for opening balance query)
function dayBefore(dateStr) {
const d = new Date(dateStr + 'T00:00:00Z');
d.setUTCDate(d.getUTCDate() - 1);
return d.toISOString().slice(0, 10);
}
// Gross opening stock = RM + WIP + FG + Trading (no conv deduction), falls back to __STOCK_OPEN__
function getGrossOpeningStock(overrides) {
const get = key => parseFloat(overrides.find(o => o.acct_code === key)?.override_val || 0) || 0;
const gross = get('__OPEN_RM__') + get('__OPEN_WIP__') + get('__OPEN_FG__') + get('__OPEN_TRADE__');
return gross > 0 ? gross : get('__STOCK_OPEN__');
}
// ── Reserves & Surplus formula:
// Total Opening = opening_balance(5100001001) − gross opening stock
// Reserves&Surplus = −(balance(2120020001, asOf) + Total Opening)
async function fetchReservesSurplus(pool, mainAcctCodes, openingAcctCodes, from, to, asOf) {
try {
const cs = require('./costingStore');
// 1. Main account cumulative balance up to BS date
const mainBalance = await fetchGroupBalance(pool, mainAcctCodes, null, asOf);
// 2. Opening account cumulative balance strictly before the period start
const openingBalance = openingAcctCodes.length
? await fetchGroupBalance(pool, openingAcctCodes, null, dayBefore(from))
: 0;
// 3. Gross opening stock: RM + WIP + FG + Trading (no conv deduction)
const overrides = await cs.getOverrides(from, to);
const openingStock = getGrossOpeningStock(overrides);
// Total Opening = opening_balance(5100001001) − gross opening stock
const totalOpening = openingBalance - openingStock;
// Reserves & Surplus = −(mainBalance + Total Opening)
return -(mainBalance + totalOpening);
} catch(e) {
console.error('[fetchReservesSurplus]', e.message);
return 0;
}
}
// Same as above but returns intermediate values for debugging
async function fetchReservesSurplusDebug(pool, mainAcctCodes, openingAcctCodes, from, to, asOf) {
const cs = require('./costingStore');
const mainBalance = await fetchGroupBalance(pool, mainAcctCodes, null, asOf);
const openingBalance = openingAcctCodes.length
? await fetchGroupBalance(pool, openingAcctCodes, null, dayBefore(from))
: 0;
const overrides = await cs.getOverrides(from, to);
const openingStock = getGrossOpeningStock(overrides);
const totalOpening = openingBalance - openingStock;
const result = -(mainBalance + totalOpening);
return {
result,
mainAcctCodes,
openingAcctCodes,
mainBalance, // D-C for 2120020001 up to asOf
openingBalance, // D-C for 5100001001 up to dayBefore(from)
openingStock, // gross opening stock (RM+WIP+FG+Trading)
totalOpening, // openingBalance - openingStock
formula: `-(${mainBalance} + (${openingBalance} - ${openingStock})) = ${result}`,
dayBeforeFrom: dayBefore(from),
};
}
// Fetch closing stock from pl_overrides — gross total (RM + WIP + FG + Trading, no conv deduction)
async function fetchClosingStock(pool, from, to) {
try {
const cs = require('./costingStore');
const overrides = await cs.getOverrides(from, to);
const get = key => parseFloat(overrides.find(o => o.acct_code === key)?.override_val || 0) || 0;
const rm = get('__CLOSE_RM__');
const wip = get('__CLOSE_WIP__');
const fg = get('__CLOSE_FG__');
const trade = get('__CLOSE_TRADE__');
const gross = rm + wip + fg + trade;
// Fall back to __STOCK_CLOSE__ if individual components not yet saved
if (gross === 0) return get('__STOCK_CLOSE__');
return gross;
} catch(_) { return 0; }
}
// Fetch total provisions (sum of all non-stock pl_overrides)
async function fetchTotalProvisions(pool, from, to) {
try {
const cs = require('./costingStore');
const overrides = await cs.getOverrides(from, to);
return overrides
.filter(o => !o.acct_code.startsWith('__'))
.reduce((s, o) => s + (parseFloat(o.override_val) || 0), 0);
} catch(_) { return 0; }
}
module.exports = { bootstrap, BS_ITEMS, getConfig, saveConfig, fetchGroupBalance, fetchReservesSurplus, fetchReservesSurplusDebug, fetchNetProfit, fetchNetProfitFromSnapshot, fetchClosingStock, fetchTotalProvisions };