Skip to content

Commit 50820eb

Browse files
authored
Merge pull request resilient-tech#3742 from ljain112/fix-gsr3b-excel
2 parents cb40790 + c651280 commit 50820eb

4 files changed

Lines changed: 325 additions & 2 deletions

File tree

20.3 KB
Binary file not shown.

india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ frappe.ui.form.on("GSTR 3B Report", {
2525
frm.set_intro(__("Please save the report again to rebuild or update"));
2626
frm.doc.__unsaved = 1;
2727

28-
// Download Button
28+
// Download JSON Button
2929
frm.add_custom_button(__("Download JSON"), function () {
3030
var w = window.open(
3131
frappe.urllib.get_full_url(
@@ -41,6 +41,22 @@ frappe.ui.form.on("GSTR 3B Report", {
4141
}
4242
});
4343

44+
// Download Excel Button
45+
frm.add_custom_button(__("Download Excel"), function () {
46+
var w = window.open(
47+
frappe.urllib.get_full_url(
48+
"/api/method/india_compliance.gst_india.doctype.gstr_3b_report.gstr_3b_report.download_gstr3b_as_excel?" +
49+
"name=" +
50+
encodeURIComponent(frm.doc.name)
51+
)
52+
);
53+
54+
if (!w) {
55+
frappe.msgprint(__("Please enable pop-ups"));
56+
return;
57+
}
58+
});
59+
4460
// View Form Button
4561
frm.add_custom_button(__("View Form"), function () {
4662
frappe.call({

india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py

Lines changed: 302 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
# For license information, please see license.txt
33

44

5+
import calendar
56
import json
67
import os
78
from collections import defaultdict
89

10+
from openpyxl.cell.cell import MergedCell
11+
912
import frappe
1013
from frappe import _
1114
from frappe.model.document import Document
@@ -17,7 +20,12 @@
1720
from india_compliance.gst_india.report.gstr_3b_details.gstr_3b_details import (
1821
IneligibleITC,
1922
)
20-
from india_compliance.gst_india.utils import get_gst_accounts_by_type, get_period
23+
from india_compliance.gst_india.utils import (
24+
get_data_file_path,
25+
get_gst_accounts_by_type,
26+
get_period,
27+
)
28+
from india_compliance.gst_india.utils.exporter import ExcelExporter
2129
from india_compliance.gst_india.utils.gstr_1.gstr_1_data import GSTR11A11BData
2230

2331
VALUES_TO_UPDATE = ["iamt", "camt", "samt", "csamt"]
@@ -749,3 +757,296 @@ def make_json(name):
749757
frappe.local.response.filename = file_name
750758
frappe.local.response.filecontent = json_data
751759
frappe.local.response.type = "download"
760+
761+
762+
@frappe.whitelist()
763+
def download_gstr3b_as_excel(name):
764+
"""Download GSTR 3B report as Excel file"""
765+
frappe.has_permission("GSTR 3B Report", throw=True)
766+
json_data = frappe.get_value("GSTR 3B Report", name, "json_output")
767+
768+
if not json_data:
769+
frappe.throw(_("Report data not found. Please generate the report."))
770+
771+
data = json.loads(json_data)
772+
exporter = GSTR3BExcelExporter(data)
773+
exporter.generate_excel()
774+
775+
776+
class GSTR3BExcelExporter:
777+
"""
778+
Export GSTR-3B data to Excel format using the official template.
779+
780+
This class handles data transformation and mapping from JSON to Excel cells
781+
following the official GSTR-3B offline utility format.
782+
"""
783+
784+
TEMPLATE_FILE = get_data_file_path("gstr3b_excel_utility_v5.7.xlsx")
785+
WORKSHEET_NAME = "GSTR-3B"
786+
787+
_STATE_CODE_TO_NAME = {code: state for state, code in STATE_NUMBERS.items()}
788+
789+
# Row mappings for each section (consistent with JSON keys)
790+
ROWS = {
791+
# Header info
792+
"gstin": 5,
793+
"year": 5,
794+
"month": 6,
795+
# Section 3.1 - Outward supplies
796+
"osup_det": 11,
797+
"osup_zero": 12,
798+
"osup_nil_exmp": 13,
799+
"isup_rev": 14,
800+
"osup_nongst": 15,
801+
"eco_reg_sup": 23,
802+
# Section 3.2 - Inter-state
803+
"inter_state_start": 88,
804+
# Section 4 - ITC
805+
"itc_import_goods": 31,
806+
"itc_import_services": 32,
807+
"itc_reverse_charge": 33,
808+
"itc_isd": 34,
809+
"itc_others": 35,
810+
"itc_reversed_rules": 37,
811+
"itc_reversed_others": 38,
812+
# Section 5 - Inward supplies
813+
"inward_gst": 48,
814+
"inward_non_gst": 49,
815+
}
816+
817+
HEADER_COLUMNS = {
818+
"gstin": 3,
819+
"year": 7,
820+
"month": 7,
821+
}
822+
823+
# Section 3.1 - Tax columns
824+
TAX_COLUMNS = {
825+
"txval": 3,
826+
"iamt": 4,
827+
"camt": 5,
828+
"csamt": 7,
829+
}
830+
831+
# Section 4 - ITC columns
832+
ITC_COLUMNS = {
833+
"iamt": 3,
834+
"camt": 4,
835+
"csamt": 6,
836+
}
837+
838+
# Section 5 - Inward supplies columns
839+
INWARD_COLUMNS = {
840+
"inter": 4,
841+
"intra": 5,
842+
}
843+
844+
# ITC type mappings based on 'ty' field in JSON
845+
ITC_AVAILABLE_TYPES = {
846+
"IMPG": "itc_import_goods",
847+
"IMPS": "itc_import_services",
848+
"ISRC": "itc_reverse_charge",
849+
"ISD": "itc_isd",
850+
"OTH": "itc_others",
851+
}
852+
853+
ITC_REVERSED_TYPES = {
854+
"RUL": "itc_reversed_rules",
855+
"OTH": "itc_reversed_others",
856+
}
857+
858+
INWARD_SUPPLY_TYPES = {
859+
"GST": "inward_gst",
860+
"NONGST": "inward_non_gst",
861+
}
862+
863+
COLUMN_SETS = {
864+
"tax": ["txval", "iamt", "camt", "csamt"],
865+
"itc": ["iamt", "camt", "csamt"],
866+
"import_itc": ["iamt", "csamt"],
867+
"inward": ["inter", "intra"],
868+
"zero_rated": ["txval", "iamt", "csamt"],
869+
"taxable_only": ["txval"],
870+
}
871+
872+
def __init__(self, data):
873+
self.data = data
874+
self.gstin = data.get("gstin")
875+
self.worksheet = None
876+
self.month = None
877+
self.fiscal_year = None
878+
879+
def generate_excel(self):
880+
"""Generate and export Excel file"""
881+
if not os.path.exists(self.TEMPLATE_FILE):
882+
frappe.throw(_("GSTR 3B Excel template not found"))
883+
884+
excel = ExcelExporter(file=self.TEMPLATE_FILE)
885+
self._update_worksheet(excel)
886+
887+
file_name = self._get_filename()
888+
excel.export(file_name)
889+
890+
def _get_filename(self):
891+
return f"GSTR-3B-{self.gstin}-{self.month}-{self.fiscal_year}.xlsx"
892+
893+
def _update_worksheet(self, excel):
894+
self.worksheet = excel.wb[self.WORKSHEET_NAME]
895+
896+
self._set_header_info()
897+
self._set_outward_supplies()
898+
self._set_ecommerce_supplies()
899+
self._set_inter_state_supplies()
900+
self._set_itc_details()
901+
self._set_inward_supplies()
902+
903+
def _set_header_info(self):
904+
"""Set header information"""
905+
period = self.data.get("ret_period", "")
906+
if not period or len(period) < 6:
907+
return
908+
909+
month_num = int(period[:2])
910+
calendar_year = int(period[2:6])
911+
912+
self.month = calendar.month_name[month_num]
913+
self.fiscal_year = self._get_fiscal_year(month_num, calendar_year)
914+
915+
self._set_cell(self.ROWS["gstin"], self.HEADER_COLUMNS["gstin"], self.gstin)
916+
self._set_cell(self.ROWS["year"], self.HEADER_COLUMNS["year"], self.fiscal_year)
917+
self._set_cell(self.ROWS["month"], self.HEADER_COLUMNS["month"], self.month)
918+
919+
def _get_fiscal_year(self, month_num, calendar_year):
920+
if month_num >= 4:
921+
fiscal_year_start = str(calendar_year)
922+
fiscal_year_end = str(calendar_year + 1)[2:]
923+
else:
924+
fiscal_year_start = str(calendar_year - 1)
925+
fiscal_year_end = str(calendar_year)[2:]
926+
927+
return f"{fiscal_year_start}-{fiscal_year_end}"
928+
929+
def _set_outward_supplies(self):
930+
sup_details = self.data.get("sup_details", {})
931+
932+
section_mappings = [
933+
("osup_det", "tax"),
934+
("osup_zero", "zero_rated"),
935+
("osup_nil_exmp", "taxable_only"),
936+
("isup_rev", "tax"),
937+
("osup_nongst", "taxable_only"),
938+
]
939+
940+
for json_key, column_set in section_mappings:
941+
data = sup_details.get(json_key, {})
942+
self._set_section_data(json_key, data, column_set)
943+
944+
def _set_ecommerce_supplies(self):
945+
eco_dtls = self.data.get("eco_dtls", {})
946+
self._set_section_data(
947+
"eco_reg_sup", eco_dtls.get("eco_reg_sup", {}), "taxable_only"
948+
)
949+
950+
def _set_inter_state_supplies(self):
951+
inter_sup = self.data.get("inter_sup", {})
952+
pos_data = self._group_by_place_of_supply(inter_sup)
953+
954+
if not pos_data:
955+
return
956+
957+
for i, (pos, data) in enumerate(sorted(pos_data.items())):
958+
row = self.ROWS["inter_state_start"] + i
959+
self._set_inter_state_row(row, pos, data)
960+
961+
def _group_by_place_of_supply(self, inter_sup):
962+
pos_data = {}
963+
categories = {
964+
"unreg_details": "unreg",
965+
"comp_details": "comp",
966+
"uin_details": "uin",
967+
}
968+
969+
for category_key, category_name in categories.items():
970+
for item in inter_sup.get(category_key, []):
971+
state_code = item.get("pos", "00")
972+
state_name = self._format_place_of_supply(state_code)
973+
974+
if state_name not in pos_data:
975+
pos_data[state_name] = {
976+
"unreg": {"txval": 0, "iamt": 0},
977+
"comp": {"txval": 0, "iamt": 0},
978+
"uin": {"txval": 0, "iamt": 0},
979+
}
980+
981+
pos_data[state_name][category_name]["txval"] += flt(
982+
item.get("txval", 0), 2
983+
)
984+
pos_data[state_name][category_name]["iamt"] += flt(
985+
item.get("iamt", 0), 2
986+
)
987+
988+
return pos_data
989+
990+
def _set_inter_state_row(self, row, pos, data):
991+
self._set_cell(row, 2, pos)
992+
993+
categories = [
994+
("unreg", 3, 4),
995+
("comp", 5, 6),
996+
("uin", 7, 8),
997+
]
998+
999+
for category, val_col, tax_col in categories:
1000+
category_data = data.get(category, {"txval": 0, "iamt": 0})
1001+
self._set_cell(row, val_col, category_data["txval"])
1002+
self._set_cell(row, tax_col, category_data["iamt"])
1003+
1004+
def _set_itc_details(self):
1005+
itc_elg = self.data.get("itc_elg", {})
1006+
self._populate_itc_sections(
1007+
itc_elg.get("itc_avl", []), self.ITC_AVAILABLE_TYPES
1008+
)
1009+
self._populate_itc_sections(itc_elg.get("itc_rev", []), self.ITC_REVERSED_TYPES)
1010+
1011+
def _populate_itc_sections(self, itc_entries, type_mapping):
1012+
for itc_entry in itc_entries:
1013+
itc_type = itc_entry.get("ty", "")
1014+
if itc_type not in type_mapping:
1015+
continue
1016+
1017+
row_key = type_mapping[itc_type]
1018+
column_set = "import_itc" if itc_type in ["IMPG", "IMPS"] else "itc"
1019+
self._set_section_data(row_key, itc_entry, column_set, self.ITC_COLUMNS)
1020+
1021+
def _set_inward_supplies(self):
1022+
inward_sup = self.data.get("inward_sup", {})
1023+
isup_details = inward_sup.get("isup_details", [])
1024+
1025+
for supply_data in isup_details:
1026+
supply_type = supply_data.get("ty")
1027+
if supply_type in self.INWARD_SUPPLY_TYPES:
1028+
row_key = self.INWARD_SUPPLY_TYPES[supply_type]
1029+
self._set_section_data(
1030+
row_key, supply_data, "inward", self.INWARD_COLUMNS
1031+
)
1032+
1033+
def _set_section_data(self, row_key, data, column_set, columns_dict=None):
1034+
row = self.ROWS[row_key]
1035+
columns = self.COLUMN_SETS[column_set]
1036+
mapping = columns_dict or self.TAX_COLUMNS
1037+
1038+
for key in columns:
1039+
if key in mapping:
1040+
value = flt(data.get(key, 0), 2)
1041+
self._set_cell(row, mapping[key], value)
1042+
1043+
def _set_cell(self, row, column, value):
1044+
cell = self.worksheet.cell(row, column)
1045+
if not isinstance(cell, MergedCell):
1046+
cell.value = value
1047+
1048+
@classmethod
1049+
def _format_place_of_supply(cls, state_code):
1050+
formatted_code = state_code.zfill(2)
1051+
state_name = cls._STATE_CODE_TO_NAME.get(formatted_code, "Other Territory")
1052+
return f"{formatted_code}-{state_name}"

india_compliance/gst_india/doctype/gstr_3b_report/test_gstr_3b_report.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
from frappe.tests import IntegrationTestCase, change_settings
88
from frappe.utils import getdate
99

10+
from india_compliance.gst_india.doctype.gstr_3b_report.gstr_3b_report import (
11+
GSTR3BExcelExporter,
12+
)
1013
from india_compliance.gst_india.utils.tests import (
1114
create_purchase_invoice,
1215
create_sales_invoice,
@@ -190,6 +193,9 @@ def test_gstr_3b_report(self):
190193
},
191194
)
192195

196+
exporter = GSTR3BExcelExporter(output)
197+
exporter.generate_excel()
198+
193199
def test_gst_rounding(self):
194200
gst_settings = frappe.get_cached_doc("GST Settings")
195201
gst_settings.round_off_gst_values = 1

0 commit comments

Comments
 (0)