Skip to content

Commit 6de98f2

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

9 files changed

Lines changed: 298 additions & 6 deletions

File tree

india_compliance/gst_india/doctype/gst_return_export/__init__.py

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
div[data-page-route="GST Return Export"] .section-body {
2+
max-width: 100% !important;
3+
}
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: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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+
"company",
9+
"column_break_filters_0",
10+
"company_gstin",
11+
"column_break_filters_1",
12+
"gst_return",
13+
"column_break_filters_2",
14+
"from_date",
15+
"column_break_filters_3",
16+
"to_date",
17+
"summary_section",
18+
"summary_html"
19+
],
20+
"fields": [
21+
{
22+
"fieldname": "company",
23+
"fieldtype": "Link",
24+
"label": "Company",
25+
"options": "Company",
26+
"reqd": 1
27+
},
28+
{
29+
"fieldname": "column_break_filters_0",
30+
"fieldtype": "Column Break"
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": "column_break_filters_3",
61+
"fieldtype": "Column Break"
62+
},
63+
{
64+
"fieldname": "to_date",
65+
"fieldtype": "Date",
66+
"label": "To Date"
67+
},
68+
{
69+
"fieldname": "summary_section",
70+
"fieldtype": "Section Break",
71+
"label": "Summary"
72+
},
73+
{
74+
"fieldname": "summary_html",
75+
"fieldtype": "HTML",
76+
"label": "Summary"
77+
}
78+
],
79+
"hide_toolbar": 1,
80+
"index_web_pages_for_search": 1,
81+
"issingle": 1,
82+
"links": [],
83+
"modified": "2026-06-26 00:00:00.000000",
84+
"modified_by": "Administrator",
85+
"module": "GST India",
86+
"name": "GST Return Export",
87+
"owner": "Administrator",
88+
"permissions": [
89+
{
90+
"create": 1,
91+
"email": 1,
92+
"export": 1,
93+
"print": 1,
94+
"read": 1,
95+
"role": "System Manager",
96+
"share": 1,
97+
"write": 1
98+
},
99+
{
100+
"create": 1,
101+
"email": 1,
102+
"export": 1,
103+
"print": 1,
104+
"read": 1,
105+
"role": "Accounts Manager",
106+
"share": 1,
107+
"write": 1
108+
},
109+
{
110+
"create": 1,
111+
"email": 1,
112+
"export": 1,
113+
"print": 1,
114+
"read": 1,
115+
"role": "Accounts User",
116+
"share": 1,
117+
"write": 1
118+
}
119+
],
120+
"quick_entry": 0,
121+
"sort_field": "modified",
122+
"sort_order": "DESC",
123+
"states": [],
124+
"track_changes": 0
125+
}
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/gst_return_log/gst_return_log.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"unfiled_summary",
2828
"filed_summary",
2929
"authenticated_summary",
30+
"raw_data",
3031
"computed_data_section",
3132
"is_latest_data",
3233
"section_break_emlz",
@@ -229,6 +230,15 @@
229230
"fieldtype": "Check",
230231
"label": "Is Nil Return",
231232
"read_only": 1
233+
},
234+
{
235+
"description": "Verbatim GSTN payload (gzipped) stored for the GST Return Export tool",
236+
"fieldname": "raw_data",
237+
"fieldtype": "Attach",
238+
"hidden": 1,
239+
"label": "Raw Data",
240+
"no_copy": 1,
241+
"read_only": 1
232242
}
233243
],
234244
"in_create": 1,

india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
getdate,
1616
)
1717
from frappe.utils.response import json_handler
18+
from frappe.utils.synchronization import filelock
1819

1920
from india_compliance.gst_india.doctype.gst_return_log.generate_gstr_1 import (
2021
FileGSTR1,
@@ -325,6 +326,53 @@ def get_decompressed_data(content):
325326
return frappe.parse_json(frappe.safe_decode(gzip.decompress(content)))
326327

327328

329+
def store_raw_return_data(gstin, return_type, return_period, json_data, *, merge=False):
330+
"""Persist the GSTN payload (gzipped) on the period's return log row,
331+
for the GST Return Export tool.
332+
333+
merge=True updates section keys into the existing payload; merge=False overwrites.
334+
"""
335+
name = f"{return_type}-{return_period}-{gstin}"
336+
337+
if not frappe.db.exists(DOCTYPE, name):
338+
get_gst_return_log(name)
339+
340+
with filelock(frappe.scrub(f"raw_return_{name}")):
341+
file = get_file_doc(DOCTYPE, name, "raw_data")
342+
343+
if merge and file and isinstance(json_data, dict):
344+
existing = get_decompressed_data(file.get_content(encodings=[]))
345+
if isinstance(existing, dict):
346+
json_data = {**existing, **json_data}
347+
348+
content = get_compressed_data(json_data)
349+
350+
if file:
351+
file.save_file(content=content, overwrite=True)
352+
else:
353+
file = frappe.get_doc(
354+
{
355+
"doctype": "File",
356+
"attached_to_doctype": DOCTYPE,
357+
"attached_to_name": name,
358+
"attached_to_field": "raw_data",
359+
"file_name": frappe.scrub(f"{name}-raw_data.json.gz"),
360+
"is_private": 1,
361+
"content": content,
362+
}
363+
).insert(ignore_permissions=True)
364+
frappe.db.set_value(DOCTYPE, name, "raw_data", file.file_url)
365+
366+
367+
def get_raw_return_data(gstin, return_type, return_period):
368+
"""Return the stored GSTN payload (parsed), or None if not synced yet."""
369+
file = get_file_doc(DOCTYPE, f"{return_type}-{return_period}-{gstin}", "raw_data")
370+
if not file:
371+
return None
372+
373+
return get_decompressed_data(file.get_content(encodings=[]))
374+
375+
328376
def create_ims_return_log(company_gstin):
329377
company = get_party_for_gstin(company_gstin, "Company")
330378

india_compliance/gst_india/utils/gstr_2/__init__.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@
1313
)
1414
from india_compliance.gst_india.doctype.gst_return_log.gst_return_log import (
1515
create_ims_return_log,
16+
store_raw_return_data,
1617
)
1718
from india_compliance.gst_india.doctype.gstr_import_log.gstr_import_log import (
1819
create_import_log,
1920
)
2021
from india_compliance.gst_india.utils import get_party_for_gstin
2122
from india_compliance.gst_india.utils.gstr_2 import gstr_2a, gstr_2b, ims
2223
from india_compliance.gst_india.utils.gstr_utils import ReturnType
24+
from india_compliance.gst_india.utils.returns_export import merge_raw
2325

