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

1224 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 ──────────────────────────────────────────────────────────
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 ('4110202001','4110202003')) 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 ('4110202001','4110202003'))
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 ('4110202001','4110202003'))
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
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
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
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
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,
CASE
WHEN SUM(txval) = 0 THEN 0
WHEN ROUND(
(SUM(iamt) + SUM(camt) + SUM(samt)) * 100.0 / SUM(txval),
0
) IN (0, 5, 12, 18, 28)
THEN ROUND(
(SUM(iamt) + SUM(camt) + SUM(samt)) * 100.0 / SUM(txval),
0
)
ELSE 0
END AS rate_of_tax_n
FROM HSN_Lines
GROUP BY
fp,
self_gstin,
hsn_sc,
gstr1Desc,
section,
uqc
)
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;