'use strict'; const express = require('express'); const router = express.Router(); const axios = require('axios'); const FormData = require('form-data'); const ExcelJS = require('exceljs'); const { verifyToken } = require('../middleware/auth'); const https = require('https'); const httpsAgent = new https.Agent({ rejectUnauthorized: false }); // ── helpers ──────────────────────────────────────────────────────────────── function safeDate(d) { if (!/^\d{4}-\d{2}-\d{2}$/.test(d)) throw new Error(`Invalid date: ${d}`); return d; } // HSN/DOC SQL uses CONVERT(VARCHAR(6),date,112) = YYYYMM; TCS wants MMYYYY function toMMYYYY(fp) { const s = String(fp || '').replace(/\D/g, ''); if (s.length === 6) { const yr = parseInt(s.slice(0, 4), 10); if (yr >= 2000 && yr <= 2099) return s.slice(4, 6) + s.slice(0, 4); } return fp; } function r2(n) { const num = parseFloat(String(n ?? '0')); if (isNaN(num)) return 0; // Use Number() on toFixed result to strip trailing zeros and prevent scientific notation return parseFloat(num.toFixed(2)); } // Map SAP unit codes → valid GST UQC codes (TCS validates against GSTN list) const VALID_UQC = new Set([ 'BAG','BAL','BDL','BKL','BOU','BOX','BTL','BUN','CAN','CBM','CCM','CMS','CTN', 'DOZ','DRM','GGK','GMS','GRS','GYD','KGS','KLR','KME','KNO','KWT','LTR','MBE', 'MLT','MTR','MTS','NOS','OTH','PAC','PCS','PRS','QTL','ROL','SET','SQC','SQF', 'SQM','TBS','TGM','TON','TUB','UGS','UNT','YDS', ]); const UQC_MAP = { KG:'KGS', GM:'GMS', GRM:'GMS', LT:'LTR', L:'LTR', LITRE:'LTR', MT:'MTR', M:'MTR', METRE:'MTR', QTL:'QTL', TON:'TON', TNE:'TON', PC:'PCS', PCS:'PCS', PEC:'PCS', EA:'NOS', EACH:'NOS', NO:'NOS', NOS:'NOS', ROLL:'ROL', ROL:'ROL', ROLE:'ROL', ROOL:'ROL', PKT:'PAC', PACK:'PAC', PAC:'PAC', BTL:'BTL', BOT:'BTL', TUB:'TUB', CAN:'CAN', CTN:'CTN', BAG:'BAG', BDL:'BDL', BOX:'BOX', SET:'SET', SQM:'SQM', SQF:'SQF', SQC:'SQC', CBM:'CBM', MTR:'MTR', MTS:'MTS', KGS:'KGS', LTR:'LTR', GMS:'GMS', }; function validUQC(code) { const up = String(code || '').toUpperCase().trim(); const mapped = UQC_MAP[up] || up; return VALID_UQC.has(mapped) ? mapped : 'OTH'; } // ── SQL builders ────────────────────────────────────────────────────────── // Deemed Export (DE) sales ledgers — invoices posting to any of these GL // accounts are reported as B2B with invoice_type 'DE' and kept out of EXP. // Updated 05-10-2026: added 4110202005, 4110202007 (matches EXCL_EI in reports.js) const DE_LEDGERS = `'4110202001','4110202003','4110202005','4110202007'`; function sqlB2B(from, to) { return ` WITH TaxData AS ( SELECT T_DOC1.DocEntry, T_DOC1.LineNum, CAST(MAX(T_TAX1.BaseSum) AS DECIMAL(11,2)) AS ItemTaxableValue, CASE WHEN MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END)=0 THEN MAX(CASE WHEN T_OSTT.AbsId=-100 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) + MAX(CASE WHEN T_OSTT.AbsId IN(-110,-150) AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) ELSE MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) END AS TaxRate, CAST(SUM(CASE WHEN T_OSTT.AbsId=-100 THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)) AS CGST_Amt, CAST(SUM(CASE WHEN T_OSTT.AbsId IN(-110,-150) THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)) AS SGST_Amt, CAST(SUM(CASE WHEN T_OSTT.AbsId=-120 THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)) AS IGST_Amt, MAX(CASE WHEN T_TAX1.RvsChrgPrc>0 THEN 'Y' ELSE 'N' END) AS rchrg FROM TAX1 T_TAX1 INNER JOIN OSTA T1 ON T_TAX1.StaCode=T1.Code INNER JOIN OSTT T_OSTT ON T1.Type=T_OSTT.AbsId INNER JOIN OTAX T_OTAX ON T_OTAX.AbsEntry=T_TAX1.AbsEntry INNER JOIN OINV T_ODOC ON T_OTAX.SrcObjType=13 AND T_OTAX.SrcObjAbs=T_ODOC.DocEntry INNER JOIN INV1 T_DOC1 ON T_TAX1.SrcArrType=12 AND T_TAX1.SrcLineNum=T_DOC1.LineNum AND T_DOC1.DocEntry=T_ODOC.DocEntry WHERE T_ODOC.GSTTranTyp='GA' AND T_ODOC.DocStatus<>'C' AND T_ODOC.InvntSttus<>'C' GROUP BY T_DOC1.DocEntry, T_DOC1.LineNum UNION ALL SELECT T_DOC1.DocEntry, T_TAX1.SrcLineNum, 0, CASE WHEN MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END)=0 THEN MAX(CASE WHEN T_OSTT.AbsId=-100 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) + MAX(CASE WHEN T_OSTT.AbsId IN(-110,-150) AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) ELSE MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) END, CAST(SUM(CASE WHEN T_OSTT.AbsId=-100 THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)), CAST(SUM(CASE WHEN T_OSTT.AbsId IN(-110,-150) THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)), CAST(SUM(CASE WHEN T_OSTT.AbsId=-120 THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)), MAX(CASE WHEN T_TAX1.RvsChrgPrc>0 THEN 'Y' ELSE 'N' END) FROM TAX1 T_TAX1 INNER JOIN OSTA T1 ON T_TAX1.StaCode=T1.Code INNER JOIN OSTT T_OSTT ON T1.Type=T_OSTT.AbsId INNER JOIN OTAX T_OTAX ON T_OTAX.AbsEntry=T_TAX1.AbsEntry INNER JOIN OINV T_ODOC ON T_OTAX.SrcObjType=13 AND T_OTAX.SrcObjAbs=T_ODOC.DocEntry INNER JOIN INV2 T_DOC2 ON T_DOC2.DocEntry=T_ODOC.DocEntry AND T_DOC2.LineNum=T_TAX1.SrcLineNum AND T_TAX1.SrcGrpNum=T_DOC2.GroupNum INNER JOIN INV1 T_DOC1 ON T_DOC1.DocEntry=T_DOC2.DocEntry AND T_DOC1.LineNum=T_DOC2.LineNum WHERE T_ODOC.GSTTranTyp='GA' GROUP BY T_DOC1.DocEntry, T_TAX1.SrcLineNum, T_TAX1.SrcGrpNum UNION ALL SELECT T_DOC3.DocEntry, T_DOC3.LineNum, 0, CASE WHEN MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END)=0 THEN MAX(CASE WHEN T_OSTT.AbsId=-100 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) + MAX(CASE WHEN T_OSTT.AbsId IN(-110,-150) AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) ELSE MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) END, CAST(SUM(CASE WHEN T_OSTT.AbsId=-100 THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)), CAST(SUM(CASE WHEN T_OSTT.AbsId IN(-110,-150) THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)), CAST(SUM(CASE WHEN T_OSTT.AbsId=-120 THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)), MAX(CASE WHEN T_TAX1.RvsChrgPrc>0 THEN 'Y' ELSE 'N' END) FROM TAX1 T_TAX1 INNER JOIN OSTA T1 ON T_TAX1.StaCode=T1.Code INNER JOIN OSTT T_OSTT ON T1.Type=T_OSTT.AbsId INNER JOIN OTAX T_OTAX ON T_OTAX.AbsEntry=T_TAX1.AbsEntry INNER JOIN OINV T_ODOC ON T_OTAX.SrcObjType=13 AND T_OTAX.SrcObjAbs=T_ODOC.DocEntry INNER JOIN INV3 T_DOC3 ON T_DOC3.DocEntry=T_ODOC.DocEntry AND T_DOC3.LineNum=T_TAX1.SrcLineNum WHERE T_ODOC.GSTTranTyp='GA' AND (T_TAX1.BaseSum>0 OR T_OSTT.NfTaxId=-60) GROUP BY T_DOC3.DocEntry, T_DOC3.LineNum ), ITS AS ( SELECT DocEntry, TaxRate, SUM(ItemTaxableValue) AS TotalTaxableValue, SUM(CGST_Amt) AS CGST_Amt, SUM(SGST_Amt) AS SGST_Amt, SUM(IGST_Amt) AS IGST_Amt, CASE WHEN MAX(rchrg)='Y' THEN 'Y' ELSE 'N' END AS ReverseCharge FROM TaxData GROUP BY DocEntry, TaxRate ) SELECT CASE WHEN EXISTS (SELECT 1 FROM INV1 T_DE WHERE T_DE.DocEntry=T_ODOC.DocEntry AND T_DE.AcctCode IN (${DE_LEDGERS})) THEN 'DE' WHEN T_DOC12.CountryB<>'IN' THEN 'EXP' WHEN T_ODOC.U_CustomerCategory='Indirect Export' THEN 'B2B' WHEN T_DOC12.BpGSTN IS NULL AND T_DOC12.CountryS='IN' THEN 'B2CS' ELSE 'B2B' END AS trans_type_code, RIGHT('0'+CAST(MONTH(T_ODOC.DocDate) AS VARCHAR(2)),2)+CAST(YEAR(T_ODOC.DocDate) AS VARCHAR(4)) AS fp, ROW_NUMBER() OVER(ORDER BY T_DOC12.DocEntry) AS ack_sr_no, T_DOC12.LocGSTN AS self_gstin, CASE WHEN T_ODOC.CANCELED='Y' THEN 'C' ELSE 'A' END AS resultflag, 'R' AS invoice_type, T_DOC12.BpGSTN AS cp_gstin, T_ODOC.CardName AS cp_gstin_name, CONCAT(T_NNM1.BeginStr, T_ODOC.DocNum) AS invoice_no, FORMAT(T_ODOC.DocDate,'dd-MM-yyyy') AS invoice_dt, T_ODOC.DocTotal AS invoice_val, ITS.TaxRate AS rate_of_tax, ITS.CGST_Amt AS cgst_amnt, ITS.SGST_Amt AS sgst_amnt, ITS.IGST_Amt AS igst_amnt, ITS.TotalTaxableValue AS taxable_val, CASE WHEN T_DOC12.BpGSTN IS NOT NULL THEN LEFT(T_DOC12.BpGSTN,2) ELSE T_OCST_S.GSTCode END AS pos, ITS.ReverseCharge AS rchrg FROM ITS INNER JOIN OINV T_ODOC ON ITS.DocEntry=T_ODOC.DocEntry INNER JOIN INV12 T_DOC12 ON T_DOC12.DocEntry=T_ODOC.DocEntry INNER JOIN NNM1 T_NNM1 ON T_NNM1.Series=T_ODOC.Series LEFT JOIN OCST T_OCST_S ON T_OCST_S.Code=T_DOC12.StateS WHERE T_ODOC.CANCELED='N' AND T_ODOC.DocDate>='${from}' AND T_ODOC.DocDate<='${to}' AND ( EXISTS (SELECT 1 FROM INV1 T_DE WHERE T_DE.DocEntry=T_ODOC.DocEntry AND T_DE.AcctCode IN (${DE_LEDGERS})) OR ( T_DOC12.CountryB='IN' AND (T_DOC12.BpGSTN IS NOT NULL OR T_ODOC.U_CustomerCategory='Indirect Export') AND T_OCST_S.GSTCode IS NOT NULL ) ) ORDER BY ITS.DocEntry, ITS.TaxRate`; } function sqlB2CL(from, to) { return ` WITH TaxData AS ( SELECT T_DOC1.DocEntry, T_DOC1.LineNum, CAST(MAX(T_TAX1.BaseSum) AS DECIMAL(11,2)) AS ItemTaxableValue, CASE WHEN MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END)=0 THEN MAX(CASE WHEN T_OSTT.AbsId=-100 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) + MAX(CASE WHEN T_OSTT.AbsId IN(-110,-150) AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) ELSE MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) END AS TaxRate, CAST(SUM(CASE WHEN T_OSTT.AbsId=-100 THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)) AS CGST_Amt, CAST(SUM(CASE WHEN T_OSTT.AbsId IN(-110,-150) THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)) AS SGST_Amt, CAST(SUM(CASE WHEN T_OSTT.AbsId=-120 THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)) AS IGST_Amt, MAX(CASE WHEN T_TAX1.RvsChrgPrc>0 THEN 'Y' ELSE 'N' END) AS rchrg FROM TAX1 T_TAX1 INNER JOIN OSTA T1 ON T_TAX1.StaCode=T1.Code INNER JOIN OSTT T_OSTT ON T1.Type=T_OSTT.AbsId INNER JOIN OTAX T_OTAX ON T_OTAX.AbsEntry=T_TAX1.AbsEntry INNER JOIN OINV T_ODOC ON T_OTAX.SrcObjType=13 AND T_OTAX.SrcObjAbs=T_ODOC.DocEntry INNER JOIN INV1 T_DOC1 ON T_TAX1.SrcArrType=12 AND T_TAX1.SrcLineNum=T_DOC1.LineNum AND T_DOC1.DocEntry=T_ODOC.DocEntry WHERE T_ODOC.GSTTranTyp='GA' AND T_ODOC.DocStatus<>'C' AND T_ODOC.InvntSttus<>'C' GROUP BY T_DOC1.DocEntry, T_DOC1.LineNum UNION ALL SELECT T_DOC1.DocEntry, T_TAX1.SrcLineNum, 0, CASE WHEN MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END)=0 THEN MAX(CASE WHEN T_OSTT.AbsId=-100 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) + MAX(CASE WHEN T_OSTT.AbsId IN(-110,-150) AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) ELSE MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) END, CAST(SUM(CASE WHEN T_OSTT.AbsId=-100 THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)), CAST(SUM(CASE WHEN T_OSTT.AbsId IN(-110,-150) THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)), CAST(SUM(CASE WHEN T_OSTT.AbsId=-120 THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)), MAX(CASE WHEN T_TAX1.RvsChrgPrc>0 THEN 'Y' ELSE 'N' END) FROM TAX1 T_TAX1 INNER JOIN OSTA T1 ON T_TAX1.StaCode=T1.Code INNER JOIN OSTT T_OSTT ON T1.Type=T_OSTT.AbsId INNER JOIN OTAX T_OTAX ON T_OTAX.AbsEntry=T_TAX1.AbsEntry INNER JOIN OINV T_ODOC ON T_OTAX.SrcObjType=13 AND T_OTAX.SrcObjAbs=T_ODOC.DocEntry INNER JOIN INV2 T_DOC2 ON T_DOC2.DocEntry=T_ODOC.DocEntry AND T_DOC2.LineNum=T_TAX1.SrcLineNum AND T_TAX1.SrcGrpNum=T_DOC2.GroupNum INNER JOIN INV1 T_DOC1 ON T_DOC1.DocEntry=T_DOC2.DocEntry AND T_DOC1.LineNum=T_DOC2.LineNum WHERE T_ODOC.GSTTranTyp='GA' GROUP BY T_DOC1.DocEntry, T_TAX1.SrcLineNum, T_TAX1.SrcGrpNum UNION ALL SELECT T_DOC3.DocEntry, T_DOC3.LineNum, 0, CASE WHEN MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END)=0 THEN MAX(CASE WHEN T_OSTT.AbsId=-100 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) + MAX(CASE WHEN T_OSTT.AbsId IN(-110,-150) AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) ELSE MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) END, CAST(SUM(CASE WHEN T_OSTT.AbsId=-100 THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)), CAST(SUM(CASE WHEN T_OSTT.AbsId IN(-110,-150) THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)), CAST(SUM(CASE WHEN T_OSTT.AbsId=-120 THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)), MAX(CASE WHEN T_TAX1.RvsChrgPrc>0 THEN 'Y' ELSE 'N' END) FROM TAX1 T_TAX1 INNER JOIN OSTA T1 ON T_TAX1.StaCode=T1.Code INNER JOIN OSTT T_OSTT ON T1.Type=T_OSTT.AbsId INNER JOIN OTAX T_OTAX ON T_OTAX.AbsEntry=T_TAX1.AbsEntry INNER JOIN OINV T_ODOC ON T_OTAX.SrcObjType=13 AND T_OTAX.SrcObjAbs=T_ODOC.DocEntry INNER JOIN INV3 T_DOC3 ON T_DOC3.DocEntry=T_ODOC.DocEntry AND T_DOC3.LineNum=T_TAX1.SrcLineNum WHERE T_ODOC.GSTTranTyp='GA' AND (T_TAX1.BaseSum>0 OR T_OSTT.NfTaxId=-60) GROUP BY T_DOC3.DocEntry, T_DOC3.LineNum ), ITS AS ( SELECT DocEntry, TaxRate, SUM(ItemTaxableValue) AS TotalTaxableValue, SUM(CGST_Amt) AS CGST_Amt, SUM(SGST_Amt) AS SGST_Amt, SUM(IGST_Amt) AS IGST_Amt, CASE WHEN MAX(rchrg)='Y' THEN 'Y' ELSE 'N' END AS ReverseCharge FROM TaxData GROUP BY DocEntry, TaxRate ) SELECT 'B2CL' AS trans_type_code, RIGHT('0'+CAST(MONTH(T_ODOC.DocDate) AS VARCHAR(2)),2)+CAST(YEAR(T_ODOC.DocDate) AS VARCHAR(4)) AS fp, ROW_NUMBER() OVER(ORDER BY T_DOC12.DocEntry) AS ack_sr_no, T_DOC12.LocGSTN AS self_gstin, CASE WHEN T_ODOC.CANCELED='Y' THEN 'C' ELSE 'A' END AS resultflag, 'R' AS invoice_type, CONCAT(T_NNM1.BeginStr, T_ODOC.DocNum) AS invoice_no, FORMAT(T_ODOC.DocDate,'dd-MM-yyyy') AS invoice_dt, T_ODOC.DocTotal AS invoice_val, ITS.TaxRate AS rate_of_tax, ITS.CGST_Amt AS cgst_amnt, ITS.SGST_Amt AS sgst_amnt, ITS.IGST_Amt AS igst_amnt, ITS.TotalTaxableValue AS taxable_val, T_OCST_S.GSTCode AS pos FROM ITS INNER JOIN OINV T_ODOC ON ITS.DocEntry=T_ODOC.DocEntry INNER JOIN INV12 T_DOC12 ON T_DOC12.DocEntry=T_ODOC.DocEntry INNER JOIN NNM1 T_NNM1 ON T_NNM1.Series=T_ODOC.Series LEFT JOIN OCST T_OCST_S ON T_OCST_S.Code=T_DOC12.StateS WHERE T_ODOC.CANCELED='N' AND T_ODOC.DocDate>='${from}' AND T_ODOC.DocDate<='${to}' AND T_DOC12.CountryB='IN' AND T_DOC12.BpGSTN IS NULL AND T_OCST_S.GSTCode IS NOT NULL AND T_ODOC.DocTotal > 100000 AND ITS.IGST_Amt > 0 ORDER BY ITS.DocEntry, ITS.TaxRate`; } function sqlB2CS(from, to) { return ` WITH TaxData AS ( SELECT T_DOC1.DocEntry, T_DOC1.LineNum, CAST(MAX(T_TAX1.BaseSum) AS DECIMAL(11,2)) AS ItemTaxableValue, CASE WHEN MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END)=0 THEN MAX(CASE WHEN T_OSTT.AbsId=-100 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) + MAX(CASE WHEN T_OSTT.AbsId IN(-110,-150) AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) ELSE MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) END AS TaxRate, CAST(SUM(CASE WHEN T_OSTT.AbsId=-100 THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)) AS CGST_Amt, CAST(SUM(CASE WHEN T_OSTT.AbsId IN(-110,-150) THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)) AS SGST_Amt, CAST(SUM(CASE WHEN T_OSTT.AbsId=-120 THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)) AS IGST_Amt, MAX(CASE WHEN T_TAX1.RvsChrgPrc>0 THEN 'Y' ELSE 'N' END) AS rchrg FROM TAX1 T_TAX1 INNER JOIN OSTA T1 ON T_TAX1.StaCode=T1.Code INNER JOIN OSTT T_OSTT ON T1.Type=T_OSTT.AbsId INNER JOIN OTAX T_OTAX ON T_OTAX.AbsEntry=T_TAX1.AbsEntry INNER JOIN OINV T_ODOC ON T_OTAX.SrcObjType=13 AND T_OTAX.SrcObjAbs=T_ODOC.DocEntry INNER JOIN INV1 T_DOC1 ON T_TAX1.SrcArrType=12 AND T_TAX1.SrcLineNum=T_DOC1.LineNum AND T_DOC1.DocEntry=T_ODOC.DocEntry WHERE T_ODOC.GSTTranTyp='GA' AND T_ODOC.DocStatus<>'C' AND T_ODOC.InvntSttus<>'C' GROUP BY T_DOC1.DocEntry, T_DOC1.LineNum UNION ALL SELECT T_DOC1.DocEntry, T_TAX1.SrcLineNum, 0, CASE WHEN MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END)=0 THEN MAX(CASE WHEN T_OSTT.AbsId=-100 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) + MAX(CASE WHEN T_OSTT.AbsId IN(-110,-150) AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) ELSE MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) END, CAST(SUM(CASE WHEN T_OSTT.AbsId=-100 THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)), CAST(SUM(CASE WHEN T_OSTT.AbsId IN(-110,-150) THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)), CAST(SUM(CASE WHEN T_OSTT.AbsId=-120 THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)), MAX(CASE WHEN T_TAX1.RvsChrgPrc>0 THEN 'Y' ELSE 'N' END) FROM TAX1 T_TAX1 INNER JOIN OSTA T1 ON T_TAX1.StaCode=T1.Code INNER JOIN OSTT T_OSTT ON T1.Type=T_OSTT.AbsId INNER JOIN OTAX T_OTAX ON T_OTAX.AbsEntry=T_TAX1.AbsEntry INNER JOIN OINV T_ODOC ON T_OTAX.SrcObjType=13 AND T_OTAX.SrcObjAbs=T_ODOC.DocEntry INNER JOIN INV2 T_DOC2 ON T_DOC2.DocEntry=T_ODOC.DocEntry AND T_DOC2.LineNum=T_TAX1.SrcLineNum AND T_TAX1.SrcGrpNum=T_DOC2.GroupNum INNER JOIN INV1 T_DOC1 ON T_DOC1.DocEntry=T_DOC2.DocEntry AND T_DOC1.LineNum=T_DOC2.LineNum WHERE T_ODOC.GSTTranTyp='GA' GROUP BY T_DOC1.DocEntry, T_TAX1.SrcLineNum, T_TAX1.SrcGrpNum UNION ALL SELECT T_DOC3.DocEntry, T_DOC3.LineNum, 0, CASE WHEN MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END)=0 THEN MAX(CASE WHEN T_OSTT.AbsId=-100 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) + MAX(CASE WHEN T_OSTT.AbsId IN(-110,-150) AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) ELSE MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) END, CAST(SUM(CASE WHEN T_OSTT.AbsId=-100 THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)), CAST(SUM(CASE WHEN T_OSTT.AbsId IN(-110,-150) THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)), CAST(SUM(CASE WHEN T_OSTT.AbsId=-120 THEN T_TAX1.VatSum ELSE 0 END) AS DECIMAL(13,2)), MAX(CASE WHEN T_TAX1.RvsChrgPrc>0 THEN 'Y' ELSE 'N' END) FROM TAX1 T_TAX1 INNER JOIN OSTA T1 ON T_TAX1.StaCode=T1.Code INNER JOIN OSTT T_OSTT ON T1.Type=T_OSTT.AbsId INNER JOIN OTAX T_OTAX ON T_OTAX.AbsEntry=T_TAX1.AbsEntry INNER JOIN OINV T_ODOC ON T_OTAX.SrcObjType=13 AND T_OTAX.SrcObjAbs=T_ODOC.DocEntry INNER JOIN INV3 T_DOC3 ON T_DOC3.DocEntry=T_ODOC.DocEntry AND T_DOC3.LineNum=T_TAX1.SrcLineNum WHERE T_ODOC.GSTTranTyp='GA' AND (T_TAX1.BaseSum>0 OR T_OSTT.NfTaxId=-60) GROUP BY T_DOC3.DocEntry, T_DOC3.LineNum ), ITS AS ( SELECT DocEntry, TaxRate, SUM(ItemTaxableValue) AS TotalTaxableValue, SUM(CGST_Amt) AS CGST_Amt, SUM(SGST_Amt) AS SGST_Amt, SUM(IGST_Amt) AS IGST_Amt, CASE WHEN MAX(rchrg)='Y' THEN 'Y' ELSE 'N' END AS ReverseCharge FROM TaxData GROUP BY DocEntry, TaxRate ) SELECT 'B2CS' AS trans_type_code, RIGHT('0'+CAST(MONTH(T_ODOC.DocDate) AS VARCHAR(2)),2)+CAST(YEAR(T_ODOC.DocDate) AS VARCHAR(4)) AS fp, ROW_NUMBER() OVER(ORDER BY T_DOC12.DocEntry) AS ack_sr_no, T_DOC12.LocGSTN AS self_gstin, CASE WHEN T_ODOC.CANCELED='Y' THEN 'C' ELSE 'A' END AS resultflag, 'R' AS invoice_type, CONCAT(T_NNM1.BeginStr, T_ODOC.DocNum) AS invoice_no, FORMAT(T_ODOC.DocDate,'dd-MM-yyyy') AS invoice_dt, T_ODOC.DocTotal AS invoice_val, ITS.TaxRate AS rate_of_tax, ITS.CGST_Amt AS cgst_amnt, ITS.SGST_Amt AS sgst_amnt, ITS.IGST_Amt AS igst_amnt, ITS.TotalTaxableValue AS taxable_val, T_OCST_S.GSTCode AS pos FROM ITS INNER JOIN OINV T_ODOC ON ITS.DocEntry=T_ODOC.DocEntry INNER JOIN INV12 T_DOC12 ON T_DOC12.DocEntry=T_ODOC.DocEntry INNER JOIN NNM1 T_NNM1 ON T_NNM1.Series=T_ODOC.Series LEFT JOIN OCST T_OCST_S ON T_OCST_S.Code=T_DOC12.StateS WHERE T_ODOC.CANCELED='N' AND T_ODOC.DocDate>='${from}' AND T_ODOC.DocDate<='${to}' AND T_DOC12.CountryB='IN' AND T_DOC12.BpGSTN IS NULL AND T_OCST_S.GSTCode IS NOT NULL AND (T_ODOC.DocTotal<=100000 OR (T_ODOC.DocTotal>100000 AND ITS.CGST_Amt>0)) ORDER BY ITS.DocEntry, ITS.TaxRate`; } // EXP — no invoice_type in SELECT (TCS throws NREQ error if present) function sqlExport(from, to) { return ` WITH TaxData AS ( SELECT T_DOC1.DocEntry, T_DOC1.LineNum, CAST(MAX(T_TAX1.BaseSum) AS DECIMAL(11,2)) AS ItemTaxableValue, CASE WHEN MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END)=0 THEN MAX(CASE WHEN T_OSTT.AbsId=-100 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) + MAX(CASE WHEN T_OSTT.AbsId IN(-110,-150) AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) ELSE MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) END AS TaxRate, MAX(CASE WHEN T_TAX1.RvsChrgPrc>0 THEN 'Y' ELSE 'N' END) AS rchrg FROM TAX1 T_TAX1 INNER JOIN OSTA T1 ON T_TAX1.StaCode=T1.Code INNER JOIN OSTT T_OSTT ON T1.Type=T_OSTT.AbsId INNER JOIN OTAX T_OTAX ON T_OTAX.AbsEntry=T_TAX1.AbsEntry INNER JOIN OINV T_ODOC ON T_OTAX.SrcObjType=13 AND T_OTAX.SrcObjAbs=T_ODOC.DocEntry INNER JOIN INV1 T_DOC1 ON T_TAX1.SrcArrType=12 AND T_TAX1.SrcLineNum=T_DOC1.LineNum AND T_DOC1.DocEntry=T_ODOC.DocEntry WHERE T_ODOC.GSTTranTyp='GA' AND T_ODOC.DocStatus<>'C' AND T_ODOC.InvntSttus<>'C' GROUP BY T_DOC1.DocEntry, T_DOC1.LineNum UNION ALL SELECT T_DOC1.DocEntry, T_TAX1.SrcLineNum, 0, CASE WHEN MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END)=0 THEN MAX(CASE WHEN T_OSTT.AbsId=-100 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) + MAX(CASE WHEN T_OSTT.AbsId IN(-110,-150) AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) ELSE MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) END, MAX(CASE WHEN T_TAX1.RvsChrgPrc>0 THEN 'Y' ELSE 'N' END) FROM TAX1 T_TAX1 INNER JOIN OSTA T1 ON T_TAX1.StaCode=T1.Code INNER JOIN OSTT T_OSTT ON T1.Type=T_OSTT.AbsId INNER JOIN OTAX T_OTAX ON T_OTAX.AbsEntry=T_TAX1.AbsEntry INNER JOIN OINV T_ODOC ON T_OTAX.SrcObjType=13 AND T_OTAX.SrcObjAbs=T_ODOC.DocEntry INNER JOIN INV2 T_DOC2 ON T_DOC2.DocEntry=T_ODOC.DocEntry AND T_DOC2.LineNum=T_TAX1.SrcLineNum AND T_TAX1.SrcGrpNum=T_DOC2.GroupNum INNER JOIN INV1 T_DOC1 ON T_DOC1.DocEntry=T_DOC2.DocEntry AND T_DOC1.LineNum=T_DOC2.LineNum WHERE T_ODOC.GSTTranTyp='GA' GROUP BY T_DOC1.DocEntry, T_TAX1.SrcLineNum, T_TAX1.SrcGrpNum UNION ALL SELECT T_DOC3.DocEntry, T_DOC3.LineNum, 0, CASE WHEN MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END)=0 THEN MAX(CASE WHEN T_OSTT.AbsId=-100 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) + MAX(CASE WHEN T_OSTT.AbsId IN(-110,-150) AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) ELSE MAX(CASE WHEN T_OSTT.AbsId=-120 AND T_ODOC.DutyStatus='Y' THEN T_TAX1.VatPercent ELSE 0 END) END, MAX(CASE WHEN T_TAX1.RvsChrgPrc>0 THEN 'Y' ELSE 'N' END) FROM TAX1 T_TAX1 INNER JOIN OSTA T1 ON T_TAX1.StaCode=T1.Code INNER JOIN OSTT T_OSTT ON T1.Type=T_OSTT.AbsId INNER JOIN OTAX T_OTAX ON T_OTAX.AbsEntry=T_TAX1.AbsEntry INNER JOIN OINV T_ODOC ON T_OTAX.SrcObjType=13 AND T_OTAX.SrcObjAbs=T_ODOC.DocEntry INNER JOIN INV3 T_DOC3 ON T_DOC3.DocEntry=T_ODOC.DocEntry AND T_DOC3.LineNum=T_TAX1.SrcLineNum WHERE T_ODOC.GSTTranTyp='GA' AND (T_TAX1.BaseSum>0 OR T_OSTT.NfTaxId=-60) GROUP BY T_DOC3.DocEntry, T_DOC3.LineNum ), ITS AS ( SELECT DocEntry, TaxRate, SUM(ItemTaxableValue) AS TotalTaxableValue, CASE WHEN MAX(rchrg)='Y' THEN 'Y' ELSE 'N' END AS ReverseCharge FROM TaxData GROUP BY DocEntry, TaxRate ) SELECT 'EXP' AS trans_type_code, RIGHT('0'+CAST(MONTH(T_ODOC.DocDate) AS VARCHAR(2)),2)+CAST(YEAR(T_ODOC.DocDate) AS VARCHAR(4)) AS fp, ROW_NUMBER() OVER(ORDER BY T_DOC12.DocEntry) AS ack_sr_no, T_DOC12.LocGSTN AS self_gstin, CASE WHEN T_ODOC.CANCELED='Y' THEN 'C' ELSE 'A' END AS resultflag, 'WOPAY' AS export_type_desc, CONCAT(T_NNM1.BeginStr, T_ODOC.DocNum) AS invoice_no, FORMAT(T_ODOC.DocDate,'dd-MM-yyyy') AS invoice_dt, T_ODOC.DocTotal AS invoice_val, ITS.TaxRate AS rate_of_tax, ITS.TotalTaxableValue AS taxable_val FROM ITS INNER JOIN OINV T_ODOC ON ITS.DocEntry=T_ODOC.DocEntry INNER JOIN INV12 T_DOC12 ON T_DOC12.DocEntry=T_ODOC.DocEntry INNER JOIN NNM1 T_NNM1 ON T_NNM1.Series=T_ODOC.Series LEFT JOIN OCST T_OCST_S ON T_OCST_S.Code=T_DOC12.StateS WHERE T_ODOC.CANCELED='N' AND T_ODOC.DocDate>='${from}' AND T_ODOC.DocDate<='${to}' AND T_DOC12.ImpORExp='Y' AND T_DOC12.CountryB<>'IN' AND T_DOC12.BpGSTN IS NULL AND T_OCST_S.GSTCode IS NULL AND NOT EXISTS (SELECT 1 FROM INV1 T_DE WHERE T_DE.DocEntry=T_ODOC.DocEntry AND T_DE.AcctCode IN (${DE_LEDGERS})) ORDER BY ITS.DocEntry, ITS.TaxRate`; } function sqlHsn(from, to) { return ` ;WITH INV_TAX AS ( SELECT DocEntry, LineNum, SUM(CASE WHEN staType = '-120' THEN TaxSum ELSE 0 END) AS iamt, SUM(CASE WHEN staType = '-110' THEN TaxSum ELSE 0 END) AS camt, SUM(CASE WHEN staType = '-100' THEN TaxSum ELSE 0 END) AS samt, SUM(CASE WHEN staType = '10' THEN TaxSum ELSE 0 END) AS tcs, -- line GST rate (IGST or CGST+SGST) — excludes TCS, unlike VatPrcnt SUM(CASE WHEN staType IN ('-120','-110','-100') THEN TaxRate ELSE 0 END) AS rt FROM INV4 GROUP BY DocEntry, LineNum ), RIN_TAX AS ( SELECT DocEntry, LineNum, SUM(CASE WHEN staType = '-120' THEN TaxSum ELSE 0 END) AS iamt, SUM(CASE WHEN staType = '-110' THEN TaxSum ELSE 0 END) AS camt, SUM(CASE WHEN staType = '-100' THEN TaxSum ELSE 0 END) AS samt, SUM(CASE WHEN staType = '10' THEN TaxSum ELSE 0 END) AS tcs, -- line GST rate (IGST or CGST+SGST) — excludes TCS, unlike VatPrcnt SUM(CASE WHEN staType IN ('-120','-110','-100') THEN TaxRate ELSE 0 END) AS rt FROM RIN4 GROUP BY DocEntry, LineNum ), HSN_Lines AS ( -- AR Invoice SELECT CONCAT('INV-', OINV.DocEntry) AS doc_key, CONVERT(VARCHAR(6), OINV.DocDate, 112) AS fp, INV12.LocGSTN AS self_gstin, REPLACE(OCHP.ChapterID, '.', '') AS hsn_sc, CASE WHEN OCHP.ChapterID NOT LIKE '99%' THEN 'Goods' ELSE 'Service' END AS gstr1Desc, CASE WHEN ISNULL(INV12.BpGSTN, '') = '' THEN 'B2C' ELSE 'B2B' END AS section, ISNULL(INV1.unitMsr, 'OTH') AS uqc, INV1.Quantity AS qty, INV1.LineTotal + INV1.VatSum AS val, INV1.LineTotal AS txval, ISNULL(T.iamt, 0) AS iamt, ISNULL(T.camt, 0) AS camt, ISNULL(T.samt, 0) AS samt, ISNULL(T.tcs, 0) AS tcs, ISNULL(T.rt, 0) AS rt FROM OINV INNER JOIN INV1 ON OINV.DocEntry = INV1.DocEntry LEFT JOIN INV_TAX T ON OINV.DocEntry = T.DocEntry AND INV1.LineNum = T.LineNum INNER JOIN INV12 ON OINV.DocEntry = INV12.DocEntry INNER JOIN OITM ON INV1.ItemCode = OITM.ItemCode INNER JOIN OCHP ON OITM.ChapterID = OCHP.AbsEntry WHERE OINV.DocDate BETWEEN '${from}' AND '${to}' AND OINV.CANCELED = 'N' UNION ALL -- AR Credit Memo SELECT CONCAT('CM-', ORIN.DocEntry) AS doc_key, CONVERT(VARCHAR(6), ORIN.DocDate, 112) AS fp, RIN12.LocGSTN AS self_gstin, REPLACE(OCHP.ChapterID, '.', '') AS hsn_sc, CASE WHEN OCHP.ChapterID NOT LIKE '99%' THEN 'Goods' ELSE 'Service' END AS gstr1Desc, CASE WHEN ISNULL(RIN12.BpGSTN, '') = '' THEN 'B2C' ELSE 'B2B' END AS section, ISNULL(RIN1.unitMsr, 'OTH') AS uqc, -RIN1.Quantity AS qty, -(RIN1.LineTotal + RIN1.VatSum) AS val, -RIN1.LineTotal AS txval, -ISNULL(T.iamt, 0) AS iamt, -ISNULL(T.camt, 0) AS camt, -ISNULL(T.samt, 0) AS samt, -ISNULL(T.tcs, 0) AS tcs, ISNULL(T.rt, 0) AS rt FROM ORIN INNER JOIN RIN1 ON ORIN.DocEntry = RIN1.DocEntry LEFT JOIN RIN_TAX T ON ORIN.DocEntry = T.DocEntry AND RIN1.LineNum = T.LineNum INNER JOIN RIN12 ON ORIN.DocEntry = RIN12.DocEntry INNER JOIN OITM ON RIN1.ItemCode = OITM.ItemCode INNER JOIN OCHP ON OITM.ChapterID = OCHP.AbsEntry WHERE ORIN.DocDate BETWEEN '${from}' AND '${to}' AND ORIN.CANCELED = 'N' ), HSN_Agg AS ( SELECT fp, self_gstin, hsn_sc, gstr1Desc, section, uqc, SUM(qty) AS qty_n, SUM(val) AS val_n, SUM(txval) AS txval_n, SUM(iamt) AS iamt_n, SUM(camt) AS camt_n, SUM(samt) AS samt_n, SUM(tcs) AS tcs_n, -- group by the line's own GST rate so 0% exports and 5%/18% supplies -- under the same HSN are separate rows (a blended rate is invalid) rt AS rate_of_tax_n FROM HSN_Lines GROUP BY fp, self_gstin, hsn_sc, gstr1Desc, section, uqc, rt ) SELECT ROW_NUMBER() OVER (ORDER BY hsn_sc, uqc, section, rate_of_tax_n) AS ack_sr_no, fp, self_gstin, hsn_sc, gstr1Desc, section, uqc, -- num: occurrence index of this HSN/SAC code — 1 if the code appears only -- once, 1,2,... if the same code repeats across different UQC/rate/section rows ROW_NUMBER() OVER (PARTITION BY hsn_sc ORDER BY uqc, section, rate_of_tax_n) AS num, -- Return values as formatted strings to avoid scientific notation LTRIM(STR(qty_n, 20, 3)) AS qty, LTRIM(STR(val_n, 20, 2)) AS val, LTRIM(STR(txval_n, 20, 2)) AS txval, LTRIM(STR(iamt_n, 20, 2)) AS iamt, LTRIM(STR(camt_n, 20, 2)) AS camt, LTRIM(STR(samt_n, 20, 2)) AS samt, LTRIM(STR(tcs_n, 20, 2)) AS tcs, LTRIM(STR(rate_of_tax_n, 20, 2)) AS rate_of_tax FROM HSN_Agg ORDER BY hsn_sc, uqc, section, rate_of_tax_n;`; } function sqlDocs(from, to) { return ` WITH DocStatus AS ( SELECT T0.DocEntry, T0.DocNum, T0.Series, T0.DocDate, 'Invoice' AS DocumentType, CASE WHEN T0.CANCELED='Y' OR CM.BaseEntry IS NOT NULL THEN 1 ELSE 0 END AS IsCanceled FROM OINV T0 LEFT JOIN (SELECT DISTINCT R1.BaseEntry FROM RIN1 R1 INNER JOIN ORIN R0 ON R0.DocEntry=R1.DocEntry WHERE R1.BaseType=13 AND R0.CANCELED='N') CM ON CM.BaseEntry=T0.DocEntry WHERE T0.DocDate>='${from}' AND T0.DocDate<='${to}' UNION ALL SELECT D0.DocEntry, D0.DocNum, D0.Series, D0.DocDate, 'Delivery', CASE WHEN D0.CANCELED='Y' THEN 1 ELSE 0 END FROM ODLN D0 INNER JOIN NNM1 N1 ON N1.Series=D0.Series WHERE N1.SeriesName LIKE '%FOC%' AND D0.DocDate>='${from}' AND D0.DocDate<='${to}' UNION ALL SELECT W0.DocEntry, W0.DocNum, W0.Series, W0.DocDate, CASE WHEN N1.SeriesName LIKE '%JW%' THEN 'JW' WHEN N1.SeriesName LIKE '%IT%' AND N1.SeriesName NOT LIKE '%ITR%' THEN 'IT' END, CASE WHEN W0.CANCELED='Y' THEN 1 ELSE 0 END FROM OWTR W0 INNER JOIN NNM1 N1 ON N1.Series=W0.Series WHERE (N1.SeriesName LIKE '%JW%' OR (N1.SeriesName LIKE '%IT%' AND N1.SeriesName NOT LIKE '%ITR%')) AND W0.DocDate>='${from}' AND W0.DocDate<='${to}' UNION ALL SELECT P0.DocEntry, P0.DocNum, P0.Series, P0.DocDate, 'AP Invoice (RCM)', CASE WHEN P0.CANCELED='Y' THEN 1 ELSE 0 END FROM OPCH P0 INNER JOIN NNM1 N1 ON N1.Series=P0.Series WHERE N1.SeriesName LIKE '%rev%' AND P0.DocDate>='${from}' AND P0.DocDate<='${to}' UNION ALL SELECT R0.DocEntry, R0.DocNum, R0.Series, R0.DocDate, 'AR Credit Memo', CASE WHEN R0.CANCELED='Y' THEN 1 ELSE 0 END FROM ORIN R0 INNER JOIN NNM1 N1 ON N1.Series=R0.Series WHERE N1.SeriesName LIKE '%ARCN%' AND R0.DocDate>='${from}' AND R0.DocDate<='${to}' ) SELECT CONVERT(char(6),DocDate,112) AS fp, Series, DocumentType, CASE WHEN DocumentType='AR Credit Memo' THEN CONCAT('CN-',MIN(DocNum)) WHEN DocumentType IN ('Invoice','Delivery') THEN CONCAT('1511',MIN(DocNum)) ELSE CAST(MIN(DocNum) AS varchar(20)) END AS sr_from, CASE WHEN DocumentType='AR Credit Memo' THEN CONCAT('CN-',MAX(DocNum)) WHEN DocumentType IN ('Invoice','Delivery') THEN CONCAT('1511',MAX(DocNum)) ELSE CAST(MAX(DocNum) AS varchar(20)) END AS sr_to, COUNT(*) AS totnum, SUM(IsCanceled) AS canceled, COUNT(*)-SUM(IsCanceled) AS net_issue FROM DocStatus GROUP BY CONVERT(char(6),DocDate,112), Series, DocumentType ORDER BY fp, Series, DocumentType`; } // ── route handlers ──────────────────────────────────────────────────────── async function handleQuery(req, res, sqlFn) { try { const from = safeDate(req.query.from || ''); const to = safeDate(req.query.to || ''); const { getPool } = require('../services/sqlPool'); const pool = await getPool(); const result = await pool.request().query(sqlFn(from, to)); const data = result.recordset || []; // Normalize all numeric-looking values to actual JS numbers // prevents DECIMAL(18,2) from being stringified as "1.568E7" downstream const normalized = data.map(row => { const out = {}; for (const [k, v] of Object.entries(row)) { if (v !== null && v !== undefined && typeof v === 'string' && /^-?[\d.]+([eE][+-]?\d+)?$/.test(v)) { out[k] = parseFloat(v); } else { out[k] = v; } } return out; }); res.json({ success: true, data: normalized }); } catch (err) { res.status(500).json({ success: false, message: err.message }); } } router.get('/b2b', verifyToken, (req, res) => handleQuery(req, res, sqlB2B)); router.get('/b2cl', verifyToken, (req, res) => handleQuery(req, res, sqlB2CL)); router.get('/b2cs', verifyToken, (req, res) => handleQuery(req, res, sqlB2CS)); router.get('/export', verifyToken, (req, res) => handleQuery(req, res, sqlExport)); router.get('/hsn', verifyToken, (req, res) => handleQuery(req, res, sqlHsn)); router.get('/docs', verifyToken, (req, res) => handleQuery(req, res, sqlDocs)); // ── TCS token ───────────────────────────────────────────────────────────── async function getTcsToken() { const baseUrl = process.env.TCS_GSP_BASE_URL || 'https://g31.tcsgsp.in'; const username = process.env.TCS_GSP_USERNAME || ''; const password = process.env.TCS_GSP_PASSWORD || ''; const resp = await axios.get( `${baseUrl}/Tax-Tool-Core/services/accessMgmt/generateToken`, { headers: { 'Content-Type': 'application/json', username, password }, httpsAgent } ); const token = resp.data?.access_token || resp.data?.token || resp.data?.accessToken; if (!token) throw new Error(`TCS token missing. Response: ${JSON.stringify(resp.data)}`); console.log('[TCS] Token obtained successfully'); return token; } // ── Build TCS-format Excel workbook ─────────────────────────────────────── async function buildTcsExcel({ b2b = [], b2cl = [], b2cs = [], exp = [], hsn = [], docs = [] }, gstin) { const wb = new ExcelJS.Workbook(); // Columns that must never render as scientific notation in Excel/TCS const NUMERIC_COLS = new Set([ 'fp','ack_sr_no','num','doc_num', 'invoice_val','taxable_val','rate_of_tax', 'igst_amnt','cgst_amnt','sgst_amnt','cess_amnt', 'diff_igst_amnt','diff_cgst_amnt','diff_sgst_amnt','diff_cess_amnt', 'diff_value','diff_percent','items_serial_no', 'txval','rt','iamt','camt','samt','csamt', 'qty','val','totnum','cancel','net_issue','pos', ]); function addSheet(name, headers, rows) { const ws = wb.addWorksheet(name); ws.addRow(headers); rows.forEach(r => { const rowData = headers.map(h => { const v = r[h]; return (v === null || v === undefined) ? null : v; }); const excelRow = ws.addRow(rowData); // Force plain decimal format — prevents 1.568502177E7 → "SANITY FAILED" headers.forEach((h, colIdx) => { if (NUMERIC_COLS.has(h)) { const cell = excelRow.getCell(colIdx + 1); if (typeof cell.value === 'number') { cell.numFmt = '0.##'; // no trailing zeros, never scientific } } }); }); } addSheet('B2B', ['trans_type_code','fp','ack_sr_no','self_gstin','system_cd','branch_cd','vertical_cd','resultflag','invoice_type','cp_gstin','cp_gstin_name','invoice_no','invoice_dt','invoice_val','taxable_val','items_serial_no','rate_of_tax','igst_amnt','cgst_amnt','sgst_amnt','cess_amnt','pos','rchrg','is_gstn_for_ecom_app','gstn_for_ecom_app','or_cp_gstin','orignal_invoice_no','orignal_invoice_dt'], b2b.map((r,i)=>({trans_type_code:'B2B',fp:Number(r.fp),ack_sr_no:i+1,self_gstin:gstin,system_cd:null,branch_cd:null,vertical_cd:null,resultflag:r.resultflag||'A',invoice_type:r.trans_type_code==='DE'?'DE':'R',cp_gstin:r.cp_gstin||null,cp_gstin_name:r.cp_gstin_name||null,invoice_no:r.invoice_no,invoice_dt:r.invoice_dt,invoice_val:r.invoice_val,taxable_val:r.taxable_val,items_serial_no:r.items_serial_no||1,rate_of_tax:r.rate_of_tax,igst_amnt:r.igst_amnt||null,cgst_amnt:r.cgst_amnt||null,sgst_amnt:r.sgst_amnt||null,cess_amnt:r.cess_amnt||null,pos:r.pos,rchrg:r.rchrg||'N',is_gstn_for_ecom_app:r.is_gstn_for_ecom_app||'N',gstn_for_ecom_app:r.gstn_for_ecom_app||null,or_cp_gstin:null,orignal_invoice_no:null,orignal_invoice_dt:null})) ); addSheet('B2BA',['trans_type_code','fp','ack_sr_no','self_gstin','system_cd','branch_cd','vertical_cd','resultflag','invoice_type','cp_gstin','cp_gstin_name','invoice_no','invoice_dt','invoice_val','taxable_val','items_serial_no','rate_of_tax','igst_amnt','cgst_amnt','sgst_amnt','cess_amnt','pos','rchrg','is_gstn_for_ecom_app','gstn_for_ecom_app','or_cp_gstin','orignal_invoice_no','orignal_invoice_dt','diff_percent'],[]); addSheet('B2CL', ['trans_type_code','fp','ack_sr_no','self_gstin','system_cd','branch_cd','vertical_cd','resultflag','invoice_type','invoice_no','invoice_dt','invoice_val','taxable_val','items_serial_no','rate_of_tax','igst_amnt','cess_amnt','pos','is_gstn_for_ecom_app','gstn_for_ecom_app','orignal_invoice_no','orignal_invoice_dt','diff_percent'], b2cl.map((r,i)=>({trans_type_code:'B2CL',fp:Number(r.fp),ack_sr_no:i+1,self_gstin:gstin,system_cd:null,branch_cd:null,vertical_cd:null,resultflag:r.resultflag||'A',invoice_type:null,invoice_no:r.invoice_no,invoice_dt:r.invoice_dt,invoice_val:r.invoice_val,taxable_val:r.taxable_val,items_serial_no:r.items_serial_no||1,rate_of_tax:r.rate_of_tax,igst_amnt:r.igst_amnt||null,cess_amnt:r.cess_amnt||null,pos:r.pos,is_gstn_for_ecom_app:r.is_gstn_for_ecom_app||'N',gstn_for_ecom_app:r.gstn_for_ecom_app||null,orignal_invoice_no:null,orignal_invoice_dt:null,diff_percent:0.65})) ); addSheet('B2CLA',['trans_type_code','fp','ack_sr_no','self_gstin','system_cd','branch_cd','vertical_cd','resultflag','invoice_type','invoice_no','invoice_dt','invoice_val','taxable_val','items_serial_no','rate_of_tax','igst_amnt','cess_amnt','pos','is_gstn_for_ecom_app','gstn_for_ecom_app','orignal_invoice_no','orignal_invoice_dt','diff_percent'],[]); addSheet('CDNR',['trans_type_code','fp','ack_sr_no','self_gstin','system_cd','branch_cd','vertical_cd','resultflag','cp_gstin','cp_gstin_name','type_of_note','reason_code','is_pre_gst_regime','dbt_crdt_note_no','dbt_crdt_note_dt','dbt_crdt_note_val','invoice_no','invoice_dt','diff_value','items_serial_no','rate_of_tax','diff_igst_amnt','diff_cgst_amnt','diff_sgst_amnt','diff_cess_amnt','or_cp_gstin','or_dbt_crdt_note_no','or_dbt_crdt_note_dt'],[]); addSheet('CDNRA',['ack_sr_no','self_gstin','system_cd','branch_cd','vertical_cd','resultflag','cp_gstin','cp_gstin_name','type_of_note','reason_code','is_pre_gst_regime','dbt_crdt_note_no','dbt_crdt_note_dt','dbt_crdt_note_val','invoice_no','invoice_dt','diff_value','items_serial_no','rate_of_tax','diff_igst_amnt','diff_cgst_amnt','diff_sgst_amnt','diff_cess_amnt','or_cp_gstin','or_dbt_crdt_note_no','or_dbt_crdt_note_dt','diff_percent'],[]); addSheet('CDNUR',['fp','ack_sr_no','self_gstin','system_cd','branch_cd','vertical_cd','resultflag','trans_cat_type','type_of_note','reason_code','is_pre_gst_regime','dbt_crdt_note_no','dbt_crdt_note_dt','dbt_crdt_note_val','invoice_no','invoice_dt','diff_value','items_serial_no','rate_of_tax','diff_igst_amnt','diff_cess_amnt','or_dbt_crdt_note_no','or_dbt_crdt_note_dt','diff_percent'],[]); addSheet('CDNURA',['fp','ack_sr_no','self_gstin','system_cd','branch_cd','vertical_cd','resultflag','trans_cat_type','type_of_note','reason_code','is_pre_gst_regime','dbt_crdt_note_no','dbt_crdt_note_dt','dbt_crdt_note_val','invoice_no','invoice_dt','diff_value','items_serial_no','rate_of_tax','diff_igst_amnt','diff_cess_amnt','or_dbt_crdt_note_no','or_dbt_crdt_note_dt','diff_percent'],[]); addSheet('EXP', ['trans_type_code','fp','ack_sr_no','self_gstin','system_cd','branch_cd','vertical_cd','resultflag','export_type_desc','invoice_no','invoice_dt','invoice_val','taxable_val','port_code','export_shipping_bill_no','export_shipping_bill_dt','rate_of_tax','igst_amnt','cess_amnt','orignal_invoice_no','orignal_invoice_dt','diff_percent'], exp.map((r,i)=>({trans_type_code:'EXP',fp:Number(r.fp),ack_sr_no:i+1,self_gstin:gstin,system_cd:null,branch_cd:null,vertical_cd:null,resultflag:r.resultflag||'A',export_type_desc:r.export_type_desc||'WOPAY',invoice_no:r.invoice_no,invoice_dt:r.invoice_dt,invoice_val:r.invoice_val,taxable_val:r.taxable_val,port_code:null,export_shipping_bill_no:r.export_shipping_bill_no||null,export_shipping_bill_dt:r.export_shipping_bill_dt||null,rate_of_tax:r.rate_of_tax,igst_amnt:r.igst_amnt||null,cess_amnt:r.cess_amnt||null,orignal_invoice_no:null,orignal_invoice_dt:null,diff_percent:0.65})) ); addSheet('EXPA',['trans_type_code','fp','ack_sr_no','self_gstin','system_cd','branch_cd','vertical_cd','resultflag','export_type_desc','invoice_no','invoice_dt','invoice_val','taxable_val','port_code','export_shipping_bill_no','export_shipping_bill_dt','rate_of_tax','igst_amnt','cess_amnt','orignal_invoice_no','orignal_invoice_dt','diff_percent'],[]); const b2csGrouped = {}; b2cs.forEach(r => { const key = `${r.pos}_${r.rate_of_tax}`; if (!b2csGrouped[key]) { b2csGrouped[key] = { pos: r.pos, rt: r.rate_of_tax, txval: 0, iamt: 0, camt: 0, samt: 0, sply_ty: Number(r.igst_amnt) > 0 ? 'INTER' : 'INTRA', }; } b2csGrouped[key].txval += Number(r.taxable_val) || 0; b2csGrouped[key].iamt += Number(r.igst_amnt) || 0; b2csGrouped[key].camt += Number(r.cgst_amnt) || 0; b2csGrouped[key].samt += Number(r.sgst_amnt) || 0; }); addSheet('B2CS', ['trans_type_code','fp','ack_sr_no','self_gstin','sply_ty','pos','txval','rt','iamt','camt','samt','csamt','typ','etin'], Object.values(b2csGrouped).map((r,i)=>({trans_type_code:'B2CS',fp:b2cs[0]?Number(b2cs[0].fp):null,ack_sr_no:i+1,self_gstin:gstin,sply_ty:r.sply_ty,pos:Number(r.pos),txval:Math.round(r.txval*100)/100,rt:Number(r.rt),iamt:r.iamt>0?Math.round(r.iamt*100)/100:null,camt:r.camt>0?Math.round(r.camt*100)/100:null,samt:r.samt>0?Math.round(r.samt*100)/100:null,csamt:null,typ:'OE',etin:null})) ); addSheet('B2CSA',['trans_type_code','fp','ack_sr_no','self_gstin','sply_ty','pos','txval','rt','iamt','camt','samt','csamt','typ','etin','omon','opos'],[]); addSheet('Nil',['trans_type_code','fp','ack_sr_no','self_gstin','sply_ty','nil_amt','expt_amt','ngsup_amt'],[]); // Aggregate HSN by (hsn_sc, uqc, rate_of_tax) — TCS requires this combination to be unique const hsnAgg = {}; hsn.forEach(r => { const uqc = validUQC(r.uqc); const rt = r.rate_of_tax || 0; const section = r.section || 'B2B'; const key = `${r.hsn_sc}_${uqc}_${rt}_${section}`; if (!hsnAgg[key]) { hsnAgg[key] = { fp: r.fp, hsn_sc: r.hsn_sc, gstr1Desc: r.gstr1Desc || 'Goods', uqc, rate_of_tax: rt, section, qty: 0, val: 0, txval: 0, iamt: 0, camt: 0, samt: 0, }; } hsnAgg[key].qty += parseFloat(r.qty) || 0; hsnAgg[key].val += parseFloat(r.val) || 0; hsnAgg[key].txval += parseFloat(r.txval) || 0; hsnAgg[key].iamt += parseFloat(r.iamt) || 0; hsnAgg[key].camt += parseFloat(r.camt) || 0; hsnAgg[key].samt += parseFloat(r.samt) || 0; }); // num: occurrence index of this HSN/SAC code among the rows being uploaded — // 1 if the code appears once, 1,2,... if the same code repeats across // different UQC/rate/section rows (verified against hsn_sc + uqc + rate_of_tax) const hsnRowsArr = Object.values(hsnAgg) .sort((a, b) => (a.hsn_sc < b.hsn_sc ? -1 : a.hsn_sc > b.hsn_sc ? 1 : a.uqc < b.uqc ? -1 : a.uqc > b.uqc ? 1 : a.section< b.section? -1 : a.section> b.section? 1 : a.rate_of_tax - b.rate_of_tax)); const hsnSeen = {}; addSheet('HSN', ['trans_type_code','fp','ack_sr_no','self_gstin','num','hsn_sc','gstr1Desc','uqc','qty','val','txval','iamt','camt','samt','csamt','rate_of_tax','section'], hsnRowsArr.map((r, i) => { hsnSeen[r.hsn_sc] = (hsnSeen[r.hsn_sc] || 0) + 1; return { trans_type_code: 'HSNSAC', fp: r.fp ? Number(toMMYYYY(r.fp)) : null, ack_sr_no: i + 1, self_gstin: gstin, num: hsnSeen[r.hsn_sc], hsn_sc: r.hsn_sc, gstr1Desc: r.gstr1Desc, uqc: r.uqc, qty: r2(r.qty), val: r2(r.val), txval: r2(r.txval), iamt: r.iamt ? r2(r.iamt) : null, camt: r.camt ? r2(r.camt) : null, samt: r.samt ? r2(r.samt) : null, csamt: null, rate_of_tax: r.rate_of_tax, section: r.section || 'B2B', }; }) ); const DOC_TYPE_MAP={'Invoice':'Invoices for outward supply','AR Credit Memo':'Credit Notes','Delivery':'Delivery Challan in cases other than by way of supply (excluding job work, supply on approval, liquid gas )','AP Invoice (RCM)':'Invoices for inward supply from unregistered person','JW':'Delivery Challan for job work','IT':'Delivery Challan for supply on approval'}; const DOC_NUM_MAP={'Invoice':1,'AR Credit Memo':4,'Delivery':12,'AP Invoice (RCM)':2,'JW':9,'IT':10}; addSheet('DOC', ['trans_type_code','fp','ack_sr_no','self_gstin','doc_num','num','docType','sr_from','sr_to','totnum','cancel','net_issue'], docs.map((r,i)=>({trans_type_code:'DOCISSUE',fp:r.fp?Number(toMMYYYY(r.fp)):null,ack_sr_no:i+1,self_gstin:gstin,doc_num:DOC_NUM_MAP[r.DocumentType]||1,num:1,docType:DOC_TYPE_MAP[r.DocumentType]||r.DocumentType,sr_from:r.sr_from,sr_to:r.sr_to,totnum:r.totnum,cancel:r.canceled,net_issue:r.net_issue})) ); addSheet('AT_ATA',['trans_type_code','fp','ack_sr_no','self_gstin','sply_ty','pos','ad_amt','rt','iamt','camt','samt','csamt','omon','or_pos','or_sply_ty'],[]); addSheet('TXPD_TXPDA',['trans_type_code','fp','ack_sr_no','self_gstin','sply_ty','pos','ad_amt','rt','iamt','camt','samt','csamt','omon','or_sply_ty','or_pos'],[]); return wb.xlsx.writeBuffer(); } // ── postSummaryRaw: for HSN rows whose numeric fields are already plain // decimal STRINGS from .toFixed(). Uses plain JSON.stringify — no replacer // that could reconvert those strings back to numbers (and trigger scientific // notation again). async function postSummaryRaw(baseUrl, clientCode, endpoint, mappingCd, templateCd, gstin, period, token, rows) { const url = `${baseUrl}/Tax-Tool-Core/services/auth/invoiceUpload/${endpoint}` + `/${clientCode}/${mappingCd}/${templateCd}/${gstin}/JSON/${period}?oprFlag=SALESSUMMARY`; console.log(`[TCS] ${endpoint} URL:`, url, '| rows:', rows.length); // Plain stringify — values are already formatted strings, must NOT be reconverted to numbers const resp = await axios.post(url, JSON.stringify(rows), { headers: { 'Content-Type': 'application/json', gstin, clientCode, Authorization: `Bearer ${token}` }, httpsAgent, }); console.log(`[TCS] ${endpoint} status:`, resp.status, JSON.stringify(resp.data)); return resp.data; } // Replacer: keep numbers as numbers (no quotes) but round to 2dp. // JSON.stringify never uses scientific notation for finite numbers below 1e21. function tcsJson(rows) { return JSON.stringify(rows, (_k, v) => { if (typeof v === 'number' && isFinite(v)) return parseFloat(v.toFixed(2)); return v; }); } async function postSummary(baseUrl, clientCode, endpoint, mappingCd, templateCd, gstin, period, token, rows) { const url = `${baseUrl}/Tax-Tool-Core/services/auth/invoiceUpload/${endpoint}` + `/${clientCode}/${mappingCd}/${templateCd}/${gstin}/JSON/${period}?oprFlag=SALESSUMMARY`; console.log(`[TCS] ${endpoint} URL:`, url, '| rows:', rows.length); const resp = await axios.post(url, tcsJson(rows), { headers: { 'Content-Type': 'application/json', gstin, clientCode, Authorization: `Bearer ${token}` }, httpsAgent, }); console.log(`[TCS] ${endpoint} status:`, resp.status, JSON.stringify(resp.data)); return resp.data; } const DOC_TYPE_MAP = { 'Invoice': 'Invoices for outward supply', 'AR Credit Memo': 'Credit Notes', 'Delivery': 'Delivery Challan in cases other than by way of supply (excluding job work, supply on approval, liquid gas )', 'AP Invoice (RCM)': 'Invoices for inward supply from unregistered person', 'JW': 'Delivery Challan for job work', 'IT': 'Delivery Challan for supply on approval', }; const DOC_NUM_MAP = { 'Invoice': 1, 'AR Credit Memo': 4, 'Delivery': 12, 'AP Invoice (RCM)': 2, 'JW': 9, 'IT': 10 }; // ═══════════════════════════════════════════════════════════════════════════ // POST /upload-tcs // Uploads all GSTR-1 sections via their respective TCS JSON APIs: // B2B/B2CL/EXP → salesInvUpld (transactional) // B2CS → salesSummaryUpldB2csb2csa (summary) // HSN → salesSummaryUpldAtatatxpd (summary) — uses postSummaryRaw // DOC → salesSummaryUpldDocumentsIssued (summary) // ═══════════════════════════════════════════════════════════════════════════ router.post('/upload-tcs', verifyToken, async (req, res) => { try { const { period, b2b = [], b2cl = [], export: exp = [], b2cs = [], hsn = [], docs = [] } = req.body; if (!period) return res.status(400).json({ success: false, message: 'period required (MMYYYY)' }); const baseUrl = process.env.TCS_GSP_BASE_URL || 'https://g31.tcsgsp.in'; const gstin = process.env.TCS_GSP_GSTIN || ''; const clientCode = process.env.TCS_GSP_CLIENT_CODE || ''; const mappingCd = process.env.TCS_GSP_MAPPING_CD || 'MFHSIDTN0000005'; const templateCd = process.env.TCS_GSP_TEMPLATE_CD || 'SalesInvoiceData802'; const missing = []; if (!gstin) missing.push('TCS_GSP_GSTIN'); if (!clientCode) missing.push('TCS_GSP_CLIENT_CODE'); if (missing.length) return res.status(400).json({ success: false, message: `Missing .env: ${missing.join(', ')}` }); const token = await getTcsToken(); let srNo = 0; const mappedB2B = b2b.map(row => ({ trans_type_code: 'B2B', fp: row.fp, ack_sr_no: ++srNo, self_gstin: gstin, system_cd: '', branch_cd: '', vertical_cd: '', resultflag: row.resultflag || 'A', export_type_desc: '', invoice_type: row.trans_type_code === 'DE' ? 'DE' : 'R', cp_gstin: row.cp_gstin || '', cp_gstin_name: row.cp_gstin_name || '', invoice_no: row.invoice_no, invoice_dt: row.invoice_dt, invoice_val: row.invoice_val, taxable_val: row.taxable_val, export_shipping_bill_no: '', export_shipping_bill_dt: '', items_serial_no: row.items_serial_no || 1, rate_of_tax: row.rate_of_tax, igst_amnt: row.igst_amnt || '', cgst_amnt: row.cgst_amnt || '', sgst_amnt: row.sgst_amnt || '', cess_amnt: row.cess_amnt || '', pos: String(row.pos || '').padStart(2, '0'), rchrg: row.rchrg || 'N', is_gstn_for_ecom_app: row.is_gstn_for_ecom_app || 'N', gstn_for_ecom_app: row.gstn_for_ecom_app || '', or_cp_gstin: '', orignal_invoice_no: '', orignal_invoice_dt: '', diff_percent: 0.65, })); const mappedB2CL = b2cl.map(row => ({ trans_type_code: 'B2CL', fp: row.fp, ack_sr_no: ++srNo, self_gstin: gstin, system_cd: '', branch_cd: '', vertical_cd: '', resultflag: row.resultflag || 'A', export_type_desc: '', invoice_type: '', cp_gstin: '', cp_gstin_name: '', invoice_no: row.invoice_no, invoice_dt: row.invoice_dt, invoice_val: row.invoice_val, taxable_val: row.taxable_val, export_shipping_bill_no: '', export_shipping_bill_dt: '', items_serial_no: row.items_serial_no || 1, rate_of_tax: row.rate_of_tax, igst_amnt: row.igst_amnt || '', cgst_amnt: '', sgst_amnt: '', cess_amnt: row.cess_amnt || '', pos: String(row.pos || '').padStart(2, '0'), rchrg: '', is_gstn_for_ecom_app: row.is_gstn_for_ecom_app || 'N', gstn_for_ecom_app: row.gstn_for_ecom_app || '', or_cp_gstin: '', orignal_invoice_no: '', orignal_invoice_dt: '', diff_percent: 0.65, })); // EXP — invoice_type field MUST NOT be included const mappedExp = exp.map(row => ({ trans_type_code: 'EXP', fp: row.fp, ack_sr_no: ++srNo, self_gstin: gstin, system_cd: '', branch_cd: '', vertical_cd: '', resultflag: row.resultflag || 'A', export_type_desc: row.export_type_desc || 'WOPAY', cp_gstin: '', cp_gstin_name: '', invoice_no: row.invoice_no, invoice_dt: row.invoice_dt, invoice_val: row.invoice_val, taxable_val: row.taxable_val, export_shipping_bill_no: row.export_shipping_bill_no || '', export_shipping_bill_dt: row.export_shipping_bill_dt || '', items_serial_no: '', rate_of_tax: row.rate_of_tax, igst_amnt: row.igst_amnt || '', cgst_amnt: '', sgst_amnt: '', cess_amnt: '', pos: '', rchrg: '', is_gstn_for_ecom_app: '', gstn_for_ecom_app: '', or_cp_gstin: '', orignal_invoice_no: '', orignal_invoice_dt: '', diff_percent: 0.65, })); const allRows = [...mappedB2B, ...mappedB2CL, ...mappedExp]; const results = {}; const errors = []; // ── 1. Transactional: B2B + B2CL + EXP ────────────────────────────── if (allRows.length) { const jsonUrl = `${baseUrl}/Tax-Tool-Core/services/auth/invoiceUpload/salesInvUpld` + `/${clientCode}/${mappingCd}/${templateCd}/${gstin}/JSON/${period}?oprFlag=SALES`; console.log('[TCS] Invoice URL:', jsonUrl); console.log('[TCS] B2B:', mappedB2B.length, '| B2CL:', mappedB2CL.length, '| EXP:', mappedExp.length); const r = await axios.post(jsonUrl, allRows, { headers: { 'Content-Type': 'application/json', gstin, clientCode, Authorization: `Bearer ${token}` }, httpsAgent, }); console.log('[TCS] Invoice status:', r.status, JSON.stringify(r.data)); results.invoices = r.data; if (r.data?.statusCd === 400 || r.data?.statusCd === '400') { const fe = Object.entries(r.data).filter(([k])=>k!=='statusCd').map(([k,v])=>`${k}: ${v}`).join(' | '); errors.push(`Invoices (B2B/B2CL/EXP): ${fe}`); } } // ── 2. B2CS summary ────────────────────────────────────────────────── if (b2cs.length) { const grouped = {}; b2cs.forEach(r => { const key = `${r.pos}_${r.rate_of_tax}`; if (!grouped[key]) grouped[key] = { pos: r.pos, rt: r.rate_of_tax, txval: 0, iamt: 0, camt: 0, samt: 0, sply_ty: Number(r.igst_amnt) > 0 ? 'INTER' : 'INTRA' }; grouped[key].txval += Number(r.taxable_val) || 0; grouped[key].iamt += Number(r.igst_amnt) || 0; grouped[key].camt += Number(r.cgst_amnt) || 0; grouped[key].samt += Number(r.sgst_amnt) || 0; }); const b2csSummary = Object.values(grouped).map((r, i) => ({ trans_type_code: 'B2CS', fp: Number(b2cs[0]?.fp) || null, // integer e.g. 42026 ack_sr_no: i + 1, self_gstin: gstin, sply_ty: r.sply_ty, pos: String(r.pos), // 2-char string e.g. "23" txval: Math.round(r.txval * 100) / 100, rt: Number(r.rt), iamt: r.iamt > 0 ? Math.round(r.iamt * 100) / 100 : 0, camt: r.camt > 0 ? Math.round(r.camt * 100) / 100 : 0, samt: r.samt > 0 ? Math.round(r.samt * 100) / 100 : 0, csamt: 0, typ: 'OE', })); const d = await postSummary(baseUrl, clientCode, 'salesSummaryUpldB2csb2csa', 'MFHGSFTN0000714', 'GSTR1SummaryFormB2CSB2CSA835', gstin, period, token, b2csSummary); results.b2cs = d; if (d?.statusCd === 400 || d?.statusCd === '400') { const fe = Object.entries(d).filter(([k])=>k!=='statusCd').map(([k,v])=>`${k}: ${v}`).join(' | '); errors.push(`B2CS: ${fe}`); } } // ── 3. HSN summary ─────────────────────────────────────────────────── if (hsn.length) { const hsnAggJson = {}; hsn.forEach(r => { const uqc = validUQC(r.uqc); const rt = r.rate_of_tax || 0; const section = r.section || 'B2B'; const key = `${r.hsn_sc}_${uqc}_${rt}_${section}`; if (!hsnAggJson[key]) hsnAggJson[key] = { fp: r.fp, hsn_sc: r.hsn_sc, gstr1Desc: r.gstr1Desc || 'Goods', uqc, rate_of_tax: rt, section, qty: 0, val: 0, txval: 0, iamt: 0, camt: 0, samt: 0, }; hsnAggJson[key].qty += Number(r.qty) || 0; hsnAggJson[key].val += Number(r.val) || 0; hsnAggJson[key].txval += Number(r.txval) || 0; hsnAggJson[key].iamt += Number(r.iamt) || 0; hsnAggJson[key].camt += Number(r.camt) || 0; hsnAggJson[key].samt += Number(r.samt) || 0; }); // num: occurrence index of this HSN/SAC code among the rows being uploaded — // 1 if the code appears once, 1,2,... if the same code repeats across // different UQC/rate/section rows (verified against hsn_sc + uqc + rate_of_tax) const hsnRowsSorted = Object.values(hsnAggJson) .sort((a, b) => (a.hsn_sc < b.hsn_sc ? -1 : a.hsn_sc > b.hsn_sc ? 1 : a.uqc < b.uqc ? -1 : a.uqc > b.uqc ? 1 : a.section< b.section? -1 : a.section> b.section? 1 : a.rate_of_tax - b.rate_of_tax)); const hsnSeenJson = {}; // All numeric fields serialized as plain decimal STRINGS via .toFixed() // so JSON.stringify never sees a raw float and cannot produce scientific notation const hsnRows = hsnRowsSorted.map((r, i) => { hsnSeenJson[r.hsn_sc] = (hsnSeenJson[r.hsn_sc] || 0) + 1; return { fp: toMMYYYY(r.fp), ack_sr_no: i + 1, self_gstin: gstin, num: hsnSeenJson[r.hsn_sc], hsn_sc: r.hsn_sc, gstr1Desc: r.gstr1Desc, uqc: r.uqc, qty: Number(parseFloat(r.qty || 0).toFixed(2)), val: parseFloat(r.val || 0).toFixed(2), // string "15685021.77" txval: parseFloat(r.txval || 0).toFixed(2), // string "15685021.77" iamt: parseFloat(r.iamt || 0).toFixed(2), camt: parseFloat(r.camt || 0).toFixed(2), samt: parseFloat(r.samt || 0).toFixed(2), csamt: '', rate_of_tax: parseFloat(r.rate_of_tax || 0).toFixed(2), section: r.section || 'B2B', }; }); // Log first 2 rows so you can verify no scientific notation before it hits TCS console.log('[TCS] HSN payload sample:', JSON.stringify(hsnRows.slice(0, 2), null, 2)); // Use postSummaryRaw — plain JSON.stringify, no replacer that could reconvert strings const d = await postSummaryRaw(baseUrl, clientCode, 'salesSummaryUpldAtatatxpd', 'MFHGSFTN0000716', 'GSTR1SummaryFormHSNSACSummary837', gstin, period, token, hsnRows); results.hsn = d; if (d?.statusCd === 400 || d?.statusCd === '400') { const fe = Object.entries(d).filter(([k])=>k!=='statusCd').map(([k,v])=>`${k}: ${v}`).join(' | '); errors.push(`HSN: ${fe}`); } } // ── 4. Documents Issued summary ────────────────────────────────────── if (docs.length) { const docRows = docs.map((r, i) => ({ fp: Number(toMMYYYY(r.fp)) || null, // integer e.g. 42026 ack_sr_no: i + 1, // integer self_gstin: gstin, doc_num: DOC_NUM_MAP[r.DocumentType] || 1, // integer num: 1, // integer docType: DOC_TYPE_MAP[r.DocumentType] || r.DocumentType, sr_from: String(r.sr_from), sr_to: String(r.sr_to), totnum: parseInt(r.totnum, 10) || 0, // integer cancel: parseInt(r.canceled, 10) || 0, // integer net_issue: parseInt(r.net_issue, 10) || 0, // integer })); const d = await postSummary(baseUrl, clientCode, 'salesSummaryUpldDocumentsIssued', 'MFHGSFTN0000717', 'GSTR1SummaryFormDocumentsIssuedSummary838', gstin, period, token, docRows); results.docs = d; if (d?.statusCd === 400 || d?.statusCd === '400') { const fe = Object.entries(d).filter(([k])=>k!=='statusCd').map(([k,v])=>`${k}: ${v}`).join(' | '); errors.push(`DOC: ${fe}`); } } if (!Object.keys(results).length) { return res.status(400).json({ success: false, message: 'No data to upload' }); } if (errors.length) { return res.status(400).json({ success: false, message: errors.join(' | '), data: results }); } const primaryAck = results.invoices?.ackNo || results.b2cs?.ackNo || results.hsn?.ackNo || results.docs?.ackNo; return res.json({ success: true, data: { ackNo: primaryAck, ...results }, method: 'JSON' }); } catch (err) { const msg = err.response?.data ? `[${err.response.status}] ${JSON.stringify(err.response.data)}` : err.message; console.error('[TCS] Error:', msg); res.status(500).json({ success: false, message: msg }); } }); // ── POST /upload-tcs-excel — Excel format upload ────────────────────────── router.post('/upload-tcs-excel', verifyToken, async (req, res) => { try { const { period, b2b = [], b2cl = [], b2cs = [], export: exp = [], hsn = [], docs = [] } = req.body; if (!period) return res.status(400).json({ success: false, message: 'period required' }); const baseUrl = process.env.TCS_GSP_BASE_URL || 'https://g31.tcsgsp.in'; const gstin = process.env.TCS_GSP_GSTIN || ''; const clientCode = process.env.TCS_GSP_CLIENT_CODE || ''; const mappingCd = process.env.TCS_GSP_MAPPING_CD || 'MFHSIDTN0000005'; const templateCd = process.env.TCS_GSP_TEMPLATE_CD || 'SalesInvoiceData802'; const token = await getTcsToken(); const excelBuf = await buildTcsExcel({ b2b, b2cl, b2cs, exp, hsn, docs }, gstin); const form = new FormData(); form.append('file', excelBuf, { filename: `GSTR1_${period}.xlsx`, contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', knownLength: excelBuf.length }); form.append('UpldGstinLst', gstin); const uploadUrl = `${baseUrl}/Tax-Tool-Core/services/auth/invoiceUpload/salesInvUpldForEXCEL/${clientCode}/${mappingCd}/${templateCd}/${gstin}/EXCEL/${period}?oprFlag=SALES`; console.log('[TCS] Excel Upload URL:', uploadUrl); const uploadResp = await axios.post(uploadUrl, form, { headers: { ...form.getHeaders(), gstin, clientCode, Authorization: `Bearer ${token}` }, maxContentLength: Infinity, maxBodyLength: Infinity, httpsAgent, }); console.log('[TCS] Excel Status :', uploadResp.status); console.log('[TCS] Excel Response:', JSON.stringify(uploadResp.data, null, 2)); const tcsData = uploadResp.data; if (tcsData?.statusCd === 400 || tcsData?.statusCd === '400') { const fieldErrors = Object.entries(tcsData).filter(([k])=>k!=='statusCd').map(([k,v])=>`${k}: ${v}`).join(' | '); return res.status(400).json({ success: false, message: `TCS Excel error — ${fieldErrors}`, raw: tcsData }); } res.json({ success: true, data: tcsData, method: 'EXCEL' }); } catch (err) { const msg = err.response?.data ? JSON.stringify(err.response.data) : err.message; res.status(500).json({ success: false, message: msg }); } }); module.exports = router;