2426

2527
class GSTRCategory(Enum):
@@ -80,6 +82,7 @@ def download_gstr_2a(gstin, return_periods, gst_categories=None):
8082

8183
json_data = frappe._dict({"gstin": gstin, "fp": return_period})
8284
has_data = False
85+
empty_categories = []
8386
for action, category in GSTR_2A_ACTIONS.items():
8487
requests_made += 1
8588

@@ -106,6 +109,7 @@ def download_gstr_2a(gstin, return_periods, gst_categories=None):
106109
classification=category.value,
107110
data_not_found=True,
108111
)
112+
empty_categories.append(category.value.lower())
109113
continue
110114

111115
# Queued
@@ -137,7 +141,7 @@ def download_gstr_2a(gstin, return_periods, gst_categories=None):
137141
json_data[action.lower()] = data
138142
has_data = True
139143

140-
save_gstr_2a(gstin, return_period, json_data)
144+
save_gstr_2a(gstin, return_period, json_data, merge=True, empty_categories=empty_categories)
141145

142146
if queued_message:
143147
publish_2a_2b_queued_message()
@@ -207,10 +211,13 @@ def download_gstr_2b(gstin, return_periods):
207211

208212
# Handle multiple files for GSTR2B
209213
if response.data and (file_count := response.data.get("fc")):
214+
merged_docdata = {}
210215
for file_num in range(1, file_count + 1):
211216
r = api.get_data(return_period, file_num=file_num)
212-
save_gstr_2b(gstin, return_period, r)
217+
merged_docdata = merge_raw(merged_docdata, r.data.get("docdata") or {})
218+
save_gstr_2b(gstin, return_period, r, store_raw=False)
213219

220+
store_raw_return_data(gstin, ReturnType.GSTR2B.value, return_period, merged_docdata)
214221
continue # skip first response if file_count is greater than 1
215222

216223
save_gstr_2b(gstin, return_period, response)
@@ -278,7 +285,7 @@ def download_ims_invoices(gstin, for_upload=False):
278285
return has_queued_invoices
279286

280287

281-
def save_gstr_2a(gstin, return_period, json_data):
288+
def save_gstr_2a(gstin, return_period, json_data, *, merge=False, empty_categories=None):
282289
return_type = ReturnType.GSTR2A
283290
if not json_data or json_data.get("gstin") != gstin or json_data.get("fp") != return_period:
284291
frappe.throw(
@@ -293,15 +300,25 @@ def save_gstr_2a(gstin, return_period, json_data):
293300
if action.lower() not in json_data:
294301
continue
295302

296-
create_import_log(gstin, return_type.value, return_period, classification=category.value)
303+
create_import_log(
304+
gstin,
305+
return_type.value,
306+
return_period,
307+
classification=category.value,
308+
)
297309

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

313+
raw_payload = dict(json_data)
314+
for category in empty_categories or []:
315+
raw_payload[category] = []
316+
store_raw_return_data(gstin, return_type.value, return_period, raw_payload, merge=merge)
317+
301318
save_gstr(gstin, return_type, return_period, json_data)
302319

303320

304-
def save_gstr_2b(gstin, return_period, json_data):
321+
def save_gstr_2b(gstin, return_period, json_data, *, store_raw=True):
305322
json_data = json_data.data
306323
return_type = ReturnType.GSTR2B
307324
if not json_data or json_data.get("gstin") != gstin:
@@ -314,6 +331,10 @@ def save_gstr_2b(gstin, return_period, json_data):
314331
)
315332

316333
create_import_log(gstin, return_type.value, return_period)
334+
335+
if store_raw:
336+
store_raw_return_data(gstin, return_type.value, return_period, json_data.get("docdata") or {})
337+
317338
save_gstr(
318339
gstin,
319340
return_type,
@@ -394,7 +415,7 @@ def update_import_history(return_periods):
394415
def _download_gstr_2a(gstin, return_period, json_data):
395416
json_data.gstin = gstin
396417
json_data.fp = return_period
397-
save_gstr_2a(gstin, return_period, json_data)
418+
save_gstr_2a(gstin, return_period, json_data, merge=True)
398419

399420

400421
def publish_2a_2b_queued_message():

0 commit comments

Comments
 (0)