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

1227 lines
66 KiB
JavaScript

'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;