Skip to content

Commit 709e03f

Browse files
Priyal RawalPriyal Rawal
authored andcommitted
feat: (p1)scaffold gst return export tool and (p2)persist raw gstn 2a/2b payload
1 parent 9710022 commit 709e03f

8 files changed

Lines changed: 325 additions & 5 deletions

File tree

india_compliance/gst_india/doctype/gst_return_export/__init__.py

Whitespace-only changes.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) 2026, Resilient Tech and contributors
2+
// For license information, please see license.txt
3+
4+
// GST Return Export — Phase 1 (GSTR-2A / 2B).
5+
// Built the Purchase Reconciliation Tool way: a Single DocType whose client
6+
// script is the tool. Filters are the DocType fields; page actions are Sync and
7+
// Export; the summary renders into the `summary_html` area (a frappe DataTable
8+
// in M4). Milestone 1 = scaffold: actions are inert placeholders.
9+
10+
frappe.ui.form.on("GST Return Export", {
11+
refresh(frm) {
12+
frm.disable_save();
13+
render_summary_placeholder(frm);
14+
},
15+
16+
company(frm) {
17+
frm.set_value("company_gstin", null);
18+
},
19+
});
20+
21+
function render_summary_placeholder(frm) {
22+
const field = frm.get_field("summary_html");
23+
if (!field) return;
24+
25+
field.$wrapper.html(`
26+
<div class="text-muted" style="padding: 2.5rem 1rem; text-align: center; border: 1px dashed var(--border-color); border-radius: 8px;">
27+
${__("Select Company, GSTIN, GST Return and period, then Sync. The summary will appear here.")}
28+
</div>
29+
`);
30+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
{
2+
"actions": [],
3+
"allow_copy": 1,
4+
"creation": "2026-06-26 00:00:00.000000",
5+
"doctype": "DocType",
6+
"engine": "InnoDB",
7+
"field_order": [
8+
"filters_section",
9+
"company",
10+
"company_gstin",
11+
"column_break_filters_1",
12+
"gst_return",
13+
"column_break_filters_2",
14+
"from_date",
15+
"to_date",
16+
"summary_section",
17+
"summary_html"
18+
],
19+
"fields": [
20+
{
21+
"fieldname": "filters_section",
22+
"fieldtype": "Section Break",
23+
"label": "Select Return"
24+
},
25+
{
26+
"fieldname": "company",
27+
"fieldtype": "Link",
28+
"label": "Company",
29+
"options": "Company",
30+
"reqd": 1
31+
},
32+
{
33+
"fieldname": "company_gstin",
34+
"fieldtype": "Autocomplete",
35+
"label": "Company GSTIN",
36+
"reqd": 1
37+
},
38+
{
39+
"fieldname": "column_break_filters_1",
40+
"fieldtype": "Column Break"
41+
},
42+
{
43+
"default": "GSTR-2B",
44+
"fieldname": "gst_return",
45+
"fieldtype": "Select",
46+
"label": "GST Return",
47+
"options": "GSTR-2A\nGSTR-2B",
48+
"reqd": 1
49+
},
50+
{
51+
"fieldname": "column_break_filters_2",
52+
"fieldtype": "Column Break"
53+
},
54+
{
55+
"fieldname": "from_date",
56+
"fieldtype": "Date",
57+
"label": "From Date"
58+
},
59+
{
60+
"fieldname": "to_date",
61+
"fieldtype": "Date",
62+
"label": "To Date"
63+
},
64+
{
65+
"fieldname": "summary_section",
66+
"fieldtype": "Section Break",
67+
"label": "Summary"
68+
},
69+
{
70+
"fieldname": "summary_html",
71+
"fieldtype": "HTML",
72+
"label": "Summary"
73+
}
74+
],
75+
"hide_toolbar": 1,
76+
"index_web_pages_for_search": 1,
77+
"issingle": 1,
78+
"links": [],
79+
"modified": "2026-06-26 00:00:00.000000",
80+
"modified_by": "Administrator",
81+
"module": "GST India",
82+
"name": "GST Return Export",
83+
"owner": "Administrator",
84+
"permissions": [
85+
{
86+
"create": 1,
87+
"email": 1,
88+
"export": 1,
89+
"print": 1,
90+
"read": 1,
91+
"role": "System Manager",
92+
"share": 1,
93+
"write": 1
94+
},
95+
{
96+
"create": 1,
97+
"email": 1,
98+
"export": 1,
99+
"print": 1,
100+
"read": 1,
101+
"role": "Accounts Manager",
102+
"share": 1,
103+
"write": 1
104+
},
105+
{
106+
"create": 1,
107+
"email": 1,
108+
"export": 1,
109+
"print": 1,
110+
"read": 1,
111+
"role": "Accounts User",
112+
"share": 1,
113+
"write": 1
114+
}
115+
],
116+
"quick_entry": 0,
117+
"sort_field": "modified",
118+
"sort_order": "DESC",
119+
"states": [],
120+
"track_changes": 0
121+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright (c) 2026, Resilient Tech and contributors
2+
# For license information, please see license.txt
3+
4+
from frappe.model.document import Document
5+
6+
7+
class GSTReturnExport(Document):
8+
"""
9+
GST Return Export tool — Phase 1 (GSTR-2A / 2B).
10+
11+
Milestone 1: scaffolding only. This Single DocType holds the user's filter
12+
selection (company, GSTIN, GST return, period). The actual behaviour lands
13+
in later milestones:
14+
- Sync from GSTN -> M3 (single period), M6 (range)
15+
- Prepared summary -> M4
16+
- Export to Excel -> M5
17+
Raw GSTN payloads are persisted via india_compliance.gst_india.utils.returns_export.
18+
"""
19+
20+
pass

india_compliance/gst_india/doctype/gstr_import_log/gstr_import_log.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"data_not_found",
1515
"dont_redownload",
1616
"request_id",
17-
"request_time"
17+
"request_time",
18+
"raw_data"
1819
],
1920
"fields": [
2021
{
@@ -70,6 +71,15 @@
7071
"fieldname": "dont_redownload",
7172
"fieldtype": "Check",
7273
"label": "Redownload not Required"
74+
},
75+
{
76+
"description": "Raw GSTN payload (gzipped) stored for the GST Return Export tool",
77+
"fieldname": "raw_data",
78+
"fieldtype": "Attach",
79+
"hidden": 1,
80+
"label": "Raw Data",
81+
"no_copy": 1,
82+
"read_only": 1
7383
}
7484
],
7585
"index_web_pages_for_search": 1,

india_compliance/gst_india/doctype/gstr_import_log/gstr_import_log.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,22 @@ def create_import_log(
1919
dont_redownload=False,
2020
request_id=None,
2121
retry_after_mins=None,
22+
raw_data=None,
2223
):
24+
25+
if raw_data is not None:
26+
return _create_import_log(
27+
gstin,
28+
return_type,
29+
return_period,
30+
classification,
31+
data_not_found,
32+
dont_redownload,
33+
request_id,
34+
retry_after_mins,
35+
raw_data=raw_data,
36+
)
37+
2338
frappe.enqueue(
2439
_create_import_log,
2540
queue="short",
@@ -44,6 +59,7 @@ def _create_import_log(
4459
dont_redownload=False,
4560
request_id=None,
4661
retry_after_mins=None,
62+
raw_data=None,
4763
):
4864
doctype = "GSTR Import Log"
4965
fields = {
@@ -68,10 +84,89 @@ def _create_import_log(
6884
log.dont_redownload = dont_redownload
6985
log.last_updated_on = frappe.utils.now()
7086
log.save(ignore_permissions=True)
87+
88+
if raw_data is not None:
89+
_attach_raw_data(log, raw_data)
90+
7191
if request_id:
7292
toggle_scheduled_jobs(False)
7393

7494

95+
def _attach_raw_data(log, json_data):
96+
"""
97+
Attach raw GSTN payload to the Import Log row for the GST Return Export tool.
98+
Always replaces. Caller must pre-merge GSTR-2B chunks before passing them here.
99+
"""
100+
from india_compliance.gst_india.doctype.gst_return_log.gst_return_log import (
101+
get_compressed_data,
102+
get_file_doc,
103+
)
104+
105+
content = get_compressed_data(json_data)
106+
107+
if file := get_file_doc(log.doctype, log.name, "raw_data"):
108+
file.save_file(content=content, overwrite=True)
109+
else:
110+
file = frappe.get_doc(
111+
{
112+
"doctype": "File",
113+
"attached_to_doctype": log.doctype,
114+
"attached_to_name": log.name,
115+
"attached_to_field": "raw_data",
116+
"file_name": frappe.scrub(f"{log.name}-raw_data.json.gz"),
117+
"is_private": 1,
118+
"content": content,
119+
}
120+
).insert(ignore_permissions=True)
121+
122+
log.db_set("raw_data", file.file_url)
123+
return file
124+
125+
126+
def get_import_log_raw(gstin, return_type, return_period, classification=None):
127+
"""Return the stored raw GSTN payload (parsed), or None if not stored yet."""
128+
from india_compliance.gst_india.doctype.gst_return_log.gst_return_log import (
129+
get_decompressed_data,
130+
get_file_doc,
131+
)
132+
133+
doctype = "GSTR Import Log"
134+
fields = {
135+
"gstin": gstin,
136+
"return_type": return_type,
137+
"return_period": return_period,
138+
}
139+
if classification:
140+
fields["classification"] = classification
141+
142+
name = frappe.db.get_value(doctype, fields)
143+
if not name:
144+
return None
145+
146+
file = get_file_doc(doctype, name, "raw_data")
147+
if not file:
148+
return None
149+
150+
return get_decompressed_data(file.get_content(encodings=[]))
151+
152+
153+
def merge_raw_payloads(existing, new):
154+
"""Merge a new raw chunk onto an existing one (for GSTR-2B multi-file)."""
155+
if isinstance(existing, dict) and isinstance(new, dict):
156+
merged = dict(existing)
157+
for key, value in new.items():
158+
if isinstance(merged.get(key), list) and isinstance(value, list):
159+
merged[key] = merged[key] + value
160+
else:
161+
merged[key] = value
162+
return merged
163+
164+
if isinstance(existing, list) and isinstance(new, list):
165+
return existing + new
166+
167+
return new
168+
169+
75170
def toggle_scheduled_jobs(stopped):
76171
scheduled_job = frappe.db.get_value(
77172
"Scheduled Job Type",

india_compliance/gst_india/utils/gstr_2/__init__.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from india_compliance.gst_india.utils import get_party_for_gstin
2121
from india_compliance.gst_india.utils.gstr_2 import gstr_2a, gstr_2b, ims
2222
from india_compliance.gst_india.utils.gstr_utils import ReturnType
23+
from india_compliance.gst_india.utils.returns_export import merge_raw
2324

2425

2526
class GSTRCategory(Enum):
@@ -207,10 +208,15 @@ def download_gstr_2b(gstin, return_periods):
207208

208209
# Handle multiple files for GSTR2B
209210
if response.data and (file_count := response.data.get("fc")):
211+
merged_docdata = {}
210212
for file_num in range(1, file_count + 1):
211213
r = api.get_data(return_period, file_num=file_num)
212-
save_gstr_2b(gstin, return_period, r)
214+
# merge raw chunks in memory; let save_gstr_2b skip the raw write
215+
merged_docdata = merge_raw(merged_docdata, r.data.get("docdata") or {})
216+
save_gstr_2b(gstin, return_period, r, store_raw=False)
213217

218+
# write the merged raw payload once (no repeated decompress/recompress)
219+
create_import_log(gstin, ReturnType.GSTR2B.value, return_period, raw_data=merged_docdata)
214220
continue # skip first response if file_count is greater than 1
215221

216222
save_gstr_2b(gstin, return_period, response)
@@ -293,15 +299,21 @@ def save_gstr_2a(gstin, return_period, json_data):
293299
if action.lower() not in json_data:
294300
continue
295301

296-
create_import_log(gstin, return_type.value, return_period, classification=category.value)
302+
create_import_log(
303+
gstin,
304+
return_type.value,
305+
return_period,
306+
classification=category.value,
307+
raw_data=json_data.get(action.lower()),
308+
)
297309

298310
# making consistent with GSTR2b
299311
json_data[category.value.lower()] = json_data.pop(action.lower())
300312

301313
save_gstr(gstin, return_type, return_period, json_data)
302314

303315

304-
def save_gstr_2b(gstin, return_period, json_data):
316+
def save_gstr_2b(gstin, return_period, json_data, *, store_raw=True):
305317
json_data = json_data.data
306318
return_type = ReturnType.GSTR2B
307319
if not json_data or json_data.get("gstin") != gstin:
@@ -313,7 +325,15 @@ def save_gstr_2b(gstin, return_period, json_data):
313325
title=_("Invalid Response Received."),
314326
)
315327

316-
create_import_log(gstin, return_type.value, return_period)
328+
if store_raw:
329+
create_import_log(
330+
gstin,
331+
return_type.value,
332+
return_period,
333+
raw_data=json_data.get("docdata") or {},
334+
)
335+
else:
336+
create_import_log(gstin, return_type.value, return_period)
317337
save_gstr(
318338
gstin,
319339
return_type,

0 commit comments

Comments
 (0)