diff --git a/india_compliance/gst_india/api_classes/base.py b/india_compliance/gst_india/api_classes/base.py
index a98649708..b61a2539b 100644
--- a/india_compliance/gst_india/api_classes/base.py
+++ b/india_compliance/gst_india/api_classes/base.py
@@ -154,6 +154,10 @@ def _make_request(
)
response_json = self.process_response(response_json)
+
+ if response_json.get("error_type") == "invalid_public_key":
+ return self._make_request(method, endpoint, params, headers, json)
+
return response_json.get("result", response_json)
except Exception as e:
diff --git a/india_compliance/gst_india/api_classes/public.py b/india_compliance/gst_india/api_classes/public.py
index f45a289a5..3653d7ef2 100644
--- a/india_compliance/gst_india/api_classes/public.py
+++ b/india_compliance/gst_india/api_classes/public.py
@@ -28,3 +28,8 @@ def get_gstin_info(self, gstin):
)
return response
+
+ def get_returns_info(self, gstin, fy):
+ return self.get(
+ "returns", params={"action": "RETTRACK", "gstin": gstin, "fy": fy}
+ )
diff --git a/india_compliance/gst_india/api_classes/returns.py b/india_compliance/gst_india/api_classes/returns.py
index eddbd5d99..ff4f4f88f 100644
--- a/india_compliance/gst_india/api_classes/returns.py
+++ b/india_compliance/gst_india/api_classes/returns.py
@@ -22,8 +22,12 @@
class PublicCertificate(BaseAPI):
BASE_PATH = "static"
- def get_gstn_public_certificate(self) -> str:
+ def get_gstn_public_certificate(self, error_message=None) -> str:
response = self.get(endpoint="gstn_g2b_prod_public")
+
+ if response.certificate == self.settings.gstn_public_certificate:
+ frappe.throw(error_message or _("Public Certificate is already up to date"))
+
self.settings.db_set("gstn_public_certificate", response.certificate)
return response.certificate
@@ -225,13 +229,16 @@ class ReturnsAPI(ReturnsAuthenticate):
"RET2B1023": "not_generated",
"RET2B1016": "no_docs_found",
"RT-3BAS1009": "no_docs_found",
+ "RET11417": "no_docs_found", # GSTR-1 Exports
"RET2B1018": "requested_before_cutoff_date",
"RTN_24": "queued",
+ "AUTH158": "invalid_otp", # Invalid OTP
"AUTH4033": "invalid_otp", # Invalid Session
# "AUTH4034": "invalid_otp", # Invalid OTP
"AUTH4038": "authorization_failed", # Session Expired
"RET11402": "authorization_failed", # API Authorization Failed for 2A
"RET2B1010": "authorization_failed", # API Authorization Failed for 2B
+ "TEC4002": "invalid_public_key",
}
def setup(self, company_gstin):
@@ -336,6 +343,14 @@ def handle_error_response(self, response):
title=_("API Request Failed"),
)
+ # Handle invalid public key
+ if response.error_type == "invalid_public_key":
+ PublicCertificate().get_gstn_public_certificate(
+ error_message=_(
+ "Looks like Public Key of GSTN used for encryption is Invalid"
+ )
+ )
+
def is_ignored_error(self, response):
error_code = response.get("error", {}).get("error_cd")
@@ -396,3 +411,25 @@ def get_data(self, action, return_period, otp=None):
endpoint="returns/gstr2a",
otp=otp,
)
+
+
+class GSTR1API(ReturnsAPI):
+ API_NAME = "GSTR-1"
+
+ def get_gstr_1_data(self, action, return_period, otp=None):
+ return self.get(
+ action,
+ return_period,
+ params={"ret_period": return_period},
+ endpoint="returns/gstr1",
+ otp=otp,
+ )
+
+ def get_einvoice_data(self, section, return_period, otp=None):
+ return self.get(
+ "EINV",
+ return_period,
+ params={"ret_period": return_period, "sec": section},
+ endpoint="returns/einvoice",
+ otp=otp,
+ )
diff --git a/india_compliance/gst_india/doctype/gst_settings/gst_settings.js b/india_compliance/gst_india/doctype/gst_settings/gst_settings.js
index 668b6a2f6..f8b59aa92 100644
--- a/india_compliance/gst_india/doctype/gst_settings/gst_settings.js
+++ b/india_compliance/gst_india/doctype/gst_settings/gst_settings.js
@@ -51,35 +51,22 @@ function filter_accounts(frm, account_field) {
function show_ic_api_promo(frm) {
if (!frm.doc.__onload?.can_show_promo) return;
+ const alert_message = `
+ Looking for API Features?
+
+ Get started with the India Compliance API!
+ `;
- const alert = $(`
-
- `).prependTo(frm.layout.wrapper);
-
- alert.on("closed.bs.alert", () => {
- frappe.xcall(
- "india_compliance.gst_india.doctype.gst_settings.gst_settings.disable_api_promo"
- );
- });
+ india_compliance.show_dismissable_alert(
+ frm.layout.wrapper,
+ alert_message,
+ "primary",
+ () => {
+ frappe.xcall(
+ "india_compliance.gst_india.doctype.gst_settings.gst_settings.disable_api_promo"
+ );
+ }
+ );
}
function show_update_gst_category_button(frm) {
diff --git a/india_compliance/gst_india/doctype/gst_settings/gst_settings.json b/india_compliance/gst_india/doctype/gst_settings/gst_settings.json
index ebb316208..aee01737c 100644
--- a/india_compliance/gst_india/doctype/gst_settings/gst_settings.json
+++ b/india_compliance/gst_india/doctype/gst_settings/gst_settings.json
@@ -40,6 +40,12 @@
"column_break_17",
"e_invoice_applicable_from",
"e_invoice_applicable_companies",
+ "gstr_1_section_break",
+ "compare_gstr_1_data",
+ "filing_frequency",
+ "column_break_cxmn",
+ "restrict_changes_after_gstr_1",
+ "role_allowed_to_modify",
"other_apis_section",
"autofill_party_info",
"archive_party_info_days",
@@ -560,12 +566,50 @@
"fieldtype": "Check",
"hidden": 1,
"label": "Is Retry e-Invoice/e-Waybill Pending"
+ },
+ {
+ "fieldname": "gstr_1_section_break",
+ "fieldtype": "Section Break",
+ "label": "GSTR-1"
+ },
+ {
+ "fieldname": "column_break_cxmn",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "filing_frequency",
+ "fieldtype": "Select",
+ "label": "Filing Frequency",
+ "options": "Monthly\nQuarterly"
+ },
+ {
+ "depends_on": "eval: doc.restrict_changes_after_gstr_1",
+ "fieldname": "role_allowed_to_modify",
+ "fieldtype": "Link",
+ "label": "Role Allowed to Modify Transactions",
+ "options": "Role"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: india_compliance.is_api_enabled(doc)",
+ "description": "Prevent any modifications to transactions once they have been reported in GSTR-1",
+ "fieldname": "restrict_changes_after_gstr_1",
+ "fieldtype": "Check",
+ "label": "Restrict Changes to Transactions After Filing"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: india_compliance.is_api_enabled(doc)",
+ "description": "Use APIs to compare records with GST Portal Data Before and After Filing",
+ "fieldname": "compare_gstr_1_data",
+ "fieldtype": "Check",
+ "label": "Compare Data with GST Portal"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2024-03-21 19:36:00.878806",
+ "modified": "2024-06-09 17:27:54.720233",
"modified_by": "Administrator",
"module": "GST India",
"name": "GST Settings",
diff --git a/india_compliance/gst_india/doctype/gst_settings/gst_settings.py b/india_compliance/gst_india/doctype/gst_settings/gst_settings.py
index 9f2a3bfe7..a83961928 100644
--- a/india_compliance/gst_india/doctype/gst_settings/gst_settings.py
+++ b/india_compliance/gst_india/doctype/gst_settings/gst_settings.py
@@ -5,7 +5,7 @@
from frappe import _
from frappe.model.document import Document
from frappe.query_builder.functions import IfNull
-from frappe.utils import getdate
+from frappe.utils import add_to_date, getdate
from india_compliance.gst_india.constants import GST_ACCOUNT_FIELDS, GST_PARTY_TYPES
from india_compliance.gst_india.constants.custom_fields import (
@@ -13,13 +13,13 @@
E_WAYBILL_FIELDS,
SALES_REVERSE_CHARGE_FIELDS,
)
+from india_compliance.gst_india.doctype.gstin.gstin import get_gstr_1_filed_upto
from india_compliance.gst_india.page.india_compliance_account import (
_disable_api_promo,
post_login,
)
from india_compliance.gst_india.utils import can_enable_api, is_api_enabled
from india_compliance.gst_india.utils.custom_fields import toggle_custom_fields
-from india_compliance.gst_india.utils.e_invoice import get_e_invoice_applicability_date
from india_compliance.gst_india.utils.gstin_info import get_gstin_info
E_INVOICE_START_DATE = "2021-01-01"
@@ -270,6 +270,42 @@ def validate_e_invoice_applicable_companies(self):
company_list.append(row.company)
+ def is_sek_valid(self, gstin, throw=False, threshold=30):
+ for credential in self.credentials:
+ if credential.service == "Returns" and credential.gstin == gstin:
+ break
+
+ else:
+ if throw:
+ frappe.throw(
+ _(
+ "No credential found for the GSTIN {0} in the GST Settings"
+ ).format(gstin)
+ )
+
+ return False
+
+ if credential.session_expiry and credential.session_expiry > add_to_date(
+ None, minutes=threshold * -1
+ ):
+ return True
+
+ def has_valid_credentials(self, gstin, service, throw=False):
+ for credential in self.credentials:
+ if credential.gstin == gstin and credential.service == service:
+ break
+ else:
+ message = _(
+ "No credential found for the GSTIN {0} in the GST Settings"
+ ).format(gstin)
+
+ if throw:
+ frappe.throw(message)
+
+ return False
+
+ return True
+
@frappe.whitelist()
def disable_api_promo():
@@ -345,6 +381,24 @@ def update_e_invoice_status():
update_not_applicable_status(e_invoice_applicability_date, company)
+def get_e_invoice_applicability_date(company, settings=None, throw=True):
+ if not settings:
+ settings = frappe.get_cached_doc("GST Settings")
+
+ e_invoice_applicable_from = settings.e_invoice_applicable_from
+
+ if settings.apply_e_invoice_only_for_selected_companies:
+ for row in settings.e_invoice_applicable_companies:
+ if company == row.company:
+ e_invoice_applicable_from = row.applicable_from
+ break
+
+ else:
+ return
+
+ return e_invoice_applicable_from
+
+
def update_pending_status(e_invoice_applicability_date, company=None):
if not e_invoice_applicability_date:
return
@@ -394,3 +448,52 @@ def update_not_applicable_status(e_invoice_applicability_date=None, company=None
company = query.where(sales_invoice.company == company)
query.run()
+
+
+def restrict_gstr_1_transaction_for(posting_date, company_gstin, gst_settings=None):
+ """
+ Check if the user is allowed to modify transactions before the GSTR-1 filing date
+ Additionally, update the `is_not_latest_gstr1_data` field in the GSTR-1 Log
+ """
+ posting_date = getdate(posting_date)
+
+ if not gst_settings:
+ gst_settings = frappe.get_cached_doc("GST Settings")
+
+ restrict = True
+
+ if not gst_settings.restrict_changes_after_gstr_1:
+ restrict = False
+
+ gstr_1_filed_upto = get_gstr_1_filed_upto(company_gstin)
+
+ if not gstr_1_filed_upto:
+ return False
+
+ if posting_date > getdate(gstr_1_filed_upto):
+ restrict = False
+
+ if (
+ gst_settings.role_allowed_to_modify in frappe.get_roles()
+ or frappe.session.user == "Administrator"
+ ):
+ restrict = False
+
+ if restrict:
+ return gstr_1_filed_upto
+
+ update_is_not_latest_gstr1_data(posting_date, company_gstin)
+
+ return None
+
+
+def update_is_not_latest_gstr1_data(posting_date, company_gstin):
+ period = posting_date.strftime("%m%Y")
+
+ frappe.db.set_value("GSTR-1 Log", f"{period}-{company_gstin}", "is_latest_data", 0)
+
+ frappe.publish_realtime(
+ "is_not_latest_data",
+ message={"filters": {"company_gstin": company_gstin, "period": period}},
+ doctype="GSTR-1 Beta",
+ )
diff --git a/india_compliance/gst_india/doctype/gstin/gstin.json b/india_compliance/gst_india/doctype/gstin/gstin.json
index 91731bbcb..234b1382c 100644
--- a/india_compliance/gst_india/doctype/gstin/gstin.json
+++ b/india_compliance/gst_india/doctype/gstin/gstin.json
@@ -12,7 +12,9 @@
"is_blocked",
"column_break_nrjd",
"last_updated_on",
- "cancelled_date"
+ "cancelled_date",
+ "section_break_ttzc",
+ "gstr_1_filed_upto"
],
"fields": [
{
@@ -59,12 +61,22 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Is Blocked"
+ },
+ {
+ "fieldname": "section_break_ttzc",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "gstr_1_filed_upto",
+ "fieldtype": "Date",
+ "hidden": 1,
+ "label": "GSTR-1 Filed Upto"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2023-08-17 11:32:42.332746",
+ "modified": "2024-05-17 15:38:05.867522",
"modified_by": "Administrator",
"module": "GST India",
"name": "GSTIN",
diff --git a/india_compliance/gst_india/doctype/gstin/gstin.py b/india_compliance/gst_india/doctype/gstin/gstin.py
index 4071d41c2..36dbb1170 100644
--- a/india_compliance/gst_india/doctype/gstin/gstin.py
+++ b/india_compliance/gst_india/doctype/gstin/gstin.py
@@ -304,3 +304,10 @@ def get_transporter_id_info(transporter_id):
"status": "Active" if response.transin else "Invalid",
}
)
+
+
+def get_gstr_1_filed_upto(gstin):
+ if not gstin:
+ return
+
+ return frappe.db.get_value("GSTIN", gstin, "gstr_1_filed_upto")
diff --git a/india_compliance/gst_india/doctype/gstr_1_beta/__init__.py b/india_compliance/gst_india/doctype/gstr_1_beta/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.css b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.css
new file mode 100644
index 000000000..63ba3901e
--- /dev/null
+++ b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.css
@@ -0,0 +1,137 @@
+div[data-page-route="GSTR-1 Beta"] {
+ --dt-row-height: 34px;
+}
+
+div[data-page-route="GSTR-1 Beta"] [data-fieldname="tabs_html"] .section-body {
+ margin-top: 0;
+}
+
+div[data-page-route="GSTR-1 Beta"] [data-fieldname="tabs_html"] .form-tabs-list {
+ margin-top: var(--margin-sm);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-right: var(--padding-lg);
+ position: inherit;
+}
+
+div[data-page-route="GSTR-1 Beta"]
+ [data-fieldname="tabs_html"]
+ .form-tabs-list
+ .custom-button-group {
+ display: flex;
+}
+
+div[data-page-route="GSTR-1 Beta"]
+ [data-fieldname="tabs_html"]
+ .form-tabs-list
+ .inner-group-button,
+div[data-page-route="GSTR-1 Beta"] [data-fieldname="tabs_html"] .filter-selector,
+div[data-page-route="GSTR-1 Beta"] [data-fieldname="tabs_html"] .view-group {
+ margin-bottom: 8px;
+ margin-left: 8px;
+}
+
+div[data-page-route="GSTR-1 Beta"]
+ [data-fieldname="tabs_html"]
+ .form-tabs-list
+ .custom-button-group
+ .btn {
+ padding: 5px 10px;
+}
+
+div[data-page-route="GSTR-1 Beta"] .tab-title-text {
+ font-size: 1.2rem;
+}
+
+div[data-page-route="GSTR-1 Beta"] .tab-subtitle-text {
+ font-size: 0.8rem;
+}
+
+div[data-page-route="GSTR-1 Beta"] .datatable .dt-scrollable {
+ overflow-y: auto !important;
+}
+
+div[data-page-route="GSTR-1 Beta"] .datatable .dt-row {
+ height: unset;
+}
+
+div[data-page-route="GSTR-1 Beta"] .datatable .dt-row-filter {
+ height: var(--dt-row-height);
+}
+
+div[data-page-route="GSTR-1 Beta"] .datatable .dt-row-filter .dt-cell {
+ max-height: var(--dt-row-height);
+}
+
+div[data-page-route="GSTR-1 Beta"] [data-fieldname="tabs_empty_state"],
+div[data-page-route="GSTR-1 Beta"] [data-fieldname="tabs_no_data"] {
+ min-height: 320px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+}
+
+div[data-page-route="GSTR-1 Beta"] [data-fieldname="tabs_empty_state"] > img,
+div[data-page-route="GSTR-1 Beta"] [data-fieldname="tabs_no_data"] > img {
+ margin-bottom: var(--margin-md);
+ max-height: 100px;
+}
+
+div[data-page-route="GSTR-1 Beta"] .tab-actions {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+}
+
+div[data-page-route="GSTR-1 Beta"] .frappe-control [data-fieldtype="HTML"] {
+ margin-left: -11px;
+ margin-right: -15px;
+}
+
+div[data-page-route="GSTR-1 Beta"] .custom-tabs > .nav-item > .nav-link {
+ padding: 4px 8px;
+}
+
+div[data-page-route="GSTR-1 Beta"] .custom-tabs > .nav-item > .nav-link.active {
+ background-color: #e2e2e2;
+ color: var(--primary);
+}
+
+div[data-page-route="GSTR-1 Beta"] .custom-tabs > .nav-item:first-child:not(:has(.disabled)):hover {
+ background-color: var(--btn-default-hover-bg);
+ border-radius: 0.4rem 0 0 0.4rem;
+ color: var(--text-color);
+}
+
+div[data-page-route="GSTR-1 Beta"] .custom-tabs > .nav-item:last-child:not(:has(.disabled)):hover {
+ background-color: var(--btn-default-hover-bg);
+ border-radius: 0 0.4rem 0.4rem 0;
+ color: var(--text-color);
+}
+
+div[data-page-route="GSTR-1 Beta"] .custom-tabs > .nav-item > .nav-link.active:hover {
+ background-color: #ededed;
+ color: var(--primary);
+}
+
+div[data-page-route="GSTR-1 Beta"] .custom-tabs > .nav-item:first-child > .nav-link {
+ border-radius: 0.4rem 0 0 0.4rem;
+}
+
+div[data-page-route="GSTR-1 Beta"] .custom-tabs > .nav-item:last-child > .nav-link {
+ border-radius: 0 0.4rem 0.4rem 0;
+}
+
+div[data-page-route="GSTR-1 Beta"] [data-fieldname="tabs_html"] .reconcile-row {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+
+.not-matched {
+ color: red;
+}
diff --git a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js
new file mode 100644
index 000000000..5b2a81a9e
--- /dev/null
+++ b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.js
@@ -0,0 +1,2187 @@
+// Copyright (c) 2024, Resilient Tech and contributors
+// For license information, please see license.txt
+
+frappe.provide("india_compliance");
+
+const DOCTYPE = "GSTR-1 Beta";
+const GSTR1_Category = {
+ B2B: "B2B, SEZ, DE",
+ EXP: "Exports",
+ B2CL: "B2C (Large)",
+ B2CS: "B2C (Others)",
+ NIL_EXEMPT: "Nil-Rated, Exempted, Non-GST",
+ CDNR: "Credit/Debit Notes (Registered)",
+ CDNUR: "Credit/Debit Notes (Unregistered)",
+ // Other Categories
+ AT: "Advances Received",
+ TXP: "Advances Adjusted",
+ HSN: "HSN Summary",
+ DOC_ISSUE: "Document Issued",
+};
+
+const GSTR1_SubCategory = {
+ B2B_REGULAR: "B2B Regular",
+ B2B_REVERSE_CHARGE: "B2B Reverse Charge",
+ SEZWP: "SEZ With Payment of Tax",
+ SEZWOP: "SEZ Without Payment of Tax",
+ DE: "Deemed Exports",
+ EXPWP: "Export With Payment of Tax",
+ EXPWOP: "Export Without Payment of Tax",
+ B2CL: "B2C (Large)",
+ B2CS: "B2C (Others)",
+ NIL_EXEMPT: "Nil-Rated, Exempted, Non-GST",
+ CDNR: "Credit/Debit Notes (Registered)",
+ CDNUR: "Credit/Debit Notes (Unregistered)",
+
+ AT: "Advances Received",
+ TXP: "Advances Adjusted",
+ HSN: "HSN Summary",
+ DOC_ISSUE: "Document Issued",
+
+ SUPECOM_52: "TCS collected by E-commerce Operator u/s 52",
+ SUPECOM_9_5: "GST Payable on RCM by E-commerce Operator u/s 9(5)",
+};
+
+const INVOICE_TYPE = {
+ [GSTR1_Category.B2B]: [
+ GSTR1_SubCategory.B2B_REGULAR,
+ GSTR1_SubCategory.B2B_REVERSE_CHARGE,
+ GSTR1_SubCategory.SEZWP,
+ GSTR1_SubCategory.SEZWOP,
+ GSTR1_SubCategory.DE,
+ ],
+ [GSTR1_Category.B2CL]: [GSTR1_SubCategory.B2CL],
+ [GSTR1_Category.EXP]: [GSTR1_SubCategory.EXPWP, GSTR1_SubCategory.EXPWOP],
+ [GSTR1_Category.NIL_EXEMPT]: [GSTR1_SubCategory.NIL_EXEMPT],
+ [GSTR1_Category.CDNR]: [GSTR1_SubCategory.CDNR],
+ [GSTR1_Category.CDNUR]: [GSTR1_SubCategory.CDNUR],
+ [GSTR1_Category.AT]: [GSTR1_SubCategory.AT],
+ [GSTR1_Category.TXP]: [GSTR1_SubCategory.TXP],
+ [GSTR1_Category.HSN]: [GSTR1_SubCategory.HSN],
+ [GSTR1_Category.DOC_ISSUE]: [GSTR1_SubCategory.DOC_ISSUE],
+};
+
+const GSTR1_DataField = {
+ TRANSACTION_TYPE: "transaction_type",
+ CUST_GSTIN: "customer_gstin",
+ ECOMMERCE_GSTIN: "ecommerce_gstin",
+ CUST_NAME: "customer_name",
+ DOC_DATE: "document_date",
+ DOC_NUMBER: "document_number",
+ DOC_TYPE: "document_type",
+ DOC_VALUE: "document_value",
+ POS: "place_of_supply",
+ DIFF_PERCENTAGE: "diff_percentage",
+ REVERSE_CHARGE: "reverse_charge",
+ TAXABLE_VALUE: "total_taxable_value",
+ TAX_RATE: "tax_rate",
+ IGST: "total_igst_amount",
+ CGST: "total_cgst_amount",
+ SGST: "total_sgst_amount",
+ CESS: "total_cess_amount",
+ UPLOAD_STATUS: "upload_status",
+
+ SHIPPING_BILL_NUMBER: "shipping_bill_number",
+ SHIPPING_BILL_DATE: "shipping_bill_date",
+ SHIPPING_PORT_CODE: "shipping_port_code",
+
+ EXEMPTED_AMOUNT: "exempted_amount",
+ NIL_RATED_AMOUNT: "nil_rated_amount",
+ NON_GST_AMOUNT: "non_gst_amount",
+
+ HSN_CODE: "hsn_code",
+ DESCRIPTION: "description",
+ UOM: "uom",
+ QUANTITY: "quantity",
+
+ FROM_SR: "from_sr_no",
+ TO_SR: "to_sr_no",
+ TOTAL_COUNT: "total_count",
+ DRAFT_COUNT: "draft_count",
+ CANCELLED_COUNT: "cancelled_count",
+};
+
+frappe.ui.form.on(DOCTYPE, {
+ async setup(frm) {
+ patch_set_indicator(frm);
+ frappe.require("gstr1.bundle.js").then(() => {
+ frm.gstr1 = new GSTR1(frm);
+ frm.trigger("company");
+ });
+
+ frm.filing_frequency = gst_settings.filing_frequency;
+
+ // Set Default Values
+ set_default_company_gstin(frm);
+ set_options_for_year(frm);
+ set_options_for_month_or_quarter(frm);
+
+ frm.__setup_complete = true;
+
+ // Setup Listeners
+ frappe.realtime.on("is_not_latest_data", message => {
+ const { filters } = message;
+
+ const [month_or_quarter, year] =
+ india_compliance.get_month_year_from_period(filters.period);
+
+ if (
+ frm.doc.company_gstin !== filters.company_gstin ||
+ frm.doc.month_or_quarter != month_or_quarter ||
+ frm.doc.year != year
+ )
+ return;
+
+ if (frm.$wrapper.find(".form-message.orange").length) return;
+ frm.set_intro(
+ __(
+ "Books data was updated after the computation of GSTR-1 data. Please generate GSTR-1 again."
+ ),
+ "orange"
+ );
+ });
+
+ frappe.realtime.on("gstr1_generation_failed", message => {
+ const { error, filters } = message;
+ let alert = `GSTR-1 Generation Failed for ${filters.company_gstin} - ${filters.month_or_quarter} - ${filters.year}.
${error}`;
+
+ frappe.msgprint({
+ title: __("GSTR-1 Generation Failed"),
+ message: alert,
+ });
+ });
+
+ frappe.realtime.on("gstr1_data_prepared", message => {
+ const { data, filters } = message;
+
+ if (
+ frm.doc.company_gstin !== filters.company_gstin ||
+ frm.doc.month_or_quarter != filters.month_or_quarter ||
+ frm.doc.year != filters.year
+ )
+ return;
+
+ frappe.after_ajax(() => {
+ frm.doc.__onload = { data };
+ frm.trigger("after_save");
+ });
+ });
+ },
+
+ async company(frm) {
+ render_empty_state(frm);
+
+ if (!frm.doc.company) return;
+ const options = await india_compliance.set_gstin_options(frm);
+
+ frm.set_value("company_gstin", options[0]);
+ },
+
+ company_gstin: render_empty_state,
+
+ month_or_quarter(frm) {
+ render_empty_state(frm);
+ },
+
+ year(frm) {
+ render_empty_state(frm);
+ set_options_for_month_or_quarter(frm);
+ },
+
+ refresh(frm) {
+ // Primary Action
+ frm.disable_save();
+ frm.page.set_primary_action(__("Generate"), () => frm.save());
+ },
+
+ before_save(frm) {
+ frm.doc.__unsaved = true;
+ },
+
+ async after_save(frm) {
+ const data = frm.doc.__onload?.data;
+ if (!frm._otp_requested && data == "otp_requested") {
+ frm._otp_requested = true;
+
+ india_compliance
+ .authenticate_otp(frm.doc.company_gstin)
+ .then(() => frm.save());
+ return;
+ }
+
+ frm._otp_requested = false;
+
+ if (!data?.status) return;
+ frm.gstr1.status = data.status;
+ frm.gstr1.refresh_data(data);
+ },
+});
+
+class GSTR1 {
+ // Render page / Setup Listeners / Setup Data
+ TABS = [
+ {
+ label: __("Books"),
+ name: "books",
+ is_active: true,
+ _TabManager: BooksTab,
+ },
+ {
+ label: __("Unfiled"),
+ name: "unfiled",
+ _TabManager: UnfiledTab,
+ },
+ {
+ label: __("Reconcile"),
+ name: "reconcile",
+ _TabManager: ReconcileTab,
+ },
+ {
+ label: __("Filed"),
+ name: "filed",
+ _TabManager: FiledTab,
+ },
+ ];
+
+ constructor(frm) {
+ this.init(frm);
+ this.render();
+ }
+
+ init(frm) {
+ this.frm = frm;
+ this.data = frm.doc._data;
+ this.filters = [];
+ this.$wrapper = frm.fields_dict.tabs_html.$wrapper;
+ }
+
+ refresh_data(data) {
+ this.render_indicator();
+
+ // clear filters if any and set default view
+ this.active_view = "Summary";
+ this.filter_group.filter_x_button.click();
+
+ if (data) this.data = data;
+
+ // set data for filing return
+ if (!this.data["filed"]) {
+ // deepcopy
+ const filed = JSON.parse(JSON.stringify(this.data["books"]));
+ Object.assign(filed, filed.aggregate_data);
+
+ this.data["filed"] = filed;
+ this.data["filed_summary"] = this.data["books_summary"];
+ }
+
+ // set idx for reconcile rows (for detail view)
+ if (this.data["reconcile"]) {
+ Object.values(this.data["reconcile"]).forEach(category => {
+ category instanceof Array &&
+ category.forEach((row, idx) => {
+ row.idx = idx;
+ });
+ });
+ }
+
+ this.set_output_gst_balances();
+
+ // refresh tabs
+ this.TABS.forEach(_tab => {
+ const tab_name = _tab.name;
+ const tab = this.tabs[`${tab_name}_tab`];
+
+ if (!this.data[tab_name]) {
+ tab.hide();
+ _tab.shown = false;
+ return;
+ }
+
+ tab.show();
+ _tab.shown = true;
+ tab.tabmanager.refresh_data(
+ this.data[tab_name],
+ this.data[`${tab_name}_summary`],
+ this.status
+ );
+ });
+ }
+
+ async refresh_view() {
+ // for change in view (Summary/Detailed)
+ this.viewgroup.set_active_view(this.active_view);
+
+ this.toggle_filter_selector();
+
+ let detailed_view_filters = [];
+ if (this.active_view === "Detailed") {
+ detailed_view_filters = this.filter_group.get_filters();
+ }
+
+ // refresh tabs
+ this.TABS.forEach(tab => {
+ if (!tab.shown) return;
+ this.tabs[`${tab.name}_tab`].tabmanager.refresh_view(
+ this.active_view,
+ this.filter_category,
+ detailed_view_filters
+ );
+ });
+ }
+
+ // RENDER
+
+ render() {
+ this.render_tab_group();
+ this.render_indicator();
+ this.setup_filter_button();
+ this.render_view_groups();
+ this.render_tabs();
+ this.setup_detail_view_listener();
+ }
+
+ render_tab_group() {
+ const tab_fields = this.TABS.reduce(
+ (acc, tab) => [
+ ...acc,
+ {
+ fieldtype: "Tab Break",
+ fieldname: `${tab.name}_tab`,
+ label: __(tab.label),
+ active: tab.is_active ? 1 : 0,
+ depends_on: tab.depends_on,
+ },
+ {
+ fieldtype: "HTML",
+ fieldname: `${tab.name}_html`,
+ },
+ ],
+ []
+ );
+
+ this.tab_group = new frappe.ui.FieldGroup({
+ fields: [
+ {
+ //hack: for the FieldGroup(Layout) to avoid rendering default "Detailed" tab
+ fieldtype: "Section Break",
+ },
+ ...tab_fields,
+ ],
+ body: this.$wrapper,
+ frm: this.frm,
+ });
+ this.tab_group.make();
+
+ // make tabs_dict for easy access
+ this.tabs = Object.fromEntries(
+ this.tab_group.tabs.map(tab => [tab.df.fieldname, tab])
+ );
+
+ // Fix css
+ this.$wrapper.find(".form-tabs-list").append(``);
+
+ // Remove padding around data table
+ this.$wrapper.closest(".form-column").css("padding", "0px");
+ this.$wrapper.closest(".row.form-section").css("padding", "0px");
+ }
+
+ render_view_groups() {
+ this.active_view = "Summary";
+ const wrapper = this.$wrapper.find(".tab-actions").find(".custom-button-group");
+
+ this.viewgroup = new india_compliance.ViewGroup({
+ $wrapper: wrapper,
+ view_names: ["Summary", "Detailed"],
+ active_view: this.active_view,
+ parent: this,
+ callback: this.change_view,
+ });
+
+ this.viewgroup.disable_view(
+ "Detailed",
+ "Click on a category from summary to view details"
+ );
+ }
+
+ render_tabs() {
+ this.TABS.forEach(tab => {
+ const wrapper = this.tab_group.get_field(`${tab.name}_html`).$wrapper;
+ this.tabs[`${tab.name}_tab`].tabmanager = new tab._TabManager(
+ this,
+ wrapper,
+ this.show_filtered_category,
+ this.filter_detailed_view
+ );
+ });
+ }
+
+ render_indicator() {
+ if (!this.status) {
+ this.frm.page.clear_indicator();
+ return;
+ }
+
+ const tab_name = this.status === "Filed" ? "Filed" : "File";
+ const color = this.status === "Filed" ? "green" : "orange";
+
+ this.$wrapper.find(`[data-fieldname="filed_tab"]`).html(tab_name);
+ this.frm.page.set_indicator(this.status, color);
+ this.frm.refresh();
+ }
+
+ // SETUP
+
+ setup_filter_button() {
+ this.filter_group = new india_compliance.FilterGroup({
+ doctype: DOCTYPE,
+ parent: this.$wrapper.find(".tab-actions"),
+ filter_options: {
+ fieldname: "description",
+ filter_fields: this.get_category_filter_fields(),
+ },
+ on_change: () => {
+ if (this.is_category_changed) {
+ this.is_category_changed = false;
+ return;
+ }
+
+ this.refresh_view();
+ },
+ });
+
+ this.toggle_filter_selector();
+ }
+
+ setup_detail_view_listener() {
+ const me = this;
+ this.$wrapper.on("click", ".btn.eye.reconcile-row", function (e) {
+ const row_index = $(this).attr("data-row-index");
+ const data = me.data.reconcile[me.filter_category][row_index];
+
+ const category_columns = me.tabs.filed_tab.tabmanager.category_columns;
+ const field_label_map = category_columns.map(col => [
+ col.fieldname,
+ col.name,
+ ]);
+
+ new DetailViewDialog(data, field_label_map);
+ });
+ }
+
+ // UTILS
+
+ show_filtered_category = category => {
+ category = category.trim();
+
+ if (category != this.filter_category) {
+ this.is_category_changed = true;
+ }
+
+ this.filter_category = category;
+
+ if (this.filter_category) this.active_view = "Detailed";
+ else this.active_view = "Summary";
+
+ this.viewgroup.enable_view("Detailed");
+
+ this.refresh_filter_options();
+ this.refresh_view();
+ };
+
+ refresh_filter_options() {
+ const filter_options = this.filter_group.filter_options;
+ this.filter_fields = this.get_category_filter_fields();
+
+ if (!this.filter_fields.length) return;
+
+ filter_options.fieldname = this.filter_fields[0].fieldname;
+ filter_options.filter_fields = this.filter_fields;
+
+ if (this.is_category_changed) {
+ this.filter_group.filter_x_button.click();
+ }
+ }
+
+ get_category_filter_fields() {
+ let fields = [];
+
+ if (
+ [
+ GSTR1_SubCategory.B2B_REGULAR,
+ GSTR1_SubCategory.B2B_REVERSE_CHARGE,
+ GSTR1_SubCategory.SEZWOP,
+ GSTR1_SubCategory.SEZWP,
+ GSTR1_SubCategory.DE,
+ GSTR1_SubCategory.CDNR,
+ ].includes(this.filter_category)
+ ) {
+ fields = [
+ {
+ label: "Customer GSTIN",
+ fieldname: GSTR1_DataField.CUST_GSTIN,
+ fieldtype: "Data",
+ },
+ {
+ label: "Reverse Charge",
+ fieldname: GSTR1_DataField.REVERSE_CHARGE,
+ fieldtype: "Data",
+ },
+ {
+ label: "Place of Supply",
+ fieldname: GSTR1_DataField.POS,
+ fieldtype: "Data",
+ },
+ ];
+ } else if (
+ [GSTR1_SubCategory.EXPWP, GSTR1_SubCategory.EXPWOP].includes(
+ this.filter_category
+ )
+ ) {
+ fields = [
+ {
+ label: "Port Code",
+ fieldname: GSTR1_DataField.SHIPPING_PORT_CODE,
+ fieldtype: "Data",
+ },
+ ];
+ } else if (
+ [
+ GSTR1_SubCategory.B2CL,
+ GSTR1_SubCategory.B2CS,
+ GSTR1_SubCategory.AT,
+ GSTR1_SubCategory.TXP,
+ GSTR1_SubCategory.CDNUR,
+ ].includes(this.filter_category)
+ ) {
+ fields = [
+ {
+ label: "Place of Supply",
+ fieldname: GSTR1_DataField.POS,
+ fieldtype: "Data",
+ },
+ ];
+ } else if (
+ [GSTR1_SubCategory.NIL_EXEMPT, GSTR1_SubCategory.DOC_ISSUE].includes(
+ this.filter_category
+ )
+ ) {
+ fields = [
+ {
+ label: "Document Type",
+ fieldname: GSTR1_DataField.DOC_TYPE,
+ fieldtype: "Data",
+ },
+ ];
+ } else if (this.filter_category === GSTR1_SubCategory.HSN) {
+ fields = [
+ {
+ label: "HSN Code",
+ fieldname: GSTR1_DataField.HSN_CODE,
+ fieldtype: "Data",
+ },
+ {
+ label: "UOM",
+ fieldname: GSTR1_DataField.UOM,
+ fieldtype: "Data",
+ },
+ ];
+ }
+
+ fields.forEach(field => (field.parent = DOCTYPE));
+ return fields;
+ }
+
+ filter_detailed_view = async (fieldname, value) => {
+ await this.filter_group.push_new_filter([DOCTYPE, fieldname, "=", value]);
+ this.filter_group.apply();
+ };
+
+ show_summary_view = () => {
+ this.viewgroup.set_active_view("Summary");
+ this.change_view("Summary");
+ };
+
+ change_view = target_view => {
+ const current_view = this.active_view;
+
+ if (!this.filter_category && current_view === "Summary") return;
+
+ this.active_view = target_view;
+ this.refresh_view();
+ };
+
+ toggle_filter_selector() {
+ if (this.active_view === "Detailed" && this.filter_fields.length)
+ this.$wrapper.find(".filter-selector").show();
+ else this.$wrapper.find(".filter-selector").hide();
+ }
+
+ async set_output_gst_balances() {
+ //Checks if gst-ledger-difference element is there and removes if already present
+ const gst_liability = await get_net_gst_liability(this.frm);
+
+ if ($(".gst-ledger-difference").length) {
+ $(".gst-ledger-difference").remove();
+ }
+
+ $(function () {
+ $('[data-toggle="tooltip"]').tooltip();
+ });
+
+ const net_transactions = {
+ IGST: gst_liability["total_igst_amount"] || 0,
+ CGST: gst_liability["total_cgst_amount"] || 0,
+ SGST: gst_liability["total_sgst_amount"] || 0,
+ CESS: gst_liability["total_cess_amount"] || 0,
+ };
+
+ const ledger_balance_cards = Object.entries(net_transactions)
+ .map(
+ ([type, net_amount]) => `
+
+
${type} Account
+
+ ${format_currency(net_amount)}
+ `
+ )
+ .join("");
+
+ const gst_liability_html = `
+
+
+ Net Output GST Liability (Credit - Debit)
+
+
+ ${ledger_balance_cards}
+
+
`;
+
+ let element = $('[data-fieldname="data_section"]');
+ element.prepend(gst_liability_html);
+ }
+}
+
+class TabManager {
+ DEFAULT_NO_DATA_MESSAGE = __("No Data");
+ CATEGORY_COLUMNS = {};
+ DEFAULT_SUMMARY = {
+ // description: "",
+ no_of_records: 0,
+ total_taxable_value: 0,
+ total_igst_amount: 0,
+ total_cgst_amount: 0,
+ total_sgst_amount: 0,
+ total_cess_amount: 0,
+ };
+
+ constructor(instance, wrapper, summary_view_callback, detailed_view_callback) {
+ this.DEFAULT_TITLE = "";
+ this.DEFAULT_SUBTITLE = "";
+ this.creation_time_string = "";
+
+ this.instance = instance;
+ this.wrapper = wrapper;
+ this.summary_view_callback = summary_view_callback;
+ this.detailed_view_callback = detailed_view_callback;
+
+ this.reset_data();
+ this.setup_wrapper();
+ this.setup_datatable(wrapper);
+ this.setup_footer(wrapper);
+ }
+
+ reset_data() {
+ this.data = {}; // Raw Data
+ this.filtered_data = {}; // Filtered Data / Detailed View
+ this.summary = {};
+ }
+
+ refresh_data(data, summary_data, status) {
+ this.data = data;
+ this.summary = summary_data;
+ this.status = status;
+ this.remove_tab_custom_buttons();
+ this.setup_actions();
+ this.datatable.refresh(this.summary);
+ this.set_default_title();
+ this.set_creation_time_string();
+ }
+
+ refresh_view(view, category, filters) {
+ if (!category && view === "Detailed") return;
+
+ this.filter_category = category;
+ let subtitle = "";
+
+ if (view === "Detailed") {
+ this.filter_fieldnames = this.instance.filter_fields.map(
+ filter => filter.fieldname
+ );
+
+ const columns_func = this.CATEGORY_COLUMNS[category];
+ if (!columns_func) return;
+
+ this.category_columns = columns_func.call(this);
+ this.setup_datatable(
+ this.wrapper,
+ this.filter_data(this.data[category], filters),
+ this.category_columns
+ );
+ this.set_title(category, null, true);
+ } else if (view === "Summary") {
+ this.setup_datatable(
+ this.wrapper,
+ this.summary,
+ this.get_summary_columns()
+ );
+ subtitle = this.DEFAULT_SUBTITLE;
+ this.set_title(this.DEFAULT_TITLE, subtitle);
+ }
+
+ this.setup_footer(this.wrapper);
+ this.set_creation_time_string();
+ }
+
+ filter_data(data, filters) {
+ if (!data) return [];
+ if (!filters || !filters.length) return data;
+
+ return data.filter(row => {
+ return filters.every(filter =>
+ india_compliance.FILTER_OPERATORS[filter[2]](
+ filter[3] || "",
+ row[filter[1]] || ""
+ )
+ );
+ });
+ }
+
+ // SETUP
+
+ set_title(title, subtitle, with_back_button = false) {
+ if (title) this.wrapper.find(".tab-title-text").text(title);
+ else this.wrapper.find(".tab-title-text").html(" ");
+
+ if (subtitle) this.wrapper.find(".tab-subtitle-text").text(subtitle);
+ else this.wrapper.find(".tab-subtitle-text").html("");
+
+ if (with_back_button) this.wrapper.find(".tab-back-button").show();
+ else this.wrapper.find(".tab-back-button").hide();
+ }
+
+ set_default_title() {
+ this.set_title(this.DEFAULT_TITLE, this.DEFAULT_SUBTITLE);
+ }
+
+ setup_wrapper() {
+ this.wrapper.append(`
+
+
+
+ `);
+
+ this.setup_back_button_listener();
+ }
+
+ setup_back_button_listener() {
+ this.wrapper.find(".tab-back-button").on("click", () => {
+ this.instance.show_summary_view();
+ });
+ }
+
+ setup_datatable(wrapper, data, columns) {
+ const _columns = columns || this.get_summary_columns();
+ const _data = data || [];
+ const treeView = this.instance.active_view === "Summary";
+
+ this.datatable = new india_compliance.DataTableManager({
+ $wrapper: wrapper.find(".data-table"),
+ columns: _columns,
+ data: _data,
+ options: {
+ showTotalRow: true,
+ checkboxColumn: false,
+ treeView: treeView,
+ noDataMessage: this.DEFAULT_NO_DATA_MESSAGE,
+ headerDropdown: [
+ {
+ label: "Collapse All Node",
+ action: () => {
+ this.datatable.datatable.rowmanager.collapseAllNodes();
+ },
+ },
+ {
+ label: "Expand All Node",
+ action: () => {
+ this.datatable.datatable.rowmanager.expandAllNodes();
+ },
+ },
+ ],
+ hooks: {
+ columnTotal: (_, row) => {
+ if (this.instance.active_view !== "Summary") return null;
+
+ if (row.colIndex === 1)
+ return (row.content = "Total Liability");
+
+ const column_field = row.column.fieldname;
+ if (!this.summary) return null;
+
+ const total = this.summary.reduce((acc, row) => {
+ if (row.indent !== 1) return acc;
+ if (
+ row.consider_in_total_taxable_value &&
+ ["no_of_records", "total_taxable_value"].includes(
+ column_field
+ )
+ )
+ acc += row[column_field] || 0;
+ else if (row.consider_in_total_tax)
+ acc += row[column_field] || 0;
+
+ return acc;
+ }, 0);
+
+ return total;
+ },
+ },
+ },
+ no_data_message: __("No data found"),
+ });
+
+ this.setup_datatable_listeners(treeView);
+ }
+
+ setup_datatable_listeners(isSummaryView) {
+ const me = this;
+
+ // Summary View
+ if (isSummaryView) {
+ this.datatable.$datatable.on("click", ".description", async function (e) {
+ e.preventDefault();
+
+ const summary_description = $(this).text();
+ me.summary_view_callback &&
+ me.summary_view_callback(summary_description);
+ });
+ return;
+ }
+
+ // Detailed View
+ this.instance.filter_fields.forEach(field => {
+ this.datatable.$datatable.on("click", `.${field.fieldname}`, function (e) {
+ e.preventDefault();
+
+ const fieldname = field.fieldname;
+ const value = $(this).text();
+ me.detailed_view_callback &&
+ me.detailed_view_callback(fieldname, value);
+ });
+ });
+ }
+
+ setup_footer(wrapper) {
+ const treeView = this.instance.active_view === "Summary";
+ if (!treeView) {
+ $(wrapper).find("[data-action=collapse_all_rows]").hide();
+ $(wrapper).find("[data-action=expand_all_rows]").hide();
+ } else {
+ $(wrapper).find("[data-action=collapse_all_rows]").show();
+ $(wrapper).find("[data-action=expand_all_rows]").hide();
+ }
+
+ this.setup_footer_actions(wrapper);
+ }
+
+ setup_footer_actions(wrapper) {
+ const me = this;
+ ["expand", "collapse"].forEach(action => {
+ $(wrapper).on("click", `.${action}`, function (e) {
+ e.preventDefault();
+ me.datatable.datatable.rowmanager[`${action}AllNodes`]();
+ $(wrapper).find("[data-action=collapse_all_rows]").toggle();
+ $(wrapper).find("[data-action=expand_all_rows]").toggle();
+ });
+ });
+ }
+
+ set_creation_time_string() {
+ const creation_time_string = this.get_creation_time_string();
+ if (!creation_time_string) return;
+
+ if ($(this.wrapper).find(".creation-time").length)
+ $(this.wrapper).find(".creation-time").remove();
+
+ this.wrapper
+ .find(".report-footer")
+ .append(
+ `${creation_time_string}
`
+ );
+ }
+
+ get_creation_time_string() {
+ if (!this.data.creation) return;
+
+ const creation = frappe.utils.to_title_case(
+ frappe.datetime.prettyDate(this.data.creation)
+ );
+
+ return `Created ${creation}`;
+ }
+
+ // UTILS
+
+ add_tab_custom_button(label, action) {
+ let button = this.wrapper.find(
+ `button[data-label="${encodeURIComponent(label)}"]`
+ );
+ if (button.length) return;
+
+ $(`
+
+ `)
+ .appendTo(this.wrapper.find(".custom-button-group"))
+ .on("click", action);
+ }
+
+ remove_tab_custom_buttons() {
+ this.wrapper.find(".custom-button-group").empty();
+ }
+
+ format_summary_table_cell(args) {
+ const isDescriptionCell = args[1]?.id === "description";
+ let value = args[0];
+
+ if (args[1]?._fieldtype === "Currency") value = format_currency(value);
+ else if (args[1]?._fieldtype === "Float") value = format_number(value);
+
+ value =
+ args[2]?.indent == 0
+ ? `${value}`
+ : isDescriptionCell
+ ? `
+ ${value}
+ `
+ : value;
+
+ return value;
+ }
+
+ format_detailed_table_cell(args) {
+ /**
+ * Update fieldname as a class to the cell
+ * and make it clickable.
+ *
+ * This is used to simplify filtering of data
+ */
+ let value = frappe.format(...args);
+
+ if (this.filter_fieldnames.includes(args[1]?.id))
+ value = `
+
+ ${value}
+ `;
+
+ return value;
+ }
+
+ get_icon(value, column, data, icon) {
+ if (!data) return "";
+ return `
+ `;
+ }
+}
+
+class GSTR1_TabManager extends TabManager {
+ // COLUMNS
+ get_summary_columns() {
+ return [
+ {
+ name: "Description",
+ fieldname: "description",
+ width: 300,
+ _value: (...args) => this.format_summary_table_cell(args),
+ },
+ {
+ name: "Total Docs",
+ fieldname: "no_of_records",
+ _fieldtype: "Float",
+ width: 100,
+ align: "center",
+ _value: (...args) => this.format_summary_table_cell(args),
+ },
+ {
+ name: "Taxable Value",
+ fieldname: GSTR1_DataField.TAXABLE_VALUE,
+ _fieldtype: "Float",
+ width: 180,
+
+ _value: (...args) => this.format_summary_table_cell(args),
+ },
+ {
+ name: "IGST",
+ fieldname: GSTR1_DataField.IGST,
+ _fieldtype: "Float",
+ width: 150,
+
+ _value: (...args) => this.format_summary_table_cell(args),
+ },
+ {
+ name: "CGST",
+ fieldname: GSTR1_DataField.CGST,
+ _fieldtype: "Float",
+ width: 150,
+
+ _value: (...args) => this.format_summary_table_cell(args),
+ },
+ {
+ name: "SGST",
+ fieldname: GSTR1_DataField.SGST,
+ _fieldtype: "Float",
+ width: 150,
+
+ _value: (...args) => this.format_summary_table_cell(args),
+ },
+ {
+ name: "CESS",
+ fieldname: GSTR1_DataField.CESS,
+ _fieldtype: "Float",
+ width: 150,
+
+ _value: (...args) => this.format_summary_table_cell(args),
+ },
+ ];
+ }
+
+ get_invoice_columns() {
+ return [
+ ...this.get_detail_view_column(),
+ {
+ name: "Invoice Date",
+ fieldname: GSTR1_DataField.DOC_DATE,
+ fieldtype: "Date",
+ width: 120,
+ },
+ {
+ name: "Invoice Number",
+ fieldname: GSTR1_DataField.DOC_NUMBER,
+ fieldtype: "Link",
+ options: "Sales Invoice",
+ width: 160,
+ },
+ {
+ name: "Customer GSTIN",
+ fieldname: GSTR1_DataField.CUST_GSTIN,
+ width: 160,
+ _value: (...args) => this.format_detailed_table_cell(args),
+ },
+ {
+ name: "Customer Name",
+ fieldname: GSTR1_DataField.CUST_NAME,
+ width: 200,
+ },
+ {
+ name: "Invoice Type",
+ fieldname: GSTR1_DataField.DOC_TYPE,
+ width: 150,
+ },
+ {
+ name: "Reverse Charge",
+ fieldname: GSTR1_DataField.REVERSE_CHARGE,
+ width: 120,
+ _value: (...args) => this.format_detailed_table_cell(args),
+ },
+ ...this.get_match_columns(),
+ ...this.get_tax_columns(),
+ {
+ name: "Invoice Value",
+ fieldname: GSTR1_DataField.DOC_VALUE,
+ fieldtype: "Currency",
+ width: 150,
+ },
+ ];
+ }
+
+ get_export_columns() {
+ return [
+ ...this.get_detail_view_column(),
+ {
+ name: "Invoice Date",
+ fieldname: GSTR1_DataField.DOC_DATE,
+ fieldtype: "Date",
+ width: 120,
+ },
+ {
+ name: "Invoice Number",
+ fieldname: GSTR1_DataField.DOC_NUMBER,
+ fieldtype: "Link",
+ options: "Sales Invoice",
+ width: 160,
+ },
+ {
+ name: "Invoice Type",
+ fieldname: GSTR1_DataField.DOC_TYPE,
+ width: 150,
+ },
+ {
+ name: "Shipping Bill Number",
+ fieldname: GSTR1_DataField.SHIPPING_BILL_NUMBER,
+ width: 150,
+ },
+ {
+ name: "Shipping Bill Date",
+ fieldname: GSTR1_DataField.SHIPPING_BILL_DATE,
+ width: 120,
+ },
+ {
+ name: "Port Code",
+ fieldname: GSTR1_DataField.SHIPPING_PORT_CODE,
+ width: 100,
+ _value: (...args) => this.format_detailed_table_cell(args),
+ },
+ ...this.get_match_columns(),
+ ...this.get_igst_tax_columns(),
+ {
+ name: "Invoice Value",
+ fieldname: GSTR1_DataField.DOC_VALUE,
+ fieldtype: "Currency",
+ width: 150,
+ },
+ ];
+ }
+
+ get_document_columns() {
+ // `Transaction Type` + Invoice Columns with `Document` as title instead of `Invoice`
+ return [
+ ...this.get_detail_view_column(),
+ {
+ name: "Transaction Type",
+ fieldname: GSTR1_DataField.TRANSACTION_TYPE,
+ width: 100,
+ },
+ {
+ name: "Document Date",
+ fieldname: GSTR1_DataField.DOC_DATE,
+ fieldtype: "Date",
+ width: 120,
+ },
+ {
+ name: "Document Number",
+ fieldname: GSTR1_DataField.DOC_NUMBER,
+ fieldtype: "Link",
+ options: "Sales Invoice",
+ width: 160,
+ },
+ {
+ name: "Customer GSTIN",
+ fieldname: GSTR1_DataField.CUST_GSTIN,
+ width: 160,
+ _value: (...args) => this.format_detailed_table_cell(args),
+ },
+ {
+ name: "Customer Name",
+ fieldname: GSTR1_DataField.CUST_NAME,
+ width: 200,
+ },
+ {
+ name: "Document Type",
+ fieldname: GSTR1_DataField.DOC_TYPE,
+ width: 150,
+ },
+ {
+ name: "Reverse Charge",
+ fieldname: GSTR1_DataField.REVERSE_CHARGE,
+ width: 120,
+ _value: (...args) => this.format_detailed_table_cell(args),
+ },
+ ...this.get_match_columns(),
+ ...this.get_tax_columns(),
+ {
+ name: "Document Value",
+ fieldname: GSTR1_DataField.DOC_VALUE,
+ fieldtype: "Currency",
+ width: 150,
+ },
+ ];
+ }
+
+ get_hsn_columns() {
+ return [
+ ...this.get_detail_view_column(),
+ {
+ name: "HSN Code",
+ fieldname: GSTR1_DataField.HSN_CODE,
+ width: 150,
+ _value: (...args) => this.format_detailed_table_cell(args),
+ },
+ {
+ name: "Description",
+ fieldname: GSTR1_DataField.DESCRIPTION,
+ width: 300,
+ },
+ {
+ name: "UOM",
+ fieldname: GSTR1_DataField.UOM,
+ width: 100,
+ _value: (...args) => this.format_detailed_table_cell(args),
+ },
+ ...this.get_match_columns(),
+ {
+ name: "Total Quantity",
+ fieldname: GSTR1_DataField.QUANTITY,
+ fieldtype: "Float",
+ width: 150,
+ },
+ {
+ name: "Tax Rate",
+ fieldname: GSTR1_DataField.TAX_RATE,
+ fieldtype: "Float",
+ width: 100,
+ },
+ {
+ name: "Taxable Value",
+ fieldname: GSTR1_DataField.TAXABLE_VALUE,
+ fieldtype: "Float",
+ width: 150,
+ },
+ {
+ name: "IGST",
+ fieldname: GSTR1_DataField.IGST,
+ fieldtype: "Float",
+ width: 100,
+ },
+ {
+ name: "CGST",
+ fieldname: GSTR1_DataField.CGST,
+ fieldtype: "Float",
+ width: 100,
+ },
+ {
+ name: "SGST",
+ fieldname: GSTR1_DataField.SGST,
+ fieldtype: "Float",
+ width: 100,
+ },
+ {
+ name: "CESS",
+ fieldname: GSTR1_DataField.CESS,
+ fieldtype: "Float",
+ width: 100,
+ },
+ ];
+ }
+
+ get_documents_issued_columns() {
+ return [
+ ...this.get_detail_view_column(),
+ {
+ name: "Document Type",
+ fieldname: GSTR1_DataField.DOC_TYPE,
+ width: 200,
+ _value: (...args) => this.format_detailed_table_cell(args),
+ },
+ ...this.get_match_columns(),
+ {
+ name: "Sr No From",
+ fieldname: GSTR1_DataField.FROM_SR,
+ width: 150,
+ },
+ {
+ name: "Sr No To",
+ fieldname: GSTR1_DataField.TO_SR,
+ width: 150,
+ },
+ {
+ name: "Total Count",
+ fieldname: GSTR1_DataField.TOTAL_COUNT,
+ width: 120,
+ },
+ {
+ name: "Draft Count",
+ fieldname: GSTR1_DataField.DRAFT_COUNT,
+ width: 120,
+ },
+ {
+ name: "Cancelled Count",
+ fieldname: GSTR1_DataField.CANCELLED_COUNT,
+ width: 120,
+ },
+ ];
+ }
+
+ get_advances_received_columns() {
+ return [
+ ...this.get_detail_view_column(),
+ ...this.get_match_columns(),
+ ...this.get_tax_columns(),
+ ];
+ }
+
+ get_advances_adjusted_columns() {
+ return [
+ ...this.get_detail_view_column(),
+ ...this.get_match_columns(),
+ ...this.get_tax_columns(),
+ ];
+ }
+
+ // Common Columns
+
+ get_tax_columns() {
+ return [
+ {
+ name: "Place of Supply",
+ fieldname: GSTR1_DataField.POS,
+ width: 150,
+ _value: (...args) => this.format_detailed_table_cell(args),
+ },
+ {
+ name: "Tax Rate",
+ fieldname: GSTR1_DataField.TAX_RATE,
+ fieldtype: "Float",
+ width: 100,
+ },
+ {
+ name: "Taxable Value",
+ fieldname: GSTR1_DataField.TAXABLE_VALUE,
+ fieldtype: "Float",
+ width: 150,
+ },
+ {
+ name: "IGST",
+ fieldname: GSTR1_DataField.IGST,
+ fieldtype: "Float",
+ width: 100,
+ },
+ {
+ name: "CGST",
+ fieldname: GSTR1_DataField.CGST,
+ fieldtype: "Float",
+ width: 100,
+ },
+ {
+ name: "SGST",
+ fieldname: GSTR1_DataField.SGST,
+ fieldtype: "Float",
+ width: 100,
+ },
+ {
+ name: "CESS",
+ fieldname: GSTR1_DataField.CESS,
+ fieldtype: "Float",
+ width: 100,
+ },
+ ];
+ }
+
+ get_igst_tax_columns(with_pos) {
+ const columns = [];
+
+ if (with_pos)
+ columns.push({
+ name: "Place of Supply",
+ fieldname: GSTR1_DataField.POS,
+ width: 150,
+ _value: (...args) => this.format_detailed_table_cell(args),
+ });
+
+ columns.push(
+ {
+ name: "Tax Rate",
+ fieldname: GSTR1_DataField.TAX_RATE,
+ fieldtype: "Float",
+ width: 100,
+ },
+ {
+ name: "Taxable Value",
+ fieldname: GSTR1_DataField.TAXABLE_VALUE,
+ fieldtype: "Float",
+ width: 150,
+ },
+ {
+ name: "IGST",
+ fieldname: GSTR1_DataField.IGST,
+ fieldtype: "Float",
+ width: 100,
+ },
+ {
+ name: "CESS",
+ fieldname: GSTR1_DataField.CESS,
+ fieldtype: "Float",
+ width: 100,
+ }
+ );
+
+ return columns;
+ }
+
+ get_match_columns() {
+ return [];
+ }
+
+ get_detail_view_column() {
+ return [];
+ }
+}
+
+class BooksTab extends GSTR1_TabManager {
+ CATEGORY_COLUMNS = {
+ // [GSTR1_Categories.NIL_EXEMPT]: this.get_document_columns,
+
+ // SUBCATEGORIES
+ [GSTR1_SubCategory.B2B_REGULAR]: this.get_invoice_columns,
+ [GSTR1_SubCategory.B2B_REVERSE_CHARGE]: this.get_invoice_columns,
+ [GSTR1_SubCategory.SEZWP]: this.get_invoice_columns,
+ [GSTR1_SubCategory.SEZWOP]: this.get_invoice_columns,
+ [GSTR1_SubCategory.DE]: this.get_invoice_columns,
+
+ [GSTR1_SubCategory.EXPWP]: this.get_export_columns,
+ [GSTR1_SubCategory.EXPWOP]: this.get_export_columns,
+
+ [GSTR1_SubCategory.B2CL]: this.get_invoice_columns,
+ [GSTR1_SubCategory.B2CS]: this.get_b2cs_columns,
+
+ [GSTR1_SubCategory.NIL_EXEMPT]: this.get_nil_exempt_columns,
+
+ [GSTR1_SubCategory.CDNR]: this.get_document_columns,
+ [GSTR1_SubCategory.CDNUR]: this.get_document_columns,
+
+ [GSTR1_SubCategory.AT]: this.get_advances_received_columns,
+ [GSTR1_SubCategory.TXP]: this.get_advances_adjusted_columns,
+
+ [GSTR1_SubCategory.HSN]: this.get_hsn_columns,
+
+ [GSTR1_SubCategory.DOC_ISSUE]: this.get_documents_issued_columns,
+ };
+
+ DEFAULT_TITLE = "Summary of Books";
+
+ setup_actions() {
+ this.add_tab_custom_button("Download Excel", () =>
+ this.download_books_as_excel()
+ );
+ this.add_tab_custom_button("Recompute", () => this.recompute_books());
+ }
+
+ filter_data(data, filters) {
+ data = super.filter_data(data, filters);
+ return data.filter(row => row.upload_status !== "Missing in Books");
+ }
+
+ // ACTIONS
+
+ download_books_as_excel() {
+ const url =
+ "india_compliance.gst_india.doctype.gstr_1_beta.gstr_1_export.download_books_as_excel";
+
+ open_url_post(`/api/method/${url}`, {
+ company_gstin: this.instance.frm.doc.company_gstin,
+ month_or_quarter: this.instance.frm.doc.month_or_quarter,
+ year: this.instance.frm.doc.year,
+ });
+ }
+
+ recompute_books() {
+ render_empty_state(this.instance.frm);
+ this.instance.frm.call("recompute_books");
+ }
+
+ // COLUMNS
+
+ get_match_columns() {
+ if (this.status === "Filed") return [];
+ return [
+ {
+ name: "Upload Status",
+ fieldname: GSTR1_DataField.UPLOAD_STATUS,
+ width: 150,
+ },
+ ];
+ }
+
+ get_b2cs_columns() {
+ let columns = this.get_document_columns();
+ columns = columns.filter(
+ col =>
+ ![GSTR1_DataField.CUST_GSTIN, GSTR1_DataField.REVERSE_CHARGE].includes(
+ col.fieldname
+ )
+ );
+
+ return columns;
+ }
+
+ get_nil_exempt_columns() {
+ return [
+ ...this.get_detail_view_column(),
+ {
+ name: "Transaction Type",
+ fieldname: GSTR1_DataField.TRANSACTION_TYPE,
+ width: 100,
+ },
+ {
+ name: "Document Date",
+ fieldname: GSTR1_DataField.DOC_DATE,
+ fieldtype: "Date",
+ width: 120,
+ },
+ {
+ name: "Document Number",
+ fieldname: GSTR1_DataField.DOC_NUMBER,
+ fieldtype: "Link",
+ options: "Sales Invoice",
+ width: 160,
+ },
+ {
+ name: "Customer GSTIN",
+ fieldname: GSTR1_DataField.CUST_GSTIN,
+ width: 160,
+ _value: (...args) => this.format_detailed_table_cell(args),
+ },
+ {
+ name: "Customer Name",
+ fieldname: GSTR1_DataField.CUST_NAME,
+ width: 200,
+ },
+ {
+ name: "Document Type",
+ fieldname: GSTR1_DataField.DOC_TYPE,
+ width: 150,
+ },
+ ...this.get_match_columns(),
+ {
+ name: "Nil-Rated Supplies",
+ fieldname: GSTR1_DataField.NIL_RATED_AMOUNT,
+ fieldtype: "Float",
+ width: 150,
+ },
+ {
+ name: "Exempted Supplies",
+ fieldname: GSTR1_DataField.EXEMPTED_AMOUNT,
+ fieldtype: "Float",
+ width: 150,
+ },
+ {
+ name: "Non-GST Supplies",
+ fieldname: GSTR1_DataField.NON_GST_AMOUNT,
+ fieldtype: "Float",
+ width: 150,
+ },
+ {
+ name: "Document Value",
+ fieldname: GSTR1_DataField.DOC_VALUE,
+ fieldtype: "Currency",
+ width: 150,
+ },
+ ];
+ }
+
+ get_advances_received_columns() {
+ return [
+ {
+ name: "Advance Date",
+ fieldname: GSTR1_DataField.DOC_DATE,
+ fieldtype: "Date",
+ width: 120,
+ },
+ {
+ name: "Payment Entry Number",
+ fieldname: GSTR1_DataField.DOC_NUMBER,
+ fieldtype: "Link",
+ options: "Payment Entry",
+ width: 160,
+ },
+ {
+ name: "Customer Name",
+ fieldname: GSTR1_DataField.CUST_NAME,
+ width: 200,
+ },
+ ...super.get_advances_received_columns(),
+ ];
+ }
+
+ get_advances_adjusted_columns() {
+ return [
+ {
+ name: "Adjustment Date",
+ fieldname: GSTR1_DataField.DOC_DATE,
+ fieldtype: "Date",
+ width: 120,
+ },
+ {
+ name: "Adjustment Entry Number",
+ fieldname: GSTR1_DataField.DOC_NUMBER,
+ fieldtype: "Link",
+ options: "Sales Invoice",
+ width: 160,
+ },
+ {
+ name: "Customer Name",
+ fieldname: GSTR1_DataField.CUST_NAME,
+ width: 200,
+ },
+ ...super.get_advances_adjusted_columns(),
+ ];
+ }
+}
+
+class FiledTab extends GSTR1_TabManager {
+ CATEGORY_COLUMNS = {
+ [GSTR1_SubCategory.B2B_REGULAR]: this.get_invoice_columns,
+ [GSTR1_SubCategory.B2B_REVERSE_CHARGE]: this.get_invoice_columns,
+ [GSTR1_SubCategory.SEZWP]: this.get_invoice_columns,
+ [GSTR1_SubCategory.SEZWOP]: this.get_invoice_columns,
+ [GSTR1_SubCategory.DE]: this.get_invoice_columns,
+
+ [GSTR1_SubCategory.EXPWP]: this.get_export_columns,
+ [GSTR1_SubCategory.EXPWOP]: this.get_export_columns,
+
+ [GSTR1_SubCategory.B2CL]: this.get_b2cl_columns,
+ [GSTR1_SubCategory.B2CS]: this.get_b2cs_columns,
+
+ [GSTR1_SubCategory.NIL_EXEMPT]: this.get_nil_exempt_columns,
+
+ [GSTR1_SubCategory.CDNR]: this.get_document_columns,
+ [GSTR1_SubCategory.CDNUR]: this.get_cdnur_columns,
+
+ [GSTR1_SubCategory.AT]: this.get_advances_received_columns,
+ [GSTR1_SubCategory.TXP]: this.get_advances_adjusted_columns,
+
+ [GSTR1_SubCategory.HSN]: this.get_hsn_columns,
+ [GSTR1_SubCategory.DOC_ISSUE]: this.get_documents_issued_columns,
+ };
+
+ setup_actions() {
+ this.add_tab_custom_button("Download Excel", () =>
+ this.download_filed_as_excel()
+ );
+
+ if (this.status !== "Filed")
+ this.add_tab_custom_button("Download JSON", () =>
+ this.download_filed_json()
+ );
+
+ if (!is_gstr1_api_enabled()) return;
+
+ if (this.status === "Filed")
+ this.add_tab_custom_button("Sync with GSTN", () =>
+ this.sync_with_gstn("filed")
+ );
+ else {
+ this.add_tab_custom_button("Mark as Filed", () => this.mark_as_filed());
+ }
+ }
+
+ set_default_title() {
+ if (this.status === "Filed") this.DEFAULT_TITLE = "Summary of Filed GSTR-1";
+ else this.DEFAULT_TITLE = "Summary of Draft GSTR-1";
+
+ super.set_default_title();
+ }
+
+ // ACTIONS
+
+ download_filed_as_excel() {
+ const url =
+ "india_compliance.gst_india.doctype.gstr_1_beta.gstr_1_export.download_filed_as_excel";
+
+ open_url_post(`/api/method/${url}`, {
+ company_gstin: this.instance.frm.doc.company_gstin,
+ month_or_quarter: this.instance.frm.doc.month_or_quarter,
+ year: this.instance.frm.doc.year,
+ });
+ }
+
+ sync_with_gstn(sync_for) {
+ render_empty_state(this.instance.frm);
+ this.instance.frm.call("sync_with_gstn", { sync_for });
+ }
+
+ download_filed_json() {
+ const me = this;
+ function get_json_data(dialog) {
+ const { include_uploaded, delete_missing } = dialog
+ ? dialog.get_values()
+ : {
+ include_uploaded: true,
+ delete_missing: false,
+ };
+
+ const doc = me.instance.frm.doc;
+
+ frappe.call({
+ method: "india_compliance.gst_india.doctype.gstr_1_beta.gstr_1_export.download_gstr_1_json",
+ args: {
+ company_gstin: doc.company_gstin,
+ year: doc.year,
+ month_or_quarter: doc.month_or_quarter,
+ include_uploaded,
+ delete_missing,
+ },
+ callback: r => {
+ india_compliance.trigger_file_download(
+ JSON.stringify(r.message.data),
+ r.message.filename
+ );
+ dialog && dialog.hide();
+ },
+ });
+ }
+
+ // without API
+ if (!is_gstr1_api_enabled()) {
+ get_json_data();
+ return;
+ }
+
+ // with API
+ const dialog = new frappe.ui.Dialog({
+ title: __("Download JSON"),
+ fields: [
+ {
+ fieldname: "include_uploaded",
+ label: __("Include Already Uploaded (matching) Invoices"),
+ description: __(
+ `This will include invoices already uploaded (and matching)
+ to GSTN (possibly e-Invoices) and overwrite them in GST Portal.
+ This is not recommended if e-Invoice is applicable to you
+ as it will overwrite the e-Invoice data in GST Portal.`
+ ),
+ fieldtype: "Check",
+ },
+ {
+ fieldname: "delete_missing",
+ label: __(
+ "Delete records that are missing in the Books from GST Portal"
+ ),
+ description: __(
+ "This will delete invoices that are not present in ERP but are present in GST Portal."
+ ),
+ fieldtype: "Check",
+ default: 1,
+ },
+ ],
+ primary_action: () => get_json_data(dialog),
+ });
+
+ dialog.show();
+ }
+
+ mark_as_filed() {
+ render_empty_state(this.instance.frm);
+ this.instance.frm
+ .call("mark_as_filed")
+ .then(() => this.instance.frm.trigger("after_save"));
+ }
+
+ // COLUMNS
+
+ get_b2cl_columns() {
+ return [
+ ...this.get_detail_view_column(),
+ {
+ name: "Invoice Date",
+ fieldname: GSTR1_DataField.DOC_DATE,
+ fieldtype: "Date",
+ width: 120,
+ },
+ {
+ name: "Invoice Number",
+ fieldname: GSTR1_DataField.DOC_NUMBER,
+ fieldtype: "Link",
+ options: "Sales Invoice",
+ width: 160,
+ },
+ {
+ name: "Customer Name",
+ fieldname: GSTR1_DataField.CUST_NAME,
+ width: 200,
+ },
+ ...this.get_match_columns(),
+ ...this.get_igst_tax_columns(true),
+ {
+ name: "Invoice Value",
+ fieldname: GSTR1_DataField.DOC_VALUE,
+ fieldtype: "Currency",
+ width: 150,
+ },
+ ];
+ }
+
+ get_b2cs_columns() {
+ return [
+ ...this.get_detail_view_column(),
+ {
+ name: "Invoice Type",
+ fieldname: GSTR1_DataField.DOC_TYPE,
+ width: 100,
+ },
+ ...this.get_tax_columns(),
+ ...this.get_match_columns(),
+ ];
+ }
+
+ get_nil_exempt_columns() {
+ return [
+ ...this.get_detail_view_column(),
+ {
+ name: "Description",
+ fieldname: GSTR1_DataField.DOC_TYPE,
+ width: 200,
+ _value: (...args) => this.format_detailed_table_cell(args),
+ },
+ ...this.get_match_columns(),
+ {
+ name: "Nil-Rated Supplies",
+ fieldname: GSTR1_DataField.NIL_RATED_AMOUNT,
+ fieldtype: "Currency",
+ width: 150,
+ },
+ {
+ name: "Exempted Supplies",
+ fieldname: GSTR1_DataField.EXEMPTED_AMOUNT,
+ fieldtype: "Currency",
+ width: 150,
+ },
+ {
+ name: "Non-GST Supplies",
+ fieldname: GSTR1_DataField.NON_GST_AMOUNT,
+ fieldtype: "Currency",
+ width: 150,
+ },
+ {
+ name: "Total Taxable Value",
+ fieldname: GSTR1_DataField.TAXABLE_VALUE,
+ fieldtype: "Currency",
+ width: 150,
+ },
+ ];
+ }
+
+ get_cdnur_columns() {
+ return [
+ ...this.get_detail_view_column(),
+ {
+ name: "Transaction Type",
+ fieldname: GSTR1_DataField.TRANSACTION_TYPE,
+ width: 100,
+ },
+ {
+ name: "Document Date",
+ fieldname: GSTR1_DataField.DOC_DATE,
+ fieldtype: "Date",
+ width: 120,
+ },
+ {
+ name: "Document Number",
+ fieldname: GSTR1_DataField.DOC_NUMBER,
+ fieldtype: "Link",
+ options: "Sales Invoice",
+ width: 160,
+ },
+ {
+ name: "Customer Name",
+ fieldname: GSTR1_DataField.CUST_NAME,
+ width: 200,
+ },
+ {
+ name: "Document Type",
+ fieldname: GSTR1_DataField.DOC_TYPE,
+ width: 150,
+ },
+ ...this.get_match_columns(),
+ ...this.get_igst_tax_columns(true),
+ {
+ name: "Document Value",
+ fieldname: GSTR1_DataField.DOC_VALUE,
+ fieldtype: "Currency",
+ width: 150,
+ },
+ ];
+ }
+}
+
+class UnfiledTab extends FiledTab {
+ setup_actions() {
+ if (!is_gstr1_api_enabled()) return;
+
+ this.add_tab_custom_button("Sync with GSTN", () =>
+ this.sync_with_gstn("unfiled")
+ );
+ }
+
+ set_default_title() {
+ this.DEFAULT_TITLE = "Summary of Invoices Uploaded on GST Portal";
+ TabManager.prototype.set_default_title.call(this);
+ }
+}
+
+class ReconcileTab extends FiledTab {
+ DEFAULT_NO_DATA_MESSAGE = __("No differences found");
+
+ set_default_title() {
+ if (this.instance.data.status === "Filed")
+ this.DEFAULT_TITLE = "Books vs Filed";
+ else this.DEFAULT_TITLE = "Books vs Unfiled";
+
+ this.DEFAULT_SUBTITLE = "Only differences";
+ TabManager.prototype.set_default_title.call(this);
+ }
+
+ setup_actions() {
+ this.add_tab_custom_button("Download Excel", () =>
+ this.download_reconcile_as_excel()
+ );
+ }
+
+ download_reconcile_as_excel() {
+ const url =
+ "india_compliance.gst_india.doctype.gstr_1_beta.gstr_1_export.download_reconcile_as_excel";
+
+ open_url_post(`/api/method/${url}`, {
+ company_gstin: this.instance.frm.doc.company_gstin,
+ month_or_quarter: this.instance.frm.doc.month_or_quarter,
+ year: this.instance.frm.doc.year,
+ });
+ }
+
+ get_creation_time_string() { } // pass
+
+ get_detail_view_column() {
+ return [
+ {
+ fieldname: "detail_view",
+ fieldtype: "html",
+ width: 60,
+ align: "center",
+ _value: (...args) => this.get_icon(...args, "eye"),
+ },
+ ];
+ }
+
+ get_match_columns() {
+ return [
+ {
+ name: "Match Status",
+ fieldname: "match_status",
+ width: 150,
+ },
+ {
+ name: "Differences",
+ fieldname: "differences",
+ width: 150,
+ },
+ ];
+ }
+}
+
+class DetailViewDialog {
+ CURRENCY_FIELD_MAP = {
+ [GSTR1_DataField.TAXABLE_VALUE]: "Taxable Value",
+ [GSTR1_DataField.IGST]: "IGST",
+ [GSTR1_DataField.CGST]: "CGST",
+ [GSTR1_DataField.SGST]: "SGST",
+ [GSTR1_DataField.CESS]: "CESS",
+ [GSTR1_DataField.DOC_VALUE]: "Invoice Value",
+ };
+
+ IGNORED_FIELDS = [
+ GSTR1_DataField.CUST_NAME,
+ GSTR1_DataField.DOC_NUMBER,
+ GSTR1_DataField.DOC_TYPE,
+ "match_status",
+ GSTR1_DataField.DESCRIPTION,
+ ];
+
+ constructor(data, field_label_map) {
+ this.data = data;
+ this.field_label_map = field_label_map;
+ this.show_dialog();
+ }
+
+ show_dialog() {
+ this.init_dialog();
+ this.render_table();
+ this.dialog.show();
+ }
+
+ init_dialog() {
+ this.dialog = new frappe.ui.Dialog({
+ title: "Detail View",
+ fields: [
+ {
+ fieldtype: "HTML",
+ fieldname: "reconcile_data",
+ },
+ ],
+ });
+ }
+
+ render_table() {
+ const detail_table = this.dialog.fields_dict.reconcile_data;
+ const field_label_map = this.field_label_map.filter(
+ field => !this.IGNORED_FIELDS.includes(field[0])
+ );
+
+ detail_table.html(
+ frappe.render_template("gstr_1_detail_comparision", {
+ data: this.data,
+ fieldname_map: field_label_map,
+ currency_map: this.CURRENCY_FIELD_MAP,
+ })
+ );
+ this._set_value_color(detail_table.$wrapper, this.data);
+ }
+
+ _set_value_color(wrapper, data) {
+ if (!Object.keys(data.gov).length || !Object.keys(data.books).length) return;
+
+ let gov_data = data.gov;
+ let books_data = data.books;
+
+ for (const key in gov_data) {
+ if (gov_data[key] === books_data[key] || key === "description") continue;
+
+ wrapper
+ .find(`[data-label='${key}'], [data-label='${key}']`)
+ .addClass("not-matched");
+ }
+ }
+}
+
+// UTILITY FUNCTIONS
+function is_gstr1_api_enabled() {
+ return (
+ india_compliance.is_api_enabled() &&
+ !gst_settings.sandbox_mode &&
+ gst_settings.compare_gstr_1_data
+ );
+}
+
+function patch_set_indicator(frm) {
+ frm.toolbar.set_indicator = function () { };
+}
+
+async function set_default_company_gstin(frm) {
+ frm.set_value("company_gstin", "");
+
+ const company = frm.doc.company;
+ const { message: gstin_list } = await frappe.call(
+ "india_compliance.gst_india.utils.get_gstin_list",
+ { party: company }
+ );
+
+ if (gstin_list && gstin_list.length) {
+ frm.set_value("company_gstin", gstin_list[0]);
+ }
+}
+
+function set_options_for_year(frm) {
+ const today = new Date();
+ const current_year = today.getFullYear();
+ const start_year = 2017;
+ const year_range = current_year - start_year + 1;
+ let options = Array.from({ length: year_range }, (_, index) => start_year + index);
+ options = options.reverse().map(year => year.toString());
+
+ frm.get_field("year").set_data(options);
+ frm.set_value("year", current_year.toString());
+}
+
+function set_options_for_month_or_quarter(frm) {
+ /**
+ * Set options for Month or Quarter based on the year and current date
+ * 1. If the year is current year, then options are till current month
+ * 2. If the year is 2017, then options are from July to December
+ * 3. Else, options are all months or quarters
+ *
+ * @param {Object} frm
+ */
+
+ const today = new Date();
+ const current_year = String(today.getFullYear());
+ const current_month_idx = today.getMonth();
+ let options;
+
+ if (!frm.doc.year) frm.doc.year = current_year;
+
+ if (frm.doc.year === current_year) {
+ // Options for current year till current month
+ if (frm.filing_frequency === "Monthly")
+ options = india_compliance.MONTH.slice(0, current_month_idx + 1);
+ else {
+ let quarter_idx;
+ if (current_month_idx <= 2) quarter_idx = 1;
+ else if (current_month_idx <= 5) quarter_idx = 2;
+ else if (current_month_idx <= 8) quarter_idx = 3;
+ else quarter_idx = 4;
+
+ options = india_compliance.QUARTER.slice(0, quarter_idx);
+ }
+ } else if (frm.doc.year === "2017") {
+ // Options for 2017 from July to December
+ if (frm.filing_frequency === "Monthly")
+ options = india_compliance.MONTH.slice(6);
+ else options = india_compliance.QUARTER.slice(2);
+ } else {
+ if (frm.filing_frequency === "Monthly") options = india_compliance.MONTH;
+ else options = india_compliance.QUARTER;
+ }
+
+ set_field_options("month_or_quarter", options);
+ if (frm.doc.year === current_year)
+ // set second last option as default
+ frm.set_value("month_or_quarter", options[options.length - 2]);
+ // set last option as default
+ else frm.set_value("month_or_quarter", options[options.length - 1]);
+}
+
+function render_empty_state(frm) {
+ if ($(".gst-ledger-difference").length) {
+ $(".gst-ledger-difference").remove();
+ }
+ frm.doc.__onload = null;
+ frm.refresh();
+}
+
+async function get_net_gst_liability(frm) {
+ const response = await frappe.call({
+ method: "india_compliance.gst_india.doctype.gstr_1_beta.gstr_1_beta.get_net_gst_liability",
+ args: {
+ month_or_quarter: frm.doc.month_or_quarter,
+ year: frm.doc.year,
+ company_gstin: frm.doc.company_gstin,
+ company: frm.doc.company,
+ },
+ });
+
+ return response?.message;
+}
diff --git a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.json b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.json
new file mode 100644
index 000000000..87f2d16b4
--- /dev/null
+++ b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.json
@@ -0,0 +1,128 @@
+{
+ "actions": [],
+ "beta": 1,
+ "creation": "2024-03-27 17:59:36.078726",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "form",
+ "company",
+ "column_break_ejve",
+ "company_gstin",
+ "column_break_ldkv",
+ "year",
+ "column_break_qcor",
+ "month_or_quarter",
+ "data_section",
+ "tabs_html",
+ "tabs_empty_state",
+ "tabs_no_data"
+ ],
+ "fields": [
+ {
+ "fieldname": "form",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "fieldname": "company_gstin",
+ "fieldtype": "Autocomplete",
+ "label": "Company GSTIN",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_ejve",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_ldkv",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_qcor",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "year",
+ "fieldtype": "Autocomplete",
+ "label": "Year",
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval: doc.__onload?.data && Object.keys(doc.__onload.data).length",
+ "fieldname": "tabs_html",
+ "fieldtype": "HTML"
+ },
+ {
+ "depends_on": "eval: !doc.__onload?.data",
+ "fieldname": "tabs_empty_state",
+ "fieldtype": "HTML",
+ "options": "\n\t{{ __(\"Generate to view the data\") }}
"
+ },
+ {
+ "depends_on": "eval: doc.__onload?.data && Object.keys(doc.__onload.data).length === 0",
+ "fieldname": "tabs_no_data",
+ "fieldtype": "HTML",
+ "options": "\n\t{{ __(\"No data available for selected filters\") }}
"
+ },
+ {
+ "fieldname": "data_section",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "month_or_quarter",
+ "fieldtype": "Select",
+ "label": "Month/Quarter",
+ "reqd": 1
+ }
+ ],
+ "hide_toolbar": 1,
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2024-05-27 19:30:01.074149",
+ "modified_by": "Administrator",
+ "module": "GST India",
+ "name": "GSTR-1 Beta",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "export": 1,
+ "read": 1,
+ "role": "System Manager",
+ "write": 1
+ },
+ {
+ "create": 1,
+ "export": 1,
+ "read": 1,
+ "role": "Accounts Manager",
+ "write": 1
+ },
+ {
+ "create": 1,
+ "export": 1,
+ "read": 1,
+ "role": "Accounts User",
+ "write": 1
+ },
+ {
+ "create": 1,
+ "export": 1,
+ "read": 1,
+ "role": "Auditor",
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.py b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.py
new file mode 100644
index 000000000..dde610a83
--- /dev/null
+++ b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_beta.py
@@ -0,0 +1,245 @@
+# Copyright (c) 2024, Resilient Tech and contributors
+# For license information, please see license.txt
+
+from datetime import datetime
+
+import frappe
+from frappe import _
+from frappe.desk.form.load import run_onload
+from frappe.model.document import Document
+from frappe.query_builder.functions import Date, Sum
+from frappe.utils import get_last_day, getdate
+
+from india_compliance.gst_india.utils import get_gst_accounts_by_type
+from india_compliance.gst_india.utils.gstin_info import get_gstr_1_return_status
+from india_compliance.gst_india.utils.gstr_utils import request_otp
+
+
+class GSTR1Beta(Document):
+
+ def onload(self):
+ data = getattr(self, "data", None)
+ if data is not None:
+ self.set_onload("data", data)
+
+ @frappe.whitelist()
+ def recompute_books(self):
+ self.validate(recompute_books=True)
+
+ @frappe.whitelist()
+ def sync_with_gstn(self, sync_for):
+ self.validate(sync_for=sync_for)
+
+ @frappe.whitelist()
+ def mark_as_filed(self):
+ period = get_period(self.month_or_quarter, self.year)
+ return_status = get_gstr_1_return_status(
+ self.company, self.company_gstin, period
+ )
+
+ if return_status != "Filed":
+ frappe.msgprint(
+ _("GSTR-1 is not yet filed on the GST Portal"), indicator="red"
+ )
+
+ else:
+ frappe.db.set_value(
+ "GSTR-1 Log",
+ f"{period}-{self.company_gstin}",
+ "filing_status",
+ return_status,
+ )
+
+ self.validate()
+ run_onload(self)
+
+ def validate(self, sync_for=None, recompute_books=False):
+ period = get_period(self.month_or_quarter, self.year)
+
+ # get gstr1 log
+ if log_name := frappe.db.exists("GSTR-1 Log", f"{period}-{self.company_gstin}"):
+
+ gstr1_log = frappe.get_doc("GSTR-1 Log", log_name)
+
+ message = None
+ if gstr1_log.status == "In Progress":
+ message = (
+ "GSTR-1 is being prepared. Please wait for the process to complete."
+ )
+
+ elif gstr1_log.status == "Queued":
+ message = (
+ "GSTR-1 download is queued and could take some time. Please wait"
+ " for the process to complete."
+ )
+
+ if message:
+ frappe.msgprint(_(message), title=_("GSTR-1 Generation In Progress"))
+ return
+
+ else:
+ gstr1_log = frappe.new_doc("GSTR-1 Log")
+ gstr1_log.company = self.company
+ gstr1_log.gstin = self.company_gstin
+ gstr1_log.return_period = period
+ gstr1_log.insert()
+
+ settings = frappe.get_cached_doc("GST Settings")
+
+ if sync_for:
+ gstr1_log.remove_json_for(sync_for)
+
+ if recompute_books:
+ gstr1_log.remove_json_for("books")
+
+ # files are already present
+ if gstr1_log.has_all_files(settings):
+ data = gstr1_log.load_data()
+
+ if data:
+ self.data = data
+ self.data["status"] = gstr1_log.filing_status or "Not Filed"
+ gstr1_log.update_status("Generated")
+ return
+
+ # request OTP
+ if gstr1_log.is_sek_needed(settings) and not settings.is_sek_valid(
+ self.company_gstin
+ ):
+ request_otp(self.company_gstin)
+ self.data = "otp_requested"
+ return
+
+ self.gstr1_log = gstr1_log
+
+ # generate gstr1
+ gstr1_log.update_status("In Progress")
+ frappe.enqueue(self.generate_gstr1, queue="short")
+ frappe.msgprint(_("GSTR-1 is being prepared"), alert=True)
+
+ def generate_gstr1(self):
+ """
+ Try to generate GSTR-1 data. Wrapper for generating GSTR-1 data
+ """
+
+ filters = frappe._dict(
+ company=self.company,
+ company_gstin=self.company_gstin,
+ month_or_quarter=self.month_or_quarter,
+ year=self.year,
+ )
+
+ try:
+ self.gstr1_log.generate_gstr1_data(filters, callback=self.on_generate)
+
+ except Exception as e:
+ self.gstr1_log.update_status("Failed", commit=True)
+
+ frappe.publish_realtime(
+ "gstr1_generation_failed",
+ message={"error": str(e), "filters": filters},
+ user=frappe.session.user,
+ doctype=self.doctype,
+ )
+
+ raise e
+
+ def on_generate(self, data, filters):
+ """
+ Once data is generated, update the status and publish the data
+ """
+ self.gstr1_log.db_set({"generation_status": "Generated", "is_latest_data": 1})
+
+ frappe.publish_realtime(
+ "gstr1_data_prepared",
+ message={"data": data, "filters": filters},
+ user=frappe.session.user,
+ doctype=self.doctype,
+ )
+
+
+####### DATA ######################################################################################
+
+
+@frappe.whitelist()
+def get_net_gst_liability(company, company_gstin, month_or_quarter, year):
+ """
+ Returns the net output balance for the given return period as per ledger entries
+ """
+
+ frappe.has_permission("GSTR-1 Beta", throw=True)
+
+ from_date, to_date = get_gstr_1_from_and_to_date(month_or_quarter, year)
+
+ filters = frappe._dict(
+ {
+ "company": company,
+ "company_gstin": company_gstin,
+ "from_date": from_date,
+ "to_date": to_date,
+ }
+ )
+ accounts = get_gst_accounts_by_type(company, "Output")
+
+ gl_entry = frappe.qb.DocType("GL Entry")
+ gst_ledger = frappe._dict(
+ frappe.qb.from_(gl_entry)
+ .select(gl_entry.account, (Sum(gl_entry.credit) - Sum(gl_entry.debit)))
+ .where(gl_entry.account.isin(list(accounts.values())))
+ .where(gl_entry.company == filters.company)
+ .where(Date(gl_entry.posting_date) >= getdate(filters.from_date))
+ .where(Date(gl_entry.posting_date) <= getdate(filters.to_date))
+ .where(gl_entry.company_gstin == filters.company_gstin)
+ .groupby(gl_entry.account)
+ .run()
+ )
+ net_output_balance = {
+ "total_igst_amount": gst_ledger.get(accounts["igst_account"], 0),
+ "total_cgst_amount": gst_ledger.get(accounts["cgst_account"], 0),
+ "total_sgst_amount": gst_ledger.get(accounts["sgst_account"], 0),
+ "total_cess_amount": gst_ledger.get(accounts["cess_account"], 0)
+ + gst_ledger.get(accounts["cess_non_advol_account"], 0),
+ }
+
+ return net_output_balance
+
+
+####### UTILS ######################################################################################
+
+
+def get_period(month_or_quarter: str, year: str) -> str:
+ """
+ Returns the period in the format MMYYYY
+ as accepted by the GST Portal
+ """
+
+ if "-" in month_or_quarter:
+ # Quarterly
+ last_month = month_or_quarter.split("-")[1]
+ month_number = str(getdate(f"{last_month}-{year}").month).zfill(2)
+
+ else:
+ # Monthly
+ month_number = str(datetime.strptime(month_or_quarter, "%B").month).zfill(2)
+
+ return f"{month_number}{year}"
+
+
+def get_gstr_1_from_and_to_date(month_or_quarter: str, year: str) -> tuple:
+ """
+ Returns the from and to date for the given month or quarter and year
+ This is used to filter the data for the given period in Books
+ """
+
+ filing_frequency = frappe.get_cached_value("GST Settings", None, "filing_frequency")
+
+ if filing_frequency == "Quarterly":
+ start_month, end_month = month_or_quarter.split("-")
+ from_date = getdate(f"{year}-{start_month}-01")
+ to_date = get_last_day(f"{year}-{end_month}-01")
+ else:
+ # Monthly (default)
+ from_date = getdate(f"{year}-{month_or_quarter}-01")
+ to_date = get_last_day(from_date)
+
+ return from_date, to_date
diff --git a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_detail_comparision.html b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_detail_comparision.html
new file mode 100644
index 000000000..0b62e8dfe
--- /dev/null
+++ b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_detail_comparision.html
@@ -0,0 +1,83 @@
+{% var gov = data.gov;
+var books = data.books%}
+
+ {%if data.customer_name%}
+
+
Customer Name:
+
{{ data.customer_name}}
+
+ {% endif %}
+
+ {%if data.document_number%}
+
+
Invoice Number:
+
{{ frappe.utils.get_form_link("Sales Invoice",data.document_number, true)
+ }}
+
+ {% endif %}
+ {%if data.document_type%}
+
+
Invoice Type:
+
{{ data.document_type }}
+
+ {% endif %}
+ {%if data.match_status%}
+
+
Match Status:
+
{{ data.match_status }}
+
+ {% endif %}
+
+
+
+ |
+ Books |
+ GSTR-1 |
+
+
+
+ {% var gov_keys=Object.keys(data.gov);
+ var books_keys=Object.keys(data.books);
+ var field_keys = gov_keys.length ? gov_keys : books_keys
+ %}
+ {% for fieldmap in fieldname_map %}
+
+ {% var column_title = fieldmap[1];
+ var fieldname = fieldmap[0];
+
+ var is_currency_field = currency_map[fieldname] || false;
+
+ if(is_currency_field){
+ if(data.books[fieldname])
+ var book_value=format_currency(data.books[fieldname]);
+ else
+ var book_value = "-"
+ }
+ else{
+ var book_value=data.books[fieldname] || "-"
+ }
+
+ if(is_currency_field){
+ if(data.gov[fieldname])
+ var gov_value=format_currency(data.gov[fieldname]);
+ else
+ var gov_value = "-"
+ }
+ else{
+ var gov_value=data.gov[fieldname] || "-"
+ }
+
+ var gov_value = is_currency_field ? format_currency(data.gov[fieldname] || 0) :
+ (data.gov[fieldname] || "-");
+
+ %}
+
+
+ {{ column_title }} |
+ {{ book_value }} |
+ {{ gov_value }} |
+
+ {% endfor %}
+
+
+
\ No newline at end of file
diff --git a/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_export.py b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_export.py
new file mode 100644
index 000000000..e0f098f30
--- /dev/null
+++ b/india_compliance/gst_india/doctype/gstr_1_beta/gstr_1_export.py
@@ -0,0 +1,2090 @@
+"""
+Export GSTR-1 data to excel or json
+"""
+
+import json
+from datetime import datetime
+from enum import Enum
+
+import frappe
+from frappe import _
+from frappe.utils import getdate
+
+from india_compliance.gst_india.doctype.gstr_1_beta.gstr_1_beta import get_period
+from india_compliance.gst_india.utils.exporter import ExcelExporter
+from india_compliance.gst_india.utils.gstr_1 import (
+ JSON_CATEGORY_EXCEL_CATEGORY_MAPPING,
+ GovExcelField,
+ GovExcelSheetName,
+ GovJsonKey,
+ GSTR1_DataField,
+ GSTR1_ItemField,
+ GSTR1_SubCategory,
+)
+from india_compliance.gst_india.utils.gstr_1.gstr_1_json_map import (
+ convert_to_gov_data_format,
+ get_category_wise_data,
+)
+
+
+class ExcelWidth(Enum):
+ XS = 10
+ SM = 15
+ MD = 20 # Default
+ LG = 25
+ XL = 30
+ XXL = 35
+
+
+CATEGORIES_WITH_ITEMS = {
+ GovJsonKey.B2B.value,
+ GovJsonKey.B2CL.value,
+ GovJsonKey.EXP.value,
+ GovJsonKey.CDNR.value,
+ GovJsonKey.CDNUR.value,
+}
+
+
+class DataProcessor:
+
+ # transform input data to required format
+ FIELD_TRANSFORMATIONS = {}
+
+ def process_data(self, input_data):
+ """
+ Objective:
+
+ 1. Flatten the input data to a list of invoices
+ 2. Format/Transform the data to match the Gov Excel format
+ """
+
+ category_wise_data = get_category_wise_data(input_data)
+ processed_data = {}
+
+ for category, data in category_wise_data.items():
+ if category in CATEGORIES_WITH_ITEMS:
+ data = self.flatten_invoice_items_to_rows(data)
+
+ if self.FIELD_TRANSFORMATIONS:
+ data = [self.apply_transformations(row) for row in data]
+
+ processed_data[category] = data
+
+ return processed_data
+
+ def apply_transformations(self, row):
+ """
+ Apply transformations to row fields
+ """
+ for field, modifier in self.FIELD_TRANSFORMATIONS.items():
+ if field in row:
+ row[field] = modifier(row[field])
+
+ return row
+
+ def flatten_invoice_items_to_rows(self, invoice_list: list | tuple) -> list:
+ """
+ input_data: List of invoices with items
+ output: List of invoices with item values
+
+ Example:
+ input_data = [
+ {
+ "key": "value",
+ "items": [{ "taxable_value": "100" }, { "taxable_value": "200" }]
+ }
+ ]
+
+ output = [
+ {"key": "value", "taxable_value": "100"},
+ {"key": "value", "taxable_value": "200"}
+ ]
+
+ Purpose: Gov Excel format requires each row to have invoice values
+ """
+ return [
+ {**invoice, **item}
+ for invoice in invoice_list
+ for item in invoice[GSTR1_DataField.ITEMS.value]
+ ]
+
+
+class GovExcel(DataProcessor):
+ """
+ Export GSTR-1 data to excel
+
+ Excel generated as per the format of Returns Offline Tool Version V3.1.8
+
+ Returns Offline Tool download link - https://www.gst.gov.in/download/returns
+ """
+
+ AMOUNT_FORMAT = "#,##0.00"
+ DATE_FORMAT = "dd-mmm-yy"
+ PERCENT_FORMAT = "0.00"
+
+ FIELD_TRANSFORMATIONS = {
+ GSTR1_DataField.DIFF_PERCENTAGE.value: lambda value: (
+ value * 100 if value != 0 else None
+ ),
+ GSTR1_DataField.DOC_DATE.value: lambda value: datetime.strptime(
+ value, "%Y-%m-%d"
+ ),
+ GSTR1_DataField.SHIPPING_BILL_DATE.value: lambda value: datetime.strptime(
+ value, "%Y-%m-%d"
+ ),
+ }
+
+ def generate(self, gstin, period):
+ """
+ Build excel file
+ """
+ self.gstin = gstin
+ self.period = period
+ gstr_1_log = frappe.get_doc("GSTR-1 Log", f"{period}-{gstin}")
+
+ self.file_field = "filed" if gstr_1_log.filed else "books"
+ data = gstr_1_log.load_data(self.file_field)[self.file_field]
+ data = self.process_data(data)
+ self.build_excel(data)
+
+ def process_data(self, data):
+ data = data.update(data.pop("aggregate_data", {}))
+ category_wise_data = super().process_data(data)
+
+ for category, category_data in category_wise_data.items():
+ # filter missing in books
+ category_wise_data[category] = [
+ row
+ for row in category_data
+ if row.get("upload_status") != "Missing in Books"
+ ]
+
+ if category not in [
+ GovJsonKey.CDNR.value,
+ GovJsonKey.CDNUR.value,
+ GovJsonKey.TXP.value,
+ ]:
+ continue
+
+ # convert to positive values
+ for doc in category_wise_data.get(category, []):
+ if doc.get(GSTR1_DataField.DOC_TYPE.value) == "D":
+ continue
+
+ doc.update(
+ {
+ key: abs(value)
+ for key, value in doc.items()
+ if isinstance(value, (int, float))
+ }
+ )
+
+ return category_wise_data
+
+ def build_excel(self, data):
+ excel = ExcelExporter()
+ for category, cat_data in data.items():
+ excel.create_sheet(
+ sheet_name=JSON_CATEGORY_EXCEL_CATEGORY_MAPPING.get(category, category),
+ headers=self.get_category_headers(category),
+ data=cat_data,
+ add_totals=False,
+ default_data_format={"height": 15},
+ )
+
+ excel.remove_sheet("Sheet")
+ excel.export(get_file_name("Gov", self.gstin, self.period))
+
+ def get_category_headers(self, category):
+ return getattr(self, f"get_{category.lower()}_headers")()
+
+ def get_b2b_headers(self):
+ return [
+ {
+ "label": _(GovExcelField.CUST_GSTIN.value),
+ "fieldname": GSTR1_DataField.CUST_GSTIN.value,
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "label": _(GovExcelField.CUST_NAME.value),
+ "fieldname": GSTR1_DataField.CUST_NAME.value,
+ "header_format": {"width": ExcelWidth.XXL.value},
+ },
+ {
+ "label": _(GovExcelField.INVOICE_NUMBER.value),
+ "fieldname": GSTR1_DataField.DOC_NUMBER.value,
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "label": _(GovExcelField.INVOICE_DATE.value),
+ "fieldname": GSTR1_DataField.DOC_DATE.value,
+ "data_format": {"number_format": self.DATE_FORMAT},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.INVOICE_VALUE.value),
+ "fieldname": GSTR1_DataField.DOC_VALUE.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": _(GovExcelField.POS.value),
+ "fieldname": GSTR1_DataField.POS.value,
+ },
+ {
+ "label": _(GovExcelField.REVERSE_CHARGE.value),
+ "fieldname": GSTR1_DataField.REVERSE_CHARGE.value,
+ "data_format": {"horizontal": "center"},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.DIFF_PERCENTAGE.value),
+ "fieldname": GSTR1_DataField.DIFF_PERCENTAGE.value,
+ "data_format": {"number_format": self.PERCENT_FORMAT},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.INVOICE_TYPE.value),
+ "fieldname": GSTR1_DataField.DOC_TYPE.value,
+ },
+ {
+ "label": _(GovExcelField.ECOMMERCE_GSTIN.value),
+ # Ignore value, just keep the column
+ "fieldname": f"_{GSTR1_DataField.ECOMMERCE_GSTIN.value}",
+ },
+ {
+ "label": _(GovExcelField.TAX_RATE.value),
+ "fieldname": GSTR1_DataField.TAX_RATE.value,
+ "data_format": {"number_format": self.PERCENT_FORMAT},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.TAXABLE_VALUE.value),
+ "fieldname": GSTR1_ItemField.TAXABLE_VALUE.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": _(GovExcelField.CESS.value),
+ "fieldname": GSTR1_ItemField.CESS.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ ]
+
+ def get_b2cl_headers(self):
+ return [
+ {
+ "label": _(GovExcelField.INVOICE_NUMBER.value),
+ "fieldname": GSTR1_DataField.DOC_NUMBER.value,
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "label": _(GovExcelField.INVOICE_DATE.value),
+ "fieldname": GSTR1_DataField.DOC_DATE.value,
+ "data_format": {"number_format": self.DATE_FORMAT},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.INVOICE_VALUE.value),
+ "fieldname": GSTR1_DataField.DOC_VALUE.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": _(GovExcelField.POS.value),
+ "fieldname": GSTR1_DataField.POS.value,
+ },
+ {
+ "label": _(GovExcelField.DIFF_PERCENTAGE.value),
+ "fieldname": GSTR1_DataField.DIFF_PERCENTAGE.value,
+ "data_format": {"number_format": self.PERCENT_FORMAT},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.TAX_RATE.value),
+ "fieldname": GSTR1_DataField.TAX_RATE.value,
+ "data_format": {"number_format": self.PERCENT_FORMAT},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.TAXABLE_VALUE.value),
+ "fieldname": GSTR1_ItemField.TAXABLE_VALUE.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": _(GovExcelField.CESS.value),
+ "fieldname": GSTR1_ItemField.CESS.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": _(GovExcelField.ECOMMERCE_GSTIN.value),
+ # Ignore value, just keep the column
+ "fieldname": f"_{GSTR1_DataField.ECOMMERCE_GSTIN.value}",
+ },
+ ]
+
+ def get_b2cs_headers(self):
+ return [
+ {
+ "label": _("Type"),
+ "fieldname": GSTR1_DataField.DOC_TYPE.value,
+ },
+ {
+ "label": _(GovExcelField.POS.value),
+ "fieldname": GSTR1_DataField.POS.value,
+ },
+ {
+ "label": _(GovExcelField.DIFF_PERCENTAGE.value),
+ "fieldname": GSTR1_DataField.DIFF_PERCENTAGE.value,
+ "data_format": {"number_format": self.PERCENT_FORMAT},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.TAX_RATE.value),
+ "fieldname": GSTR1_DataField.TAX_RATE.value,
+ "data_format": {"number_format": self.PERCENT_FORMAT},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.TAXABLE_VALUE.value),
+ "fieldname": GSTR1_DataField.TAXABLE_VALUE.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": _(GovExcelField.CESS.value),
+ "fieldname": GSTR1_DataField.CESS.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": _(GovExcelField.ECOMMERCE_GSTIN.value),
+ # Ignore value, just keep the column
+ "fieldname": f"_{GSTR1_DataField.ECOMMERCE_GSTIN.value}",
+ },
+ ]
+
+ def get_cdnr_headers(self):
+ return [
+ {
+ "label": _(GovExcelField.CUST_GSTIN.value),
+ "fieldname": GSTR1_DataField.CUST_GSTIN.value,
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "label": _(GovExcelField.CUST_NAME.value),
+ "fieldname": GSTR1_DataField.CUST_NAME.value,
+ "header_format": {"width": ExcelWidth.XXL.value},
+ },
+ {
+ "label": _(GovExcelField.NOTE_NO.value),
+ "fieldname": GSTR1_DataField.DOC_NUMBER.value,
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "label": _(GovExcelField.NOTE_DATE.value),
+ "fieldname": GSTR1_DataField.DOC_DATE.value,
+ "data_format": {"number_format": self.DATE_FORMAT},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.NOTE_TYPE.value),
+ "fieldname": GSTR1_DataField.TRANSACTION_TYPE.value,
+ },
+ {
+ "label": _(GovExcelField.POS.value),
+ "fieldname": GSTR1_DataField.POS.value,
+ },
+ {
+ "label": _(GovExcelField.REVERSE_CHARGE.value),
+ "fieldname": GSTR1_DataField.REVERSE_CHARGE.value,
+ "data_format": {"horizontal": "center"},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _("Note Supply Type"),
+ "fieldname": GSTR1_DataField.DOC_TYPE.value,
+ },
+ {
+ "label": _(GovExcelField.NOTE_VALUE.value),
+ "fieldname": GSTR1_DataField.DOC_VALUE.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": _(GovExcelField.DIFF_PERCENTAGE.value),
+ "fieldname": GSTR1_DataField.DIFF_PERCENTAGE.value,
+ "data_format": {"number_format": self.PERCENT_FORMAT},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.TAX_RATE.value),
+ "fieldname": GSTR1_DataField.TAX_RATE.value,
+ "data_format": {"number_format": self.PERCENT_FORMAT},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.TAXABLE_VALUE.value),
+ "fieldname": GSTR1_ItemField.TAXABLE_VALUE.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": _(GovExcelField.CESS.value),
+ "fieldname": GSTR1_ItemField.CESS.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ ]
+
+ def get_cdnur_headers(self):
+ return [
+ {
+ "label": _("UR Type"),
+ "fieldname": GSTR1_DataField.DOC_TYPE.value,
+ },
+ {
+ "label": _(GovExcelField.NOTE_NO.value),
+ "fieldname": GSTR1_DataField.DOC_NUMBER.value,
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "label": _(GovExcelField.NOTE_DATE.value),
+ "fieldname": GSTR1_DataField.DOC_DATE.value,
+ "data_format": {"number_format": self.DATE_FORMAT},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.NOTE_TYPE.value),
+ "fieldname": GSTR1_DataField.TRANSACTION_TYPE.value,
+ },
+ {
+ "label": _(GovExcelField.POS.value),
+ "fieldname": GSTR1_DataField.POS.value,
+ },
+ {
+ "label": _(GovExcelField.NOTE_VALUE.value),
+ "fieldname": GSTR1_DataField.DOC_VALUE.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": _(GovExcelField.DIFF_PERCENTAGE.value),
+ "fieldname": GSTR1_DataField.DIFF_PERCENTAGE.value,
+ "data_format": {"number_format": self.PERCENT_FORMAT},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.TAX_RATE.value),
+ "fieldname": GSTR1_DataField.TAX_RATE.value,
+ "data_format": {"number_format": self.PERCENT_FORMAT},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.TAXABLE_VALUE.value),
+ "fieldname": GSTR1_ItemField.TAXABLE_VALUE.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": _(GovExcelField.CESS.value),
+ "fieldname": GSTR1_ItemField.CESS.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ ]
+
+ def get_exp_headers(self):
+ return [
+ {
+ "label": _("Export Type"),
+ "fieldname": GSTR1_DataField.DOC_TYPE.value,
+ },
+ {
+ "label": _(GovExcelField.INVOICE_NUMBER.value),
+ "fieldname": GSTR1_DataField.DOC_NUMBER.value,
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "label": _(GovExcelField.INVOICE_DATE.value),
+ "fieldname": GSTR1_DataField.DOC_DATE.value,
+ "data_format": {"number_format": self.DATE_FORMAT},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.INVOICE_VALUE.value),
+ "fieldname": GSTR1_DataField.DOC_VALUE.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": _(GovExcelField.PORT_CODE.value),
+ "fieldname": GSTR1_DataField.SHIPPING_PORT_CODE.value,
+ },
+ {
+ "label": _(GovExcelField.SHIPPING_BILL_NO.value),
+ "fieldname": GSTR1_DataField.SHIPPING_BILL_NUMBER.value,
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "label": _(GovExcelField.SHIPPING_BILL_DATE.value),
+ "fieldname": GSTR1_DataField.SHIPPING_BILL_DATE.value,
+ "data_format": {"number_format": self.DATE_FORMAT},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.TAX_RATE.value),
+ "fieldname": GSTR1_DataField.TAX_RATE.value,
+ "data_format": {"number_format": self.PERCENT_FORMAT},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.TAXABLE_VALUE.value),
+ "fieldname": GSTR1_ItemField.TAXABLE_VALUE.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": _(GovExcelField.CESS.value),
+ "fieldname": GSTR1_ItemField.CESS.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ ]
+
+ def get_at_headers(self):
+ return [
+ {
+ "label": _(GovExcelField.POS.value),
+ "fieldname": GSTR1_DataField.POS.value,
+ },
+ {
+ "label": _(GovExcelField.DIFF_PERCENTAGE.value),
+ "fieldname": GSTR1_DataField.DIFF_PERCENTAGE.value,
+ "data_format": {
+ "number_format": self.PERCENT_FORMAT,
+ },
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.TAX_RATE.value),
+ "fieldname": GSTR1_DataField.TAX_RATE.value,
+ "data_format": {"number_format": self.PERCENT_FORMAT},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _("Gross Advance Received"),
+ "fieldname": GSTR1_DataField.TAXABLE_VALUE.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": _(GovExcelField.CESS.value),
+ "fieldname": GSTR1_DataField.CESS.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ ]
+
+ def get_txpd_headers(self):
+ return [
+ {
+ "label": _(GovExcelField.POS.value),
+ "fieldname": GSTR1_DataField.POS.value,
+ },
+ {
+ "label": _(GovExcelField.DIFF_PERCENTAGE.value),
+ "fieldname": GSTR1_DataField.DIFF_PERCENTAGE.value,
+ "data_format": {"number_format": self.PERCENT_FORMAT},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.TAX_RATE.value),
+ "fieldname": GSTR1_DataField.TAX_RATE.value,
+ "data_format": {"number_format": self.PERCENT_FORMAT},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _("Gross Advance Adjusted"),
+ "fieldname": GSTR1_DataField.TAXABLE_VALUE.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": _(GovExcelField.CESS.value),
+ "fieldname": GSTR1_DataField.CESS.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ ]
+
+ def get_nil_headers(self):
+ return [
+ {
+ "label": _(GovExcelField.DESCRIPTION.value),
+ "fieldname": GSTR1_DataField.DOC_TYPE.value,
+ "header_format": {"width": ExcelWidth.XXL.value},
+ },
+ {
+ "label": _("Nil Rated Supplies"),
+ "fieldname": GSTR1_DataField.NIL_RATED_AMOUNT.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": _("Exempted(other than nil rated/non GST supply)"),
+ "fieldname": GSTR1_DataField.EXEMPTED_AMOUNT.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": _("Non-GST Supplies"),
+ "fieldname": GSTR1_DataField.NON_GST_AMOUNT.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ ]
+
+ def get_hsn_headers(self):
+ return [
+ {
+ "label": _(GovExcelField.HSN_CODE.value),
+ "fieldname": GSTR1_DataField.HSN_CODE.value,
+ },
+ {
+ "label": _(GovExcelField.DESCRIPTION.value),
+ "fieldname": GSTR1_DataField.DESCRIPTION.value,
+ },
+ {
+ "label": _(GovExcelField.UOM.value),
+ "fieldname": GSTR1_DataField.UOM.value,
+ },
+ {
+ "label": _(GovExcelField.QUANTITY.value),
+ "fieldname": GSTR1_DataField.QUANTITY.value,
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.TOTAL_VALUE.value),
+ "fieldname": GSTR1_DataField.DOC_VALUE.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": _(GovExcelField.TAX_RATE.value),
+ "fieldname": GSTR1_DataField.TAX_RATE.value,
+ "data_format": {"number_format": self.PERCENT_FORMAT},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.TAXABLE_VALUE.value),
+ "fieldname": GSTR1_DataField.TAXABLE_VALUE.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": _(GovExcelField.IGST.value),
+ "fieldname": GSTR1_DataField.IGST.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": _(GovExcelField.CGST.value),
+ "fieldname": GSTR1_DataField.CGST.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": _(GovExcelField.SGST.value),
+ "fieldname": GSTR1_DataField.SGST.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": _(GovExcelField.CESS.value),
+ "fieldname": GSTR1_DataField.CESS.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ ]
+
+ def get_doc_issue_headers(self):
+ return [
+ {
+ "label": _("Nature of Document"),
+ "fieldname": GSTR1_DataField.DOC_TYPE.value,
+ "header_format": {"width": ExcelWidth.XXL.value},
+ },
+ {
+ "label": _("Sr. No. From"),
+ "fieldname": GSTR1_DataField.FROM_SR.value,
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "label": _("Sr. No. To"),
+ "fieldname": GSTR1_DataField.TO_SR.value,
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "label": _("Total Number"),
+ "fieldname": GSTR1_DataField.TOTAL_COUNT.value,
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _("Cancelled"),
+ "fieldname": GSTR1_DataField.CANCELLED_COUNT.value,
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ ]
+
+
+class BooksExcel(DataProcessor):
+ AMOUNT_FORMAT = "#,##0.00"
+ DATE_FORMAT = "dd-mmm-yy"
+ PERCENT_FORMAT = "0.00"
+ DEFAULT_DATA_FORMAT = {"height": 15}
+
+ def __init__(self, company_gstin, month_or_quarter, year):
+ self.company_gstin = company_gstin
+ self.month_or_quarter = month_or_quarter
+ self.year = year
+
+ self.period = get_period(month_or_quarter, year)
+ gstr1_log = frappe.get_doc("GSTR-1 Log", f"{self.period}-{company_gstin}")
+
+ self.data = self.process_data(gstr1_log.load_data("books")["books"])
+
+ def process_data(self, data):
+ category_wise_data = super().process_data(data)
+
+ DOC_ITEM_FIELD_MAP = {
+ GSTR1_DataField.TAXABLE_VALUE.value: GSTR1_ItemField.TAXABLE_VALUE.value,
+ GSTR1_DataField.IGST.value: GSTR1_ItemField.IGST.value,
+ GSTR1_DataField.CGST.value: GSTR1_ItemField.CGST.value,
+ GSTR1_DataField.SGST.value: GSTR1_ItemField.SGST.value,
+ GSTR1_DataField.CESS.value: GSTR1_ItemField.CESS.value,
+ }
+
+ for category, category_data in category_wise_data.items():
+ # filter missing in books
+ category_wise_data[category] = [
+ doc
+ for doc in category_data
+ if doc.get("upload_status") != "Missing in Books"
+ ]
+
+ # copy doc value to item fields
+ if category != GovJsonKey.B2CS.value:
+ continue
+
+ for doc in category_wise_data[category]:
+ for doc_field, item_field in DOC_ITEM_FIELD_MAP.items():
+ doc[item_field] = doc.get(doc_field, 0)
+
+ return category_wise_data
+
+ def export_data(self):
+ excel = ExcelExporter()
+ excel.remove_sheet("Sheet")
+
+ excel.create_sheet(
+ sheet_name="invoices",
+ headers=self.get_document_headers(),
+ data=self.get_document_data(),
+ default_data_format=self.DEFAULT_DATA_FORMAT,
+ add_totals=False,
+ )
+
+ self.create_other_sheets(excel)
+ excel.export(get_file_name("Books", self.company_gstin, self.period))
+
+ def create_other_sheets(self, excel: ExcelExporter):
+ for category in ("NIL_EXEMPT", "HSN", "AT", "TXP", "DOC_ISSUE"):
+ data = self.data.get(GovJsonKey[category].value)
+
+ if not data:
+ continue
+
+ excel.create_sheet(
+ sheet_name=GovExcelSheetName[category].value,
+ headers=getattr(self, f"get_{category.lower()}_headers")(),
+ data=data,
+ default_data_format=self.DEFAULT_DATA_FORMAT,
+ add_totals=False,
+ )
+
+ def get_document_data(self):
+ taxable_inv_categories = [
+ GovJsonKey.B2B.value,
+ GovJsonKey.EXP.value,
+ GovJsonKey.B2CL.value,
+ GovJsonKey.CDNR.value,
+ GovJsonKey.CDNUR.value,
+ GovJsonKey.B2CS.value,
+ ]
+
+ category_data = []
+ for key, values in self.data.items():
+ if key not in taxable_inv_categories:
+ continue
+
+ category_data.extend(values)
+
+ return category_data
+
+ def get_document_headers(self):
+ return [
+ {
+ "label": _("Transaction Type"),
+ "fieldname": GSTR1_DataField.TRANSACTION_TYPE.value,
+ },
+ {
+ "label": _("Document Date"),
+ "fieldname": GSTR1_DataField.DOC_DATE.value,
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _("Document Number"),
+ "fieldname": GSTR1_DataField.DOC_NUMBER.value,
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "label": _("Customer GSTIN"),
+ "fieldname": GSTR1_DataField.CUST_GSTIN.value,
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "label": _("Customer Name"),
+ "fieldname": GSTR1_DataField.CUST_NAME.value,
+ "header_format": {"width": ExcelWidth.XXL.value},
+ },
+ {
+ "label": _("Document Type"),
+ "fieldname": GSTR1_DataField.DOC_TYPE.value,
+ },
+ {
+ "label": _(GovExcelField.SHIPPING_BILL_NO.value),
+ "fieldname": GSTR1_DataField.SHIPPING_BILL_NUMBER.value,
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "label": _(GovExcelField.SHIPPING_BILL_DATE.value),
+ "fieldname": GSTR1_DataField.SHIPPING_BILL_DATE.value,
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.PORT_CODE.value),
+ "fieldname": GSTR1_DataField.SHIPPING_PORT_CODE.value,
+ },
+ {
+ "label": _(GovExcelField.REVERSE_CHARGE.value),
+ "fieldname": GSTR1_DataField.REVERSE_CHARGE.value,
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _("Upload Status"),
+ "fieldname": GSTR1_DataField.UPLOAD_STATUS.value,
+ },
+ {
+ "label": _(GovExcelField.POS.value),
+ "fieldname": GSTR1_DataField.POS.value,
+ },
+ {
+ "label": _("Tax Rate"),
+ "fieldname": GSTR1_ItemField.TAX_RATE.value,
+ "data_format": {"number_format": self.PERCENT_FORMAT},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "fieldname": GSTR1_ItemField.TAXABLE_VALUE.value,
+ "label": _("Taxable Value"),
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "fieldname": GSTR1_ItemField.IGST.value,
+ "label": _("IGST"),
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "fieldname": GSTR1_ItemField.CGST.value,
+ "label": _("CGST"),
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "fieldname": GSTR1_ItemField.SGST.value,
+ "label": _("SGST"),
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "fieldname": GSTR1_ItemField.CESS.value,
+ "label": _("CESS"),
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": _("Document Value"),
+ "fieldname": GSTR1_DataField.DOC_VALUE.value,
+ },
+ ]
+
+ def get_at_headers(self):
+ return [
+ {
+ "label": _("Advance Date"),
+ "fieldname": GSTR1_DataField.DOC_DATE.value,
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _("Payment Entry Number"),
+ "fieldname": GSTR1_DataField.DOC_NUMBER.value,
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "label": _("Customer"),
+ "fieldname": GSTR1_DataField.CUST_NAME.value,
+ "header_format": {"width": ExcelWidth.XXL.value},
+ },
+ {
+ "label": _(GovExcelField.POS.value),
+ "fieldname": GSTR1_DataField.POS.value,
+ },
+ {
+ "label": "Upload Status",
+ "fieldname": GSTR1_DataField.UPLOAD_STATUS.value,
+ },
+ *self.get_amount_headers(),
+ ]
+
+ def get_txp_headers(self):
+ return [
+ {
+ "label": _("Adjustment Date"),
+ "fieldname": GSTR1_DataField.DOC_DATE.value,
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _("Adjustment Entry Number"),
+ "fieldname": GSTR1_DataField.DOC_NUMBER.value,
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "label": _("Customer"),
+ "fieldname": GSTR1_DataField.CUST_NAME.value,
+ "header_format": {"width": ExcelWidth.XXL.value},
+ },
+ {
+ "label": _(GovExcelField.POS.value),
+ "fieldname": GSTR1_DataField.POS.value,
+ },
+ {
+ "label": "Upload Status",
+ "fieldname": GSTR1_DataField.UPLOAD_STATUS.value,
+ },
+ *self.get_amount_headers(),
+ ]
+
+ def get_hsn_headers(self):
+ return [
+ {
+ "label": _("HSN Code"),
+ "fieldname": GSTR1_DataField.HSN_CODE.value,
+ },
+ {
+ "label": _(GovExcelField.DESCRIPTION.value),
+ "fieldname": GSTR1_DataField.DESCRIPTION.value,
+ "header_format": {"width": ExcelWidth.XXL.value},
+ },
+ {
+ "label": _("UOM"),
+ "fieldname": GSTR1_DataField.UOM.value,
+ },
+ {
+ "label": _(GovExcelField.TAX_RATE.value),
+ "fieldname": GSTR1_DataField.TAX_RATE.value,
+ "data_format": {"number_format": self.PERCENT_FORMAT},
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": "Upload Status",
+ "fieldname": GSTR1_DataField.UPLOAD_STATUS.value,
+ },
+ {
+ "label": _(GovExcelField.QUANTITY.value),
+ "fieldname": GSTR1_DataField.QUANTITY.value,
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _(GovExcelField.TOTAL_VALUE.value),
+ "fieldname": GSTR1_DataField.DOC_VALUE.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ *self.get_amount_headers(),
+ ]
+
+ def get_doc_issue_headers(self):
+ return [
+ {
+ "label": _("Document Type"),
+ "fieldname": GSTR1_DataField.DOC_TYPE.value,
+ "header_format": {"width": ExcelWidth.XXL.value},
+ },
+ {
+ "label": "Upload Status",
+ "fieldname": GSTR1_DataField.UPLOAD_STATUS.value,
+ },
+ {
+ "label": _("Sr No From"),
+ "fieldname": GSTR1_DataField.FROM_SR.value,
+ },
+ {
+ "label": _("Sr No To"),
+ "fieldname": GSTR1_DataField.TO_SR.value,
+ },
+ {
+ "label": _("Total Count"),
+ "fieldname": GSTR1_DataField.TOTAL_COUNT.value,
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _("Draft Count"),
+ "fieldname": GSTR1_DataField.DRAFT_COUNT.value,
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": _("Cancelled Count"),
+ "fieldname": GSTR1_DataField.CANCELLED_COUNT.value,
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ ]
+
+ def get_amount_headers(self):
+ return [
+ {
+ "fieldname": GSTR1_DataField.TAXABLE_VALUE.value,
+ "label": _("Taxable Value"),
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "fieldname": GSTR1_DataField.IGST.value,
+ "label": _("IGST"),
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "fieldname": GSTR1_DataField.CGST.value,
+ "label": _("CGST"),
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "fieldname": GSTR1_DataField.SGST.value,
+ "label": _("SGST"),
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "fieldname": GSTR1_DataField.CESS.value,
+ "label": _("CESS"),
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ ]
+
+ def get_nil_exempt_headers(self):
+ return [
+ {
+ "label": "Transaction Type",
+ "fieldname": GSTR1_DataField.TRANSACTION_TYPE.value,
+ },
+ {
+ "label": "Documenrt Date",
+ "fieldname": GSTR1_DataField.DOC_DATE.value,
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "label": "Document Number",
+ "fieldname": GSTR1_DataField.DOC_NUMBER.value,
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "label": "Customer Name",
+ "fieldname": GSTR1_DataField.CUST_NAME.value,
+ "header_format": {"width": ExcelWidth.XXL.value},
+ },
+ {
+ "label": "Document Type",
+ "fieldname": GSTR1_DataField.DOC_TYPE.value,
+ "header_format": {"width": ExcelWidth.XXL.value},
+ },
+ {
+ "label": "Upload Status",
+ "fieldname": GSTR1_DataField.UPLOAD_STATUS.value,
+ },
+ {
+ "label": "Nil Rated Supplies",
+ "fieldname": GSTR1_DataField.NIL_RATED_AMOUNT.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": "Exempted Supplies",
+ "fieldname": GSTR1_DataField.EXEMPTED_AMOUNT.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": "Non-GST Supplies",
+ "fieldname": GSTR1_DataField.NON_GST_AMOUNT.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "label": "Document Value",
+ "fieldname": GSTR1_DataField.DOC_VALUE.value,
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ ]
+
+
+class ReconcileExcel:
+ AMOUNT_FORMAT = "#,##0.00"
+ DATE_FORMAT = "dd-mmm-yy"
+
+ COLOR_PALLATE = frappe._dict(
+ {
+ "dark_gray": "d9d9d9",
+ "light_gray": "f2f2f2",
+ "dark_pink": "e6b9b8",
+ "light_pink": "f2dcdb",
+ "sky_blue": "c6d9f1",
+ "light_blue": "dce6f2",
+ "green": "d7e4bd",
+ "light_green": "ebf1de",
+ }
+ )
+
+ DEFAULT_HEADER_FORMAT = {"bg_color": COLOR_PALLATE.dark_gray}
+ DEFAULT_DATA_FORMAT = {"bg_color": COLOR_PALLATE.light_gray}
+
+ def __init__(self, company_gstin, month_or_quarter, year):
+ self.company_gstin = company_gstin
+ self.month_or_quarter = month_or_quarter
+ self.year = year
+
+ self.period = get_period(month_or_quarter, year)
+ gstr1_log = frappe.get_doc("GSTR-1 Log", f"{self.period}-{company_gstin}")
+
+ self.summary = gstr1_log.load_data("reconcile_summary")["reconcile_summary"]
+ data = gstr1_log.load_data("reconcile")["reconcile"]
+ self.data = get_category_wise_data(data)
+
+ def export_data(self):
+ excel = ExcelExporter()
+ excel.remove_sheet("Sheet")
+
+ excel.create_sheet(
+ sheet_name="reconcile summary",
+ headers=self.get_reconcile_summary_headers(),
+ data=self.get_reconcile_summary_data(),
+ default_data_format=self.DEFAULT_DATA_FORMAT,
+ default_header_format=self.DEFAULT_HEADER_FORMAT,
+ add_totals=False,
+ )
+
+ for category in (
+ "B2B",
+ "EXP",
+ "B2CL",
+ "B2CS",
+ "NIL_EXEMPT",
+ "CDNR",
+ "CDNUR",
+ "AT",
+ "TXP",
+ "HSN",
+ "DOC_ISSUE",
+ ):
+ self.create_sheet(excel, category)
+
+ excel.export(get_file_name("Reconcile", self.company_gstin, self.period))
+
+ def get_reconcile_summary_headers(self):
+ headers = [
+ {
+ "fieldname": GSTR1_DataField.DESCRIPTION.value,
+ "label": _(GovExcelField.DESCRIPTION.value),
+ "header_format": {"width": ExcelWidth.XXL.value},
+ },
+ {
+ "fieldname": GSTR1_DataField.TAXABLE_VALUE.value,
+ "label": _(GovExcelField.TAXABLE_VALUE.value),
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "fieldname": GSTR1_DataField.IGST.value,
+ "label": _("IGST"),
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "fieldname": GSTR1_DataField.CGST.value,
+ "label": _("CGST"),
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "fieldname": GSTR1_DataField.SGST.value,
+ "label": _("SGST"),
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ {
+ "fieldname": GSTR1_DataField.CESS.value,
+ "label": _("CESS"),
+ "data_format": {"number_format": self.AMOUNT_FORMAT},
+ },
+ ]
+ return headers
+
+ def get_reconcile_summary_data(self):
+ excel_data = []
+ for row in self.summary:
+ if row["indent"] == 1:
+ continue
+ excel_data.append(row)
+
+ return excel_data
+
+ def create_sheet(self, excel: ExcelExporter, category):
+ data = self.get_data(category)
+ if not data:
+ return
+
+ category_key = GovJsonKey[category].value
+ merged_headers = getattr(
+ self,
+ f"get_merge_headers_for_{category_key}",
+ self.get_merge_headers,
+ )()
+
+ excel.create_sheet(
+ sheet_name=GovExcelSheetName[category].value,
+ merged_headers=merged_headers,
+ headers=getattr(self, f"get_{category_key}_headers")(),
+ data=data,
+ default_data_format=self.DEFAULT_DATA_FORMAT,
+ default_header_format=self.DEFAULT_HEADER_FORMAT,
+ add_totals=False,
+ )
+
+ def get_data(self, category):
+ data = self.data.get(GovJsonKey[category].value, [])
+ excel_data = []
+
+ for row in data:
+ row_dict = self.get_row_dict(row)
+ excel_data.append(row_dict)
+
+ return excel_data
+
+ def get_merge_headers(self):
+ return frappe._dict(
+ {
+ "Books": [
+ "books_" + GSTR1_DataField.POS.value,
+ "books_" + GSTR1_DataField.CESS.value,
+ ],
+ "GSTR-1": [
+ "gstr_1_" + GSTR1_DataField.POS.value,
+ "gstr_1_" + GSTR1_DataField.CESS.value,
+ ],
+ }
+ )
+
+ def get_merge_headers_for_exp(self):
+ return self.get_merge_headers_for_b2cs()
+
+ def get_merge_headers_for_b2cs(self):
+ return frappe._dict(
+ {
+ "Books": [
+ "books_" + GSTR1_DataField.TAXABLE_VALUE.value,
+ "books_" + GSTR1_DataField.CESS.value,
+ ],
+ "GSTR-1": [
+ "gstr_1_" + GSTR1_DataField.TAXABLE_VALUE.value,
+ "gstr_1_" + GSTR1_DataField.CESS.value,
+ ],
+ }
+ )
+
+ def get_merge_headers_for_nil(self):
+ return frappe._dict(
+ {
+ "Books": [
+ "books_" + GSTR1_DataField.NIL_RATED_AMOUNT.value,
+ "books_" + GSTR1_DataField.TAXABLE_VALUE.value,
+ ],
+ "GSTR-1": [
+ "gstr_1_" + GSTR1_DataField.NIL_RATED_AMOUNT.value,
+ "gstr_1_" + GSTR1_DataField.TAXABLE_VALUE.value,
+ ],
+ }
+ )
+
+ def get_merge_headers_for_doc_issue(self):
+ return frappe._dict(
+ {
+ "Books": [
+ "books_" + GSTR1_DataField.FROM_SR.value,
+ "books_" + GSTR1_DataField.CANCELLED_COUNT.value,
+ ],
+ "GSTR-1": [
+ "gstr_1_" + GSTR1_DataField.FROM_SR.value,
+ "gstr_1_" + GSTR1_DataField.CANCELLED_COUNT.value,
+ ],
+ }
+ )
+
+ def get_merge_headers_for_hsn(self):
+ return frappe._dict(
+ {
+ "Books": [
+ "books_" + GSTR1_DataField.QUANTITY.value,
+ "books_" + GSTR1_DataField.CESS.value,
+ ],
+ "GSTR-1": [
+ "gstr_1_" + GSTR1_DataField.QUANTITY.value,
+ "gstr_1_" + GSTR1_DataField.CESS.value,
+ ],
+ }
+ )
+
+ def get_merge_headers_for_at(self):
+ return self.get_merge_headers_for_b2cs()
+
+ def get_merge_headers_for_txpd(self):
+ return self.get_merge_headers_for_b2cs()
+
+ def get_b2b_headers(self):
+ return [
+ {
+ "fieldname": GSTR1_DataField.DOC_TYPE.value,
+ "label": _("Document Type"),
+ },
+ {
+ "fieldname": GSTR1_DataField.DOC_DATE.value,
+ "label": _("Document Date"),
+ "header_format": {
+ "width": ExcelWidth.XS.value,
+ "number_format": self.DATE_FORMAT,
+ },
+ },
+ {
+ "fieldname": GSTR1_DataField.DOC_NUMBER.value,
+ "label": _("Document No"),
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "fieldname": GSTR1_DataField.CUST_GSTIN.value,
+ "label": _("Customer GSTIN"),
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "fieldname": GSTR1_DataField.CUST_NAME.value,
+ "label": _("Customer Name"),
+ "header_format": {"width": ExcelWidth.XXL.value},
+ },
+ {"fieldname": "match_status", "label": _("Match Status")},
+ *self.get_tax_difference_columns(),
+ *self.get_common_compare_columns(),
+ ]
+
+ def get_b2cl_headers(self):
+ return [
+ {
+ "fieldname": GSTR1_DataField.DOC_TYPE.value,
+ "label": _("Document Type"),
+ },
+ {
+ "fieldname": GSTR1_DataField.DOC_DATE.value,
+ "label": _("Document Date"),
+ "header_format": {
+ "width": ExcelWidth.XS.value,
+ "number_format": self.DATE_FORMAT,
+ },
+ },
+ {
+ "fieldname": GSTR1_DataField.DOC_NUMBER.value,
+ "label": _("Document No"),
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "fieldname": GSTR1_DataField.CUST_NAME.value,
+ "label": _("Customer Name"),
+ "header_format": {"width": ExcelWidth.XXL.value},
+ },
+ {"fieldname": "match_status", "label": _("Match Status")},
+ *self.get_tax_difference_columns(),
+ {
+ "fieldname": "books_" + GSTR1_DataField.POS.value,
+ "label": _(GovExcelField.POS.value),
+ "compare_with": "gstr_1_" + GSTR1_DataField.POS.value,
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_green,
+ },
+ "header_format": {
+ "bg_color": self.COLOR_PALLATE.green,
+ },
+ },
+ *self.get_amount_field_columns(for_books=True, only_igst=True),
+ {
+ "fieldname": "gstr_1_" + GSTR1_DataField.POS.value,
+ "label": _(GovExcelField.POS.value),
+ "compare_with": "books_" + GSTR1_DataField.POS.value,
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_blue,
+ },
+ "header_format": {
+ "bg_color": self.COLOR_PALLATE.sky_blue,
+ },
+ },
+ *self.get_amount_field_columns(for_books=False, only_igst=True),
+ ]
+
+ def get_exp_headers(self):
+ return [
+ {
+ "fieldname": GSTR1_DataField.DOC_TYPE.value,
+ "label": _("Document Type"),
+ },
+ {
+ "fieldname": GSTR1_DataField.DOC_DATE.value,
+ "label": _("Document Date"),
+ "header_format": {
+ "width": ExcelWidth.XS.value,
+ "number_format": self.DATE_FORMAT,
+ },
+ },
+ {
+ "fieldname": GSTR1_DataField.DOC_NUMBER.value,
+ "label": _("Document No"),
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "fieldname": GSTR1_DataField.CUST_NAME.value,
+ "label": _("Customer Name"),
+ "header_format": {"width": ExcelWidth.XXL.value},
+ },
+ {
+ "fieldname": GSTR1_DataField.SHIPPING_BILL_NUMBER.value,
+ "label": _(GovExcelField.SHIPPING_BILL_NO.value),
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "fieldname": GSTR1_DataField.SHIPPING_BILL_DATE.value,
+ "label": _(GovExcelField.SHIPPING_BILL_DATE.value),
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {
+ "fieldname": GSTR1_DataField.SHIPPING_PORT_CODE.value,
+ "label": _("Shipping Port Code"),
+ },
+ {"fieldname": "match_status", "label": _("Match Status")},
+ *self.get_tax_difference_columns(),
+ *self.get_amount_field_columns(for_books=True, only_igst=True),
+ *self.get_amount_field_columns(for_books=False, only_igst=True),
+ ]
+
+ def get_b2cs_headers(self):
+ return [
+ {
+ "fieldname": GSTR1_DataField.DOC_TYPE.value,
+ "label": _("Document Type"),
+ },
+ {
+ "fieldname": GSTR1_DataField.POS.value,
+ "label": _(GovExcelField.POS.value),
+ },
+ {
+ "fieldname": GSTR1_DataField.TAX_RATE.value,
+ "label": _("Tax Rate"),
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {"fieldname": "match_status", "label": _("Match Status")},
+ *self.get_tax_difference_columns(),
+ *self.get_amount_field_columns(for_books=True),
+ *self.get_amount_field_columns(for_books=False),
+ ]
+
+ def get_nil_headers(self):
+ return [
+ {
+ "fieldname": GSTR1_DataField.DOC_TYPE.value,
+ "label": _("Document Type"),
+ "header_format": {"width": ExcelWidth.XXL.value},
+ },
+ {"fieldname": "match_status", "label": _("Match Status")},
+ *self.get_tax_difference_columns(),
+ {
+ "fieldname": "books_" + GSTR1_DataField.NIL_RATED_AMOUNT.value,
+ "label": _("Nil-Rated Supplies"),
+ "compare_with": "gstr_1_" + GSTR1_DataField.NIL_RATED_AMOUNT.value,
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_green,
+ "number_format": self.AMOUNT_FORMAT,
+ },
+ "header_format": {"bg_color": self.COLOR_PALLATE.green},
+ },
+ {
+ "fieldname": "books_" + GSTR1_DataField.EXEMPTED_AMOUNT.value,
+ "label": _("Exempted Supplies"),
+ "compare_with": "gstr_1_" + GSTR1_DataField.EXEMPTED_AMOUNT.value,
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_green,
+ "number_format": self.AMOUNT_FORMAT,
+ },
+ "header_format": {"bg_color": self.COLOR_PALLATE.green},
+ },
+ {
+ "fieldname": "books_" + GSTR1_DataField.NON_GST_AMOUNT.value,
+ "label": _("Non-GST Supplies"),
+ "compare_with": "gstr_1_" + GSTR1_DataField.NON_GST_AMOUNT.value,
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_green,
+ "number_format": self.AMOUNT_FORMAT,
+ },
+ "header_format": {"bg_color": self.COLOR_PALLATE.green},
+ },
+ {
+ "fieldname": "books_" + GSTR1_DataField.TAXABLE_VALUE.value,
+ "label": _(GovExcelField.TAXABLE_VALUE.value),
+ "compare_with": "gstr_1_" + GSTR1_DataField.TAXABLE_VALUE.value,
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_green,
+ "number_format": self.AMOUNT_FORMAT,
+ },
+ "header_format": {"bg_color": self.COLOR_PALLATE.green},
+ },
+ {
+ "fieldname": "gstr_1_" + GSTR1_DataField.NIL_RATED_AMOUNT.value,
+ "label": _("Nil-Rated Supplies"),
+ "compare_with": "books_" + GSTR1_DataField.NIL_RATED_AMOUNT.value,
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_blue,
+ "number_format": self.AMOUNT_FORMAT,
+ },
+ "header_format": {"bg_color": self.COLOR_PALLATE.sky_blue},
+ },
+ {
+ "fieldname": "gstr_1_" + GSTR1_DataField.EXEMPTED_AMOUNT.value,
+ "label": _("Exempted Supplies"),
+ "compare_with": "books_" + GSTR1_DataField.EXEMPTED_AMOUNT.value,
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_blue,
+ "number_format": self.AMOUNT_FORMAT,
+ },
+ "header_format": {"bg_color": self.COLOR_PALLATE.sky_blue},
+ },
+ {
+ "fieldname": "gstr_1_" + GSTR1_DataField.NON_GST_AMOUNT.value,
+ "label": _("Non-GST Supplies"),
+ "compare_with": "books_" + GSTR1_DataField.NON_GST_AMOUNT.value,
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_blue,
+ "number_format": self.AMOUNT_FORMAT,
+ },
+ "header_format": {"bg_color": self.COLOR_PALLATE.sky_blue},
+ },
+ {
+ "fieldname": "gstr_1_" + GSTR1_DataField.TAXABLE_VALUE.value,
+ "label": _(GovExcelField.TAXABLE_VALUE.value),
+ "compare_with": "books_" + GSTR1_DataField.TAXABLE_VALUE.value,
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_blue,
+ "number_format": self.AMOUNT_FORMAT,
+ },
+ "header_format": {"bg_color": self.COLOR_PALLATE.sky_blue},
+ },
+ ]
+
+ def get_cdnr_headers(self):
+ return [
+ {
+ "fieldname": GSTR1_DataField.DOC_TYPE.value,
+ "label": _("Document Type"),
+ },
+ {
+ "fieldname": GSTR1_DataField.DOC_DATE.value,
+ "label": _("Document Date"),
+ "header_format": {
+ "width": ExcelWidth.XS.value,
+ "number_format": self.DATE_FORMAT,
+ },
+ },
+ {
+ "fieldname": GSTR1_DataField.DOC_NUMBER.value,
+ "label": _("Document No"),
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "fieldname": GSTR1_DataField.CUST_GSTIN.value,
+ "label": _("Customer GSTIN"),
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "fieldname": GSTR1_DataField.CUST_NAME.value,
+ "label": _("Customer Name"),
+ "header_format": {"width": ExcelWidth.XXL.value},
+ },
+ {"fieldname": "match_status", "label": _("Match Status")},
+ *self.get_tax_difference_columns(),
+ *self.get_common_compare_columns(),
+ ]
+
+ def get_cdnur_headers(self):
+ return [
+ {
+ "fieldname": GSTR1_DataField.DOC_TYPE.value,
+ "label": _("Document Type"),
+ },
+ {
+ "fieldname": GSTR1_DataField.DOC_DATE.value,
+ "label": _("Document Date"),
+ "header_format": {
+ "width": ExcelWidth.XS.value,
+ "number_format": self.DATE_FORMAT,
+ },
+ },
+ {
+ "fieldname": GSTR1_DataField.DOC_NUMBER.value,
+ "label": _("Document No"),
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "fieldname": GSTR1_DataField.CUST_GSTIN.value,
+ "label": _("Customer GSTIN"),
+ "header_format": {"width": ExcelWidth.SM.value},
+ },
+ {
+ "fieldname": GSTR1_DataField.CUST_NAME.value,
+ "label": _("Customer Name"),
+ "header_format": {"width": ExcelWidth.XXL.value},
+ },
+ {"fieldname": "match_status", "label": _("Match Status")},
+ *self.get_tax_difference_columns(),
+ {
+ "fieldname": "books_" + GSTR1_DataField.POS.value,
+ "label": _(GovExcelField.POS.value),
+ "compare_with": "gstr_1_" + GSTR1_DataField.POS.value,
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_green,
+ },
+ "header_format": {
+ "bg_color": self.COLOR_PALLATE.green,
+ },
+ },
+ *self.get_amount_field_columns(for_books=True, only_igst=True),
+ {
+ "fieldname": "gstr_1_" + GSTR1_DataField.POS.value,
+ "label": _(GovExcelField.POS.value),
+ "compare_with": "books_" + GSTR1_DataField.POS.value,
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_blue,
+ },
+ "header_format": {
+ "bg_color": self.COLOR_PALLATE.sky_blue,
+ },
+ },
+ *self.get_amount_field_columns(for_books=False, only_igst=True),
+ ]
+
+ def get_doc_issue_headers(self):
+ headers = [
+ {
+ "fieldname": GSTR1_DataField.DOC_TYPE.value,
+ "label": _("Document Type"),
+ "header_format": {"width": ExcelWidth.XXL.value},
+ },
+ {
+ "fieldname": "match_status",
+ "label": _("Match Status"),
+ },
+ {
+ "fieldname": "books_" + GSTR1_DataField.FROM_SR.value,
+ "label": _("SR No From"),
+ "compare_with": "gstr_1_" + GSTR1_DataField.FROM_SR.value,
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_green,
+ },
+ "header_format": {
+ "bg_color": self.COLOR_PALLATE.green,
+ },
+ },
+ {
+ "fieldname": "books_" + GSTR1_DataField.TO_SR.value,
+ "label": _("SR No To"),
+ "compare_with": "gstr_1_" + GSTR1_DataField.TO_SR.value,
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_green,
+ },
+ "header_format": {
+ "bg_color": self.COLOR_PALLATE.green,
+ },
+ },
+ {
+ "fieldname": "books_" + GSTR1_DataField.TOTAL_COUNT.value,
+ "label": _("Total Count"),
+ "compare_with": "gstr_1_" + GSTR1_DataField.TOTAL_COUNT.value,
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_green,
+ },
+ "header_format": {
+ "bg_color": self.COLOR_PALLATE.green,
+ "width": ExcelWidth.XS.value,
+ },
+ },
+ {
+ "fieldname": "books_" + GSTR1_DataField.CANCELLED_COUNT.value,
+ "label": _("Cancelled Count"),
+ "compare_with": "gstr_1_" + GSTR1_DataField.CANCELLED_COUNT.value,
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_green,
+ },
+ "header_format": {
+ "bg_color": self.COLOR_PALLATE.green,
+ "width": ExcelWidth.XS.value,
+ },
+ },
+ {
+ "fieldname": "gstr_1_" + GSTR1_DataField.FROM_SR.value,
+ "label": _("Sr No From"),
+ "compare_with": "books_" + GSTR1_DataField.FROM_SR.value,
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_blue,
+ },
+ "header_format": {
+ "bg_color": self.COLOR_PALLATE.sky_blue,
+ },
+ },
+ {
+ "fieldname": "gstr_1_" + GSTR1_DataField.TO_SR.value,
+ "label": _("Sr No To"),
+ "compare_with": "books_" + GSTR1_DataField.TO_SR.value,
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_blue,
+ },
+ "header_format": {
+ "bg_color": self.COLOR_PALLATE.sky_blue,
+ },
+ },
+ {
+ "fieldname": "gstr_1_" + GSTR1_DataField.TOTAL_COUNT.value,
+ "label": _("Total Count"),
+ "compare_with": "books_" + GSTR1_DataField.TOTAL_COUNT.value,
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_blue,
+ },
+ "header_format": {
+ "bg_color": self.COLOR_PALLATE.sky_blue,
+ "width": ExcelWidth.XS.value,
+ },
+ },
+ {
+ "fieldname": "gstr_1_" + GSTR1_DataField.CANCELLED_COUNT.value,
+ "label": _("Cancelled Count"),
+ "compare_with": "books_" + GSTR1_DataField.CANCELLED_COUNT.value,
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_blue,
+ },
+ "header_format": {
+ "bg_color": self.COLOR_PALLATE.sky_blue,
+ "width": ExcelWidth.XS.value,
+ },
+ },
+ ]
+
+ return headers
+
+ def get_hsn_headers(self):
+ headers = [
+ {"fieldname": GSTR1_DataField.HSN_CODE.value, "label": _("HSN Code")},
+ {
+ "fieldname": GSTR1_DataField.DESCRIPTION.value,
+ "label": _("Description"),
+ "header_format": {"width": ExcelWidth.XXL.value},
+ },
+ {
+ "fieldname": GSTR1_DataField.UOM.value,
+ "label": _(GovExcelField.UOM.value),
+ },
+ {
+ "fieldname": GSTR1_DataField.TAX_RATE.value,
+ "label": _(GovExcelField.TAX_RATE.value),
+ "header_format": {"width": ExcelWidth.XS.value},
+ },
+ {"fieldname": "match_status", "label": _("Match Status")},
+ *self.get_tax_difference_columns(),
+ {
+ "fieldname": "books_" + GSTR1_DataField.QUANTITY.value,
+ "label": _("Quantity"),
+ "compare_with": "gstr_1_" + GSTR1_DataField.QUANTITY.value,
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_green,
+ },
+ "header_format": {
+ "bg_color": self.COLOR_PALLATE.green,
+ "width": ExcelWidth.XS.value,
+ },
+ },
+ *self.get_amount_field_columns(for_books=True),
+ {
+ "fieldname": "gstr_1_" + GSTR1_DataField.QUANTITY.value,
+ "label": _("Quantity"),
+ "compare_with": "books_" + GSTR1_DataField.QUANTITY.value,
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_blue,
+ },
+ "header_format": {
+ "bg_color": self.COLOR_PALLATE.sky_blue,
+ "width": ExcelWidth.XS.value,
+ },
+ },
+ *self.get_amount_field_columns(for_books=False),
+ ]
+
+ return headers
+
+ def get_at_headers(self):
+ return [
+ {
+ "fieldname": GSTR1_DataField.POS.value,
+ "label": _("POS"),
+ },
+ {"fieldname": "match_status", "label": _("Match Status")},
+ *self.get_tax_difference_columns(),
+ *self.get_amount_field_columns(for_books=True),
+ *self.get_amount_field_columns(for_books=False),
+ ]
+
+ def get_txpd_headers(self):
+ return self.get_at_headers()
+
+ def get_row_dict(self, row: dict) -> dict:
+ books = row.pop("books", {})
+ gstr_1 = row.pop("gov", {})
+
+ row.update({"books_" + key: value for key, value in books.items()})
+ row.update({"gstr_1_" + key: value for key, value in gstr_1.items()})
+
+ doc_date = row.get(GSTR1_DataField.DOC_DATE.value)
+ row[GSTR1_DataField.DOC_DATE.value] = getdate(doc_date) if doc_date else ""
+
+ self.update_differences(row)
+
+ return row
+
+ def update_differences(self, row_dict):
+ taxable_value_key = GSTR1_DataField.TAXABLE_VALUE.value
+ igst_key = GSTR1_DataField.IGST.value
+ cgst_key = GSTR1_DataField.CGST.value
+ sgst_key = GSTR1_DataField.SGST.value
+ cess_key = GSTR1_DataField.CESS.value
+
+ row_dict["taxable_value_difference"] = (
+ row_dict.get("books_" + taxable_value_key, 0)
+ ) - (row_dict.get("gstr_1_" + taxable_value_key, 0))
+
+ row_dict["tax_difference"] = 0
+ for tax_key in [igst_key, cgst_key, sgst_key, cess_key]:
+ row_dict["tax_difference"] += row_dict.get("books_" + tax_key, 0) - (
+ row_dict.get("gstr_1_" + tax_key, 0)
+ )
+
+ # COMMON COLUMNS
+
+ def get_tax_difference_columns(self):
+ return [
+ {
+ "fieldname": "taxable_value_difference",
+ "label": _("Taxable Value Difference"),
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_pink,
+ "number_format": self.AMOUNT_FORMAT,
+ },
+ "header_format": {
+ "bg_color": self.COLOR_PALLATE.dark_pink,
+ },
+ },
+ {
+ "fieldname": "tax_difference",
+ "label": _("Tax Difference"),
+ "data_format": {
+ "bg_color": self.COLOR_PALLATE.light_pink,
+ "number_format": self.AMOUNT_FORMAT,
+ },
+ "header_format": {
+ "bg_color": self.COLOR_PALLATE.dark_pink,
+ },
+ },
+ ]
+
+ def get_common_compare_columns(self):
+ return [
+ *self.get_tax_details_columns(for_books=True),
+ *self.get_amount_field_columns(for_books=True),
+ *self.get_tax_details_columns(for_books=False),
+ *self.get_amount_field_columns(for_books=False),
+ ]
+
+ def get_amount_field_columns(self, for_books=True, only_igst=False):
+ if for_books:
+ field_prefix = "books_"
+ compare_with = "gstr_1_"
+ data_format = {
+ "bg_color": self.COLOR_PALLATE.light_green,
+ "number_format": self.AMOUNT_FORMAT,
+ }
+ header_format = {"bg_color": self.COLOR_PALLATE.green}
+
+ else:
+ field_prefix = "gstr_1_"
+ compare_with = "books_"
+ data_format = {
+ "bg_color": self.COLOR_PALLATE.light_blue,
+ "number_format": self.AMOUNT_FORMAT,
+ }
+ header_format = {"bg_color": self.COLOR_PALLATE.sky_blue}
+
+ def get_cgst_sgst_columns():
+ if only_igst:
+ return []
+
+ return [
+ {
+ "fieldname": field_prefix + GSTR1_DataField.CGST.value,
+ "label": _("CGST"),
+ "compare_with": compare_with + GSTR1_DataField.CGST.value,
+ "data_format": data_format,
+ "header_format": header_format,
+ },
+ {
+ "fieldname": field_prefix + GSTR1_DataField.SGST.value,
+ "label": _("SGST"),
+ "compare_with": compare_with + GSTR1_DataField.SGST.value,
+ "data_format": data_format,
+ "header_format": header_format,
+ },
+ ]
+
+ return [
+ {
+ "fieldname": field_prefix + GSTR1_DataField.TAXABLE_VALUE.value,
+ "label": _(GovExcelField.TAXABLE_VALUE.value),
+ "compare_with": compare_with + GSTR1_DataField.TAXABLE_VALUE.value,
+ "data_format": data_format,
+ "header_format": header_format,
+ },
+ {
+ "fieldname": field_prefix + GSTR1_DataField.IGST.value,
+ "label": _("IGST"),
+ "compare_with": compare_with + GSTR1_DataField.IGST.value,
+ "data_format": data_format,
+ "header_format": header_format,
+ },
+ *get_cgst_sgst_columns(),
+ {
+ "fieldname": field_prefix + GSTR1_DataField.CESS.value,
+ "label": _("CESS"),
+ "compare_with": compare_with + GSTR1_DataField.CESS.value,
+ "data_format": data_format,
+ "header_format": header_format,
+ },
+ ]
+
+ def get_tax_details_columns(self, for_books=True):
+ if for_books:
+ field_prefix = "books_"
+ compare_with = "gstr_1_"
+ data_color = self.COLOR_PALLATE.light_green
+ header_color = self.COLOR_PALLATE.green
+
+ else:
+ field_prefix = "gstr_1_"
+ compare_with = "books_"
+ data_color = self.COLOR_PALLATE.light_blue
+ header_color = self.COLOR_PALLATE.sky_blue
+
+ return [
+ {
+ "fieldname": field_prefix + GSTR1_DataField.POS.value,
+ "label": _(GovExcelField.POS.value),
+ "compare_with": compare_with + GSTR1_DataField.POS.value,
+ "data_format": {"bg_color": data_color},
+ "header_format": {"bg_color": header_color},
+ },
+ {
+ "fieldname": field_prefix + GSTR1_DataField.REVERSE_CHARGE.value,
+ "label": _(GovExcelField.REVERSE_CHARGE.value),
+ "compare_with": compare_with + GSTR1_DataField.REVERSE_CHARGE.value,
+ "data_format": {"bg_color": data_color},
+ "header_format": {
+ "bg_color": header_color,
+ "width": ExcelWidth.XS.value,
+ },
+ },
+ ]
+
+
+@frappe.whitelist()
+def download_filed_as_excel(company_gstin, month_or_quarter, year):
+ frappe.has_permission("GSTR-1 Beta", "export", throw=True)
+ GovExcel().generate(company_gstin, get_period(month_or_quarter, year))
+
+
+@frappe.whitelist()
+def download_books_as_excel(company_gstin, month_or_quarter, year):
+ frappe.has_permission("GSTR-1 Beta", "export", throw=True)
+
+ books_excel = BooksExcel(company_gstin, month_or_quarter, year)
+ books_excel.export_data()
+
+
+@frappe.whitelist()
+def download_reconcile_as_excel(company_gstin, month_or_quarter, year):
+ frappe.has_permission("GSTR-1 Beta", "export", throw=True)
+
+ reconcile_excel = ReconcileExcel(company_gstin, month_or_quarter, year)
+ reconcile_excel.export_data()
+
+
+@frappe.whitelist()
+def download_gstr_1_json(
+ company_gstin,
+ year,
+ month_or_quarter,
+ include_uploaded=False,
+ delete_missing=False,
+):
+ frappe.has_permission("GSTR-1 Beta", "export", throw=True)
+
+ if isinstance(include_uploaded, str):
+ include_uploaded = json.loads(include_uploaded)
+
+ if isinstance(delete_missing, str):
+ delete_missing = json.loads(delete_missing)
+
+ period = get_period(month_or_quarter, year)
+ gstr1_log = frappe.get_doc("GSTR-1 Log", f"{period}-{company_gstin}")
+
+ data = gstr1_log.get_json_for("books")
+ data = data.update(data.pop("aggregate_data", {}))
+
+ for subcategory, subcategory_data in data.items():
+ if subcategory in {
+ GSTR1_SubCategory.NIL_EXEMPT.value,
+ GSTR1_SubCategory.HSN.value,
+ }:
+ continue
+
+ discard_invoices = []
+
+ if isinstance(subcategory_data, str):
+ continue
+
+ for key, row in subcategory_data.items():
+ if isinstance(row, list):
+ row = row[0]
+
+ if not row.get("upload_status"):
+ continue
+
+ if row.get("upload_status") == "Uploaded" and not include_uploaded:
+ discard_invoices.append(key)
+ continue
+
+ if row.get("upload_status") == "Missing in Books":
+ if delete_missing:
+ row["flag"] = "D"
+ else:
+ discard_invoices.append(key)
+
+ for key in discard_invoices:
+ subcategory_data.pop(key)
+
+ gstr1_log.normalize_data(data)
+
+ return {
+ "data": {
+ "gstin": company_gstin,
+ "fp": period,
+ **convert_to_gov_data_format(data, company_gstin),
+ },
+ "filename": f"GSTR-1-Gov-{company_gstin}-{period}.json",
+ }
+
+
+def get_file_name(field_name, gstin, period):
+ return f"GSTR-1-{field_name}-{gstin}-{period}"
diff --git a/india_compliance/gst_india/doctype/gstr_1_beta/test_gstr_1_beta.py b/india_compliance/gst_india/doctype/gstr_1_beta/test_gstr_1_beta.py
new file mode 100644
index 000000000..67158cb74
--- /dev/null
+++ b/india_compliance/gst_india/doctype/gstr_1_beta/test_gstr_1_beta.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2024, Resilient Tech and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestGSTR1Beta(FrappeTestCase):
+ pass
diff --git a/india_compliance/gst_india/doctype/gstr_1_log/__init__.py b/india_compliance/gst_india/doctype/gstr_1_log/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/india_compliance/gst_india/doctype/gstr_1_log/gstr_1_log.js b/india_compliance/gst_india/doctype/gstr_1_log/gstr_1_log.js
new file mode 100644
index 000000000..30af20e14
--- /dev/null
+++ b/india_compliance/gst_india/doctype/gstr_1_log/gstr_1_log.js
@@ -0,0 +1,33 @@
+// Copyright (c) 2024, Resilient Tech and contributors
+// For license information, please see license.txt
+
+
+frappe.ui.form.on("GSTR-1 Log", {
+ refresh(frm) {
+ const [month_or_quarter, year] = india_compliance.get_month_year_from_period(frm.doc.return_period);
+
+ frm.add_custom_button(__("View GSTR-1"), () => {
+ frappe.set_route("Form", "GSTR-1 Beta")
+
+ // after form loads
+ new Promise((resolve) => {
+ const interval = setInterval(() => {
+ if (cur_frm.doctype === "GSTR-1 Beta" && cur_frm.__setup_complete) {
+ clearInterval(interval);
+ resolve();
+ }
+ }, 100);
+
+ }).then(async () => {
+ await cur_frm.set_value({
+ "company": frm.doc.company,
+ "company_gstin": frm.doc.gstin,
+ "year": year,
+ "month_or_quarter": month_or_quarter,
+ });
+ cur_frm.save();
+
+ });
+ });
+ },
+});
diff --git a/india_compliance/gst_india/doctype/gstr_1_log/gstr_1_log.json b/india_compliance/gst_india/doctype/gstr_1_log/gstr_1_log.json
new file mode 100644
index 000000000..3955a768e
--- /dev/null
+++ b/india_compliance/gst_india/doctype/gstr_1_log/gstr_1_log.json
@@ -0,0 +1,238 @@
+{
+ "actions": [],
+ "autoname": "format:{return_period}-{gstin}",
+ "creation": "2024-03-11 11:59:06.887429",
+ "description": "Keeps the log of GSTR-1 filed by GSTIN and Period",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "filing_summary_section",
+ "filing_status",
+ "return_period",
+ "company",
+ "column_break_sqwh",
+ "filing_date",
+ "acknowledgement_number",
+ "gstin",
+ "generation_status",
+ "section_break_oisv",
+ "unfiled",
+ "filed",
+ "column_break_hxfu",
+ "unfiled_summary",
+ "filed_summary",
+ "computed_data_section",
+ "is_latest_data",
+ "section_break_emlz",
+ "books",
+ "column_break_ehcm",
+ "books_summary",
+ "reconciled_data_section",
+ "reconcile",
+ "column_break_ndup",
+ "reconcile_summary"
+ ],
+ "fields": [
+ {
+ "fieldname": "filing_summary_section",
+ "fieldtype": "Section Break",
+ "label": "Filing Summary"
+ },
+ {
+ "fieldname": "filing_status",
+ "fieldtype": "Data",
+ "label": "Filing Status",
+ "read_only": 1
+ },
+ {
+ "fieldname": "return_period",
+ "fieldtype": "Data",
+ "label": "Return Period",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_sqwh",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "filing_date",
+ "fieldtype": "Date",
+ "label": "Filing Date",
+ "read_only": 1
+ },
+ {
+ "fieldname": "acknowledgement_number",
+ "fieldtype": "Data",
+ "label": "Acknowledgement Number",
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_oisv",
+ "fieldtype": "Section Break",
+ "label": "Government Data"
+ },
+ {
+ "fieldname": "gstin",
+ "fieldtype": "Data",
+ "label": "GSTIN",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_hxfu",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "computed_data_section",
+ "fieldtype": "Section Break",
+ "hide_border": 1,
+ "label": "Computed Data"
+ },
+ {
+ "fieldname": "reconciled_data_section",
+ "fieldtype": "Section Break",
+ "label": "Reconciled Data"
+ },
+ {
+ "fieldname": "column_break_ndup",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_latest_data",
+ "fieldtype": "Check",
+ "label": "Is Latest Data",
+ "read_only": 1
+ },
+ {
+ "fieldname": "generation_status",
+ "fieldtype": "Select",
+ "hidden": 1,
+ "label": "Generation Status",
+ "options": "\nIn Progress\nGenerated\nFailed",
+ "read_only": 1
+ },
+ {
+ "fieldname": "filed",
+ "fieldtype": "Attach",
+ "label": "Filed Data",
+ "read_only": 1
+ },
+ {
+ "fieldname": "filed_summary",
+ "fieldtype": "Attach",
+ "label": "Filed Summary",
+ "read_only": 1
+ },
+ {
+ "fieldname": "books",
+ "fieldtype": "Attach",
+ "label": "Books Data",
+ "read_only": 1
+ },
+ {
+ "fieldname": "books_summary",
+ "fieldtype": "Attach",
+ "label": "Books Summary",
+ "read_only": 1
+ },
+ {
+ "fieldname": "reconcile",
+ "fieldtype": "Attach",
+ "label": "Reconcile GSTR-1",
+ "read_only": 1
+ },
+ {
+ "fieldname": "reconcile_summary",
+ "fieldtype": "Attach",
+ "label": "Reconcile Summary",
+ "read_only": 1
+ },
+ {
+ "fieldname": "unfiled",
+ "fieldtype": "Attach",
+ "label": "Unfiled Invoices",
+ "read_only": 1
+ },
+ {
+ "fieldname": "unfiled_summary",
+ "fieldtype": "Attach",
+ "label": "Unfiled Summary",
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_emlz",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_ehcm",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "read_only": 1
+ }
+ ],
+ "in_create": 1,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2024-05-28 19:49:55.513493",
+ "modified_by": "Administrator",
+ "module": "GST India",
+ "name": "GSTR-1 Log",
+ "naming_rule": "Expression",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Auditor",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/india_compliance/gst_india/doctype/gstr_1_log/gstr_1_log.py b/india_compliance/gst_india/doctype/gstr_1_log/gstr_1_log.py
new file mode 100644
index 000000000..b9c4f7616
--- /dev/null
+++ b/india_compliance/gst_india/doctype/gstr_1_log/gstr_1_log.py
@@ -0,0 +1,916 @@
+# Copyright (c) 2024, Resilient Tech and contributors
+# For license information, please see license.txt
+import gzip
+import itertools
+from datetime import datetime
+
+import frappe
+from frappe import unscrub
+from frappe.model.document import Document
+from frappe.utils import flt, get_datetime, get_datetime_str, get_last_day, getdate
+
+from india_compliance.gst_india.utils import is_production_api_enabled
+from india_compliance.gst_india.utils.gstr_1 import GSTR1_SubCategory
+from india_compliance.gst_india.utils.gstr_1.__init__ import (
+ CATEGORY_SUB_CATEGORY_MAPPING,
+ SUBCATEGORIES_NOT_CONSIDERED_IN_TOTAL_TAX,
+ SUBCATEGORIES_NOT_CONSIDERED_IN_TOTAL_TAXABLE_VALUE,
+ GSTR1_DataField,
+)
+from india_compliance.gst_india.utils.gstr_1.gstr_1_download import (
+ download_gstr1_json_data,
+)
+from india_compliance.gst_india.utils.gstr_1.gstr_1_json_map import (
+ GSTR1BooksData,
+ summarize_retsum_data,
+)
+from india_compliance.gst_india.utils.gstr_utils import request_otp
+
+
+class SummarizeGSTR1:
+ AMOUNT_FIELDS = {
+ "total_taxable_value": 0,
+ "total_igst_amount": 0,
+ "total_cgst_amount": 0,
+ "total_sgst_amount": 0,
+ "total_cess_amount": 0,
+ }
+
+ def get_summarized_data(self, data, is_filed=False):
+ """
+ Helper function to summarize data for each sub-category
+ """
+ if is_filed and data.get("summary"):
+ return summarize_retsum_data(data.get("summary"))
+
+ subcategory_summary = self.get_subcategory_summary(data)
+
+ return self.get_overall_summary(subcategory_summary)
+
+ def get_overall_summary(self, subcategory_summary):
+ """
+ Summarize data for each category with subcategories
+
+ Steps:
+ 1. Init Category row
+ 2. Summarize category by adding subcategory rows
+ 3. Remove category row if no records
+ 4. Round Values
+ """
+ cateogory_summary = []
+ for category, sub_categories in CATEGORY_SUB_CATEGORY_MAPPING.items():
+ # Init category row
+ category = category.value
+ summary_row = {
+ "description": category,
+ "no_of_records": 0,
+ "indent": 0,
+ **self.AMOUNT_FIELDS,
+ }
+
+ cateogory_summary.append(summary_row)
+ remove_category_row = True
+
+ for subcategory in sub_categories:
+ # update category row
+ subcategory = subcategory.value
+ if subcategory not in subcategory_summary:
+ continue
+
+ subcategory_row = subcategory_summary[subcategory]
+ summary_row["no_of_records"] += subcategory_row["no_of_records"] or 0
+
+ for key in self.AMOUNT_FIELDS:
+ summary_row[key] += subcategory_row[key]
+
+ # add subcategory row
+ cateogory_summary.append(subcategory_row)
+ remove_category_row = False
+
+ if not summary_row["no_of_records"]:
+ summary_row["no_of_records"] = ""
+
+ if remove_category_row:
+ cateogory_summary.remove(summary_row)
+
+ # Round Values
+ for row in cateogory_summary:
+ for key, value in row.items():
+ if isinstance(value, (int, float)):
+ row[key] = flt(value, 2)
+ return cateogory_summary
+
+ def get_subcategory_summary(self, data):
+ """
+ Summarize invoices for each subcategory
+
+ Steps:
+ 1. Init subcategory row
+ 2. Summarize subcategory by adding invoice rows
+ 3. Update no_of_records / count for each subcategory
+ """
+ subcategory_summary = {}
+
+ for subcategory in GSTR1_SubCategory:
+ subcategory = subcategory.value
+ if subcategory not in data:
+ continue
+
+ summary_row = subcategory_summary.setdefault(
+ subcategory, self.default_subcategory_summary(subcategory)
+ )
+
+ _data = data[subcategory]
+ for row in _data:
+ if row.get("upload_status") == "Missing in Books":
+ continue
+
+ for key in self.AMOUNT_FIELDS:
+ summary_row[key] += row.get(key, 0)
+
+ if doc_num := row.get("document_number"):
+ summary_row["unique_records"].add(doc_num)
+
+ elif subcategory == GSTR1_SubCategory.DOC_ISSUE.value:
+ self.count_doc_issue_summary(summary_row, row)
+
+ elif subcategory == GSTR1_SubCategory.HSN.value:
+ self.count_hsn_summary(summary_row)
+
+ for subcategory in subcategory_summary.keys():
+ summary_row = subcategory_summary[subcategory]
+ count = len(summary_row["unique_records"])
+ if count:
+ summary_row["no_of_records"] = count
+
+ summary_row.pop("unique_records")
+
+ return subcategory_summary
+
+ def default_subcategory_summary(self, subcategory):
+ """
+ Considered in total taxable value:
+ Subcategories for which taxable values and counts are considered in front-end
+
+ Considered in total tax:
+ Subcategories for which tax values are considered in front-end
+
+ Indent:
+ 0: Category
+ 1: Subcategory
+ """
+ return {
+ "description": subcategory,
+ "no_of_records": 0,
+ "indent": 1,
+ "consider_in_total_taxable_value": (
+ False
+ if subcategory in SUBCATEGORIES_NOT_CONSIDERED_IN_TOTAL_TAXABLE_VALUE
+ else True
+ ),
+ "consider_in_total_tax": (
+ False
+ if subcategory in SUBCATEGORIES_NOT_CONSIDERED_IN_TOTAL_TAX
+ else True
+ ),
+ "unique_records": set(),
+ **self.AMOUNT_FIELDS,
+ }
+
+ @staticmethod
+ def count_doc_issue_summary(summary_row, data_row):
+ summary_row["no_of_records"] += data_row.get(
+ GSTR1_DataField.TOTAL_COUNT.value, 0
+ ) - data_row.get(GSTR1_DataField.CANCELLED_COUNT.value, 0)
+
+ @staticmethod
+ def count_hsn_summary(summary_row):
+ summary_row["no_of_records"] += 1
+
+
+class ReconcileGSTR1:
+ IGNORED_FIELDS = {GSTR1_DataField.TAX_RATE.value, GSTR1_DataField.DOC_VALUE.value}
+ UNREQUIRED_KEYS = {
+ GSTR1_DataField.TRANSACTION_TYPE.value,
+ GSTR1_DataField.DOC_NUMBER.value,
+ GSTR1_DataField.DOC_DATE.value,
+ GSTR1_DataField.CUST_GSTIN.value,
+ GSTR1_DataField.CUST_NAME.value,
+ GSTR1_DataField.REVERSE_CHARGE.value,
+ }
+
+ def get_reconcile_gstr1_data(self, gov_data, books_data):
+ """
+ This function reconciles the data between Books and Gov Data
+
+ Steps:
+ 1. If already reconciled, return the reconciled data
+ 2. Update Upload Status for Books Data (if return is not filed)
+ 3. Reconcile for each subcategory
+ - For each row in Books Data, compare with Gov Data
+ - For each row in Gov Data (if not in Books Data)
+ """
+ if self.is_latest_data and self.reconcile:
+ reconcile_data = self.get_json_for("reconcile")
+
+ if reconcile_data:
+ return reconcile_data
+
+ reconciled_data = {}
+ if self.filing_status == "Filed":
+ update_books_match = False
+ else:
+ update_books_match = True
+
+ for subcategory in GSTR1_SubCategory:
+ subcategory = subcategory.value
+ books_subdata = books_data.get(subcategory) or {}
+ gov_subdata = gov_data.get(subcategory) or {}
+
+ if not books_subdata and not gov_subdata:
+ continue
+
+ is_list = False # Object Type for the subdata_value
+
+ reconcile_subdata = {}
+
+ # Books vs Gov
+ for key, books_value in books_subdata.items():
+ if not reconcile_subdata:
+ is_list = isinstance(books_value, list)
+
+ gov_value = gov_subdata.get(key)
+
+ reconcile_row = self.get_reconciled_row(books_value, gov_value)
+
+ if reconcile_row:
+ reconcile_subdata[key] = reconcile_row
+
+ if not update_books_match:
+ continue
+
+ books_values = books_value if is_list else [books_value]
+
+ # Update each row in Books Data
+ for row in books_values:
+ if row.get("upload_status") == "Missing in Books":
+ continue
+
+ if not gov_value:
+ row["upload_status"] = "Not Uploaded"
+ continue
+
+ if reconcile_row:
+ row["upload_status"] = "Mismatch"
+ else:
+ row["upload_status"] = "Uploaded"
+
+ # In Gov but not in Books
+ for key, gov_value in gov_subdata.items():
+ if key in books_subdata:
+ continue
+
+ if not reconcile_subdata:
+ is_list = isinstance(gov_value, list)
+
+ reconcile_subdata[key] = self.get_reconciled_row(None, gov_value)
+
+ if not update_books_match:
+ continue
+
+ books_empty_row = self.get_empty_row(
+ gov_value[0] if is_list else gov_value
+ )
+ books_empty_row["upload_status"] = "Missing in Books"
+
+ books_subdata[key] = [books_empty_row] if is_list else books_empty_row
+
+ if update_books_match and not books_data.get(subcategory):
+ books_data[subcategory] = books_subdata
+
+ if reconcile_subdata:
+ reconciled_data[subcategory] = reconcile_subdata
+
+ if update_books_match:
+ self.update_json_for("books", books_data)
+
+ self.update_json_for("reconcile", reconciled_data)
+
+ return reconciled_data
+
+ @staticmethod
+ def get_reconciled_row(books_row, gov_row):
+ """
+ Compare books_row with gov_row and return the difference
+
+ Args:
+ books_row (dict|list): Books Row Data
+ gov_row (dict|list): Gov Row Data
+
+ Returns:
+ dict|list: Reconciled Row Data
+
+ Steps:
+ 1. Get Empty Row with all values as 0
+ 2. Prefer Gov Row if available to compute empty row
+ 3. Compute comparable Gov and Books Row
+ 4. Compare the rows
+ 5. Compute match status and differences
+ 6. Return the reconciled row only if there are differences
+ """
+ is_list = isinstance(gov_row if gov_row else books_row, list)
+
+ # Get Empty Row
+ if is_list:
+ reconcile_row = ReconcileGSTR1.get_empty_row(
+ gov_row[0] if gov_row else books_row[0], ReconcileGSTR1.UNREQUIRED_KEYS
+ )
+ gov_row = gov_row[0] if gov_row else {}
+ books_row = (
+ AggregateInvoices.get_aggregate_invoices(books_row) if books_row else {}
+ )
+
+ else:
+ reconcile_row = ReconcileGSTR1.get_empty_row(gov_row or books_row)
+ gov_row = gov_row or {}
+ books_row = books_row or {}
+
+ # Default Status
+ reconcile_row["match_status"] = "Matched"
+ reconcile_row["differences"] = []
+
+ if not gov_row:
+ reconcile_row["match_status"] = "Missing in GSTR-1"
+
+ if not books_row:
+ reconcile_row["match_status"] = "Missing in Books"
+
+ # Compute Differences
+ for key, value in reconcile_row.items():
+ if (
+ isinstance(value, (int, float))
+ and key not in AggregateInvoices.IGNORED_FIELDS
+ ):
+ reconcile_row[key] = flt(
+ (books_row.get(key) or 0) - (gov_row.get(key) or 0), 2
+ )
+ has_different_value = reconcile_row[key] != 0
+
+ elif key in ("customer_gstin", "place_of_supply"):
+ has_different_value = books_row.get(key) != gov_row.get(key)
+
+ else:
+ continue
+
+ if not has_different_value:
+ continue
+
+ if "Missing" not in reconcile_row["match_status"]:
+ reconcile_row["match_status"] = "Mismatch"
+ reconcile_row["differences"].append(unscrub(key))
+
+ reconcile_row["differences"] = ", ".join(reconcile_row["differences"])
+
+ # Return
+ if reconcile_row["match_status"] == "Matched":
+ return
+
+ reconcile_row["books"] = books_row
+ reconcile_row["gov"] = gov_row
+
+ if is_list:
+ return [reconcile_row]
+
+ return reconcile_row
+
+ @staticmethod
+ def get_empty_row(row: dict, unrequired_keys=None):
+ """
+ Row with all values as 0
+ """
+ empty_row = row.copy()
+
+ for key, value in empty_row.items():
+ if key in AggregateInvoices.IGNORED_FIELDS:
+ continue
+
+ if unrequired_keys and key in unrequired_keys:
+ empty_row[key] = None
+ continue
+
+ if isinstance(value, (int, float)):
+ empty_row[key] = 0
+
+ if key == "items":
+ empty_row[key] = [{}]
+
+ return empty_row
+
+
+class AggregateInvoices:
+ IGNORED_FIELDS = {GSTR1_DataField.TAX_RATE.value, GSTR1_DataField.DOC_VALUE.value}
+
+ @staticmethod
+ def get_aggregate_data(data: dict):
+ """
+ Aggregate invoices for each subcategory where required
+ and updates the data
+ """
+ sub_categories_requiring_aggregation = [
+ GSTR1_SubCategory.B2CS,
+ GSTR1_SubCategory.NIL_EXEMPT,
+ GSTR1_SubCategory.AT,
+ GSTR1_SubCategory.TXP,
+ ]
+
+ aggregate_data = {}
+
+ for subcategory in sub_categories_requiring_aggregation:
+ subcategory_data = data.get(subcategory.value)
+
+ if not subcategory_data:
+ continue
+
+ aggregate_data[subcategory.value] = (
+ AggregateInvoices.get_aggregate_subcategory(subcategory_data)
+ )
+
+ return aggregate_data
+
+ @staticmethod
+ def get_aggregate_subcategory(subcategory_data: dict):
+ value_keys = []
+ aggregate_invoices = {}
+
+ for _id, invoices in subcategory_data.items():
+ if not value_keys:
+ value_keys = AggregateInvoices.get_value_keys(invoices[0])
+
+ aggregate_invoices[_id] = [
+ AggregateInvoices.get_aggregate_invoices(invoices, value_keys)
+ ]
+
+ return aggregate_invoices
+
+ @staticmethod
+ def get_aggregate_invoices(invoices: list, value_keys: list = None) -> dict:
+ """
+ There can be multiple rows in books data for a single row in gov data
+ Aggregate all the rows to a single row
+ """
+ if not value_keys:
+ value_keys = AggregateInvoices.get_value_keys(invoices[0])
+
+ aggregated_invoice = invoices[0].copy()
+ aggregated_invoice.update(
+ {
+ key: sum([invoice.get(key, 0) for invoice in invoices])
+ for key in value_keys
+ }
+ )
+
+ return aggregated_invoice
+
+ @staticmethod
+ def get_value_keys(invoice: dict):
+ keys = []
+
+ for key, value in invoice.items():
+ if not isinstance(value, (int, float)):
+ continue
+
+ if key in AggregateInvoices.IGNORED_FIELDS:
+ continue
+
+ keys.append(key)
+
+ return keys
+
+
+class GenerateGSTR1(SummarizeGSTR1, ReconcileGSTR1, AggregateInvoices):
+ def generate_gstr1_data(self, filters, callback=None):
+ """
+ Generate GSTR-1 Data
+
+ Steps:
+ 1. Check if APIs are enabled. If not, generate only books data.
+ 2. Get the return status
+ 3. Get Gov Data
+ 4. Get Books Data
+ 5. Reconcile Data
+ 6. Summarize Data and return
+ """
+ data = {}
+
+ # APIs Disabled
+ if not self.is_gstr1_api_enabled():
+ return self.generate_only_books_data(data, filters, callback)
+
+ # APIs Enabled
+ status = self.get_return_status()
+
+ if status == "Filed":
+ gov_data_field = "filed"
+ else:
+ gov_data_field = "unfiled"
+
+ # Get Data
+ gov_data, is_enqueued = self.get_gov_gstr1_data()
+
+ if error_type := gov_data.get("error_type"):
+ # otp_requested, invalid_otp
+
+ if error_type == "invalid_otp":
+ request_otp(filters.company_gstin)
+
+ data = "otp_requested"
+ return callback and callback(data, filters)
+
+ books_data = self.get_books_gstr1_data(filters)
+
+ if is_enqueued:
+ return
+
+ reconcile_data = self.get_reconcile_gstr1_data(gov_data, books_data)
+
+ if status != "Filed":
+ books_data.update({"aggregate_data": self.get_aggregate_data(books_data)})
+ self.update_json_for("books", books_data)
+
+ # Compile Data
+ data["status"] = status
+
+ data["reconcile"] = self.normalize_data(reconcile_data)
+ data[gov_data_field] = self.normalize_data(gov_data)
+ data["books"] = self.normalize_data(books_data)
+
+ self.summarize_data(data)
+ return callback and callback(data, filters)
+
+ def generate_only_books_data(self, data, filters, callback=None):
+ status = "Not Filed"
+
+ books_data = self.get_books_gstr1_data(filters, aggregate=True)
+
+ data["books"] = self.normalize_data(books_data)
+ data["status"] = status
+
+ self.summarize_data(data)
+ return callback and callback(data, filters)
+
+ # GET DATA
+ def get_gov_gstr1_data(self):
+ if self.filing_status == "Filed":
+ data_field = "filed"
+ else:
+ data_field = "unfiled"
+
+ # data exists
+ if self.get(data_field):
+ mapped_data = self.get_json_for(data_field)
+
+ if mapped_data:
+ return mapped_data, False
+
+ # download data
+ return download_gstr1_json_data(self)
+
+ def get_books_gstr1_data(self, filters, aggregate=False):
+ from india_compliance.gst_india.doctype.gstr_1_beta.gstr_1_beta import (
+ get_gstr_1_from_and_to_date,
+ )
+
+ # Query / Process / Map / Sumarize / Optionally Save & Return
+ data_field = "books"
+
+ # data exists
+ if self.is_latest_data and self.get(data_field):
+ books_data = self.get_json_for(data_field)
+
+ if books_data:
+ return books_data
+
+ from_date, to_date = get_gstr_1_from_and_to_date(
+ filters.month_or_quarter, filters.year
+ )
+
+ _filters = frappe._dict(
+ {
+ "company": filters.company,
+ "company_gstin": filters.company_gstin,
+ "from_date": from_date,
+ "to_date": to_date,
+ }
+ )
+
+ # compute data
+ books_data = GSTR1BooksData(_filters).prepare_mapped_data()
+ if aggregate:
+ books_data.update({"aggregate_data": self.get_aggregate_data(books_data)})
+
+ self.update_json_for(data_field, books_data, reset_reconcile=True)
+
+ return books_data
+
+ # DATA MODIFIERS
+ def summarize_data(self, data):
+ """
+ Summarize data for all fields => reconcile, filed, unfiled, books
+
+ If return status is filed, use summary provided by Govt (usecase: amendments manually updated).
+ Else, summarize the data and save it.
+ """
+ summary_fields = {
+ "reconcile": "reconcile_summary",
+ "filed": "filed_summary",
+ "unfiled": "unfiled_summary",
+ "books": "books_summary",
+ }
+
+ for key, field in summary_fields.items():
+ if not data.get(key):
+ continue
+
+ if data.get(field):
+ continue
+
+ if self.is_latest_data and self.get(field):
+ _data = self.get_json_for(field)
+
+ if _data:
+ data[field] = _data
+ continue
+
+ summary_data = self.get_summarized_data(
+ data[key], self.filing_status == "Filed"
+ )
+
+ self.update_json_for(field, summary_data)
+ data[field] = summary_data
+
+ @staticmethod
+ def normalize_data(data):
+ """
+ Helper function to convert complex objects to simple objects
+ Returns object list of rows for each sub-category
+ """
+ for subcategory, subcategory_data in data.items():
+ if subcategory == "aggregate_data":
+ data[subcategory] = GenerateGSTR1.normalize_data(subcategory_data)
+ continue
+
+ if isinstance(subcategory_data, list | tuple | str):
+ continue
+
+ # get first key and value from subcategory_data
+ first_value = next(iter(subcategory_data.values()), None)
+
+ if isinstance(first_value, list | tuple):
+ # flatten the list of objects
+ data[subcategory] = list(itertools.chain(*subcategory_data.values()))
+
+ else:
+ data[subcategory] = [*subcategory_data.values()]
+
+ return data
+
+
+class GSTR1Log(GenerateGSTR1, Document):
+
+ @property
+ def status(self):
+ return self.generation_status
+
+ def update_status(self, status, commit=False):
+ self.db_set("generation_status", status, commit=commit)
+
+ # FILE UTILITY
+ def load_data(self, file_field=None):
+ data = {}
+
+ if file_field:
+ file_fields = [file_field]
+ else:
+ file_fields = self.get_applicable_file_fields()
+
+ for file_field in file_fields:
+ if json_data := self.get_json_for(file_field):
+ if "summary" not in file_field:
+ json_data = self.normalize_data(json_data)
+
+ data[file_field] = json_data
+
+ return data
+
+ def get_json_for(self, file_field):
+ try:
+ if file := get_file_doc(self.doctype, self.name, file_field):
+ return get_decompressed_data(file.get_content(encodings=[]))
+
+ except FileNotFoundError:
+ # say File not restored
+ self.db_set(file_field, None)
+ return
+
+ def update_json_for(
+ self, file_field, json_data, overwrite=True, reset_reconcile=False
+ ):
+ if "summary" not in file_field:
+ json_data["creation"] = get_datetime_str(get_datetime())
+ self.remove_json_for(f"{file_field}_summary")
+
+ # reset reconciled data
+ if reset_reconcile:
+ self.remove_json_for("reconcile")
+
+ # new file
+ if not getattr(self, file_field):
+ content = get_compressed_data(json_data)
+ file_name = frappe.scrub("{0}-{1}.json.gz".format(self.name, file_field))
+ file = frappe.get_doc(
+ {
+ "doctype": "File",
+ "attached_to_doctype": self.doctype,
+ "attached_to_name": self.name,
+ "attached_to_field": file_field,
+ "file_name": file_name,
+ "is_private": 1,
+ "content": content,
+ }
+ ).insert()
+ self.db_set(file_field, file.file_url)
+ return
+
+ # existing file
+ file = get_file_doc(self.doctype, self.name, file_field)
+
+ if overwrite:
+ new_json = json_data
+
+ else:
+ new_json = get_decompressed_data(file.get_content(encodings=[]))
+ new_json.update(json_data)
+
+ content = get_compressed_data(new_json)
+
+ file.save_file(content=content, overwrite=True)
+ self.db_set(file_field, file.file_url)
+
+ def remove_json_for(self, file_field):
+ if not self.get(file_field):
+ return
+
+ file = get_file_doc(self.doctype, self.name, file_field)
+ if file:
+ file.delete()
+
+ self.db_set(file_field, None)
+
+ if "summary" not in file_field:
+ self.remove_json_for(f"{file_field}_summary")
+
+ if file_field == "filed":
+ self.remove_json_for("unfiled")
+
+ # GSTR 1 UTILITY
+ def is_gstr1_api_enabled(self, settings=None):
+ if not settings:
+ settings = frappe.get_cached_doc("GST Settings")
+
+ return (
+ is_production_api_enabled(settings)
+ and settings.compare_gstr_1_data
+ and settings.has_valid_credentials(self.gstin, "Returns")
+ )
+
+ def is_sek_needed(self, settings=None):
+ if not self.is_gstr1_api_enabled(settings):
+ return False
+
+ if not self.unfiled or self.filing_status != "Filed":
+ return True
+
+ if not self.filed:
+ return True
+
+ return False
+
+ def has_all_files(self, settings=None):
+ if not self.is_latest_data:
+ return False
+
+ file_fields = self.get_applicable_file_fields(settings)
+ return all(getattr(self, file_field) for file_field in file_fields)
+
+ def get_return_status(self):
+ from india_compliance.gst_india.utils.gstin_info import get_gstr_1_return_status
+
+ status = self.get("filing_status")
+ if not status:
+ status = get_gstr_1_return_status(
+ self.company,
+ self.gstin,
+ self.return_period,
+ )
+ self.filing_status = status
+
+ return status
+
+ def get_applicable_file_fields(self, settings=None):
+ # Books aggregated data stored in filed (as to file)
+ fields = ["books", "books_summary"]
+
+ if self.is_gstr1_api_enabled(settings):
+ fields.extend(["reconcile", "reconcile_summary"])
+
+ if self.filing_status == "Filed":
+ fields.extend(["filed", "filed_summary"])
+ else:
+ fields.extend(["unfiled", "unfiled_summary"])
+
+ return fields
+
+
+def process_gstr_1_returns_info(company, gstin, response):
+ return_info = {}
+
+ # compile gstr-1 returns info
+ for info in response.get("EFiledlist"):
+ if info["rtntype"] == "GSTR1":
+ return_info[f"{info['ret_prd']}-{gstin}"] = info
+
+ # existing logs
+ gstr1_logs = frappe._dict(
+ frappe.get_all(
+ "GSTR-1 Log",
+ filters={"name": ("in", list(return_info.keys()))},
+ fields=["name", "acknowledgement_number"],
+ as_list=1,
+ )
+ )
+
+ # update gstr-1 filed upto
+ gstin_doc = frappe.get_doc("GSTIN", gstin)
+ if not gstin_doc:
+ gstin_doc = frappe.new_doc("GSTIN", gstin=gstin, status="Active")
+
+ def _update_gstr_1_filed_upto(filing_date):
+ if not gstin_doc.gstr_1_filed_upto or filing_date > getdate(
+ gstin_doc.gstr_1_filed_upto
+ ):
+ gstin_doc.gstr_1_filed_upto = filing_date
+ gstin_doc.save()
+
+ # create or update filed logs
+ for key, info in return_info.items():
+ filing_details = {
+ "filing_status": info["status"],
+ "acknowledgement_number": info["arn"],
+ "filing_date": datetime.strptime(info["dof"], "%d-%m-%Y").date(),
+ }
+
+ filed_upto = get_last_day(
+ getdate(f"{info['ret_prd'][2:]}-{info['ret_prd'][0:2]}-01")
+ )
+
+ if key in gstr1_logs:
+ if gstr1_logs[key] != info["arn"]:
+ frappe.db.set_value("GSTR-1 Log", key, filing_details)
+ _update_gstr_1_filed_upto(filed_upto)
+
+ # No updates if status is same
+ continue
+
+ frappe.get_doc(
+ {
+ "doctype": "GSTR-1 Log",
+ "company": company,
+ "gstin": gstin,
+ "return_period": info["ret_prd"],
+ **filing_details,
+ }
+ ).insert()
+ _update_gstr_1_filed_upto(filed_upto)
+
+
+def get_file_doc(doctype, docname, attached_to_field):
+ try:
+ return frappe.get_doc(
+ "File",
+ {
+ "attached_to_doctype": doctype,
+ "attached_to_name": docname,
+ "attached_to_field": attached_to_field,
+ },
+ )
+
+ except frappe.DoesNotExistError:
+ return None
+
+
+def get_compressed_data(json_data):
+ return gzip.compress(frappe.safe_encode(frappe.as_json(json_data)))
+
+
+def get_decompressed_data(content):
+ return frappe.parse_json(frappe.safe_decode(gzip.decompress(content)))
diff --git a/india_compliance/gst_india/doctype/gstr_1_log/test_gstr_1_log.py b/india_compliance/gst_india/doctype/gstr_1_log/test_gstr_1_log.py
new file mode 100644
index 000000000..e8978437c
--- /dev/null
+++ b/india_compliance/gst_india/doctype/gstr_1_log/test_gstr_1_log.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2024, Resilient Tech and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestGSTR1FiledLog(FrappeTestCase):
+ pass
diff --git a/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py b/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py
index b7c0c7cad..db9c4e591 100644
--- a/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py
+++ b/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py
@@ -123,7 +123,13 @@ def get_itc_reversal_entries(self):
def update_itc_reversal_from_purchase_invoice(self):
ineligible_credit = IneligibleITC(
self.company, self.gst_details.get("gstin"), self.month_no, self.year
- ).get_for_purchase_invoice(group_by="ineligibility_reason")
+ ).get_ineligible_itc_us_17_5_for_purchase(group_by="ineligibility_reason")
+
+ ineligible_credit_due_to_pos = IneligibleITC(
+ self.company, self.gst_details.get("gstin"), self.month_no, self.year
+ ).get_ineligible_itc_due_to_pos_for_purchase(group_by="ineligibility_reason")
+
+ ineligible_credit.extend(ineligible_credit_due_to_pos)
return self.process_ineligible_credit(ineligible_credit)
diff --git a/india_compliance/gst_india/doctype/gstr_import_log/gstr_import_log.py b/india_compliance/gst_india/doctype/gstr_import_log/gstr_import_log.py
index 6aa69e282..869e5dd68 100644
--- a/india_compliance/gst_india/doctype/gstr_import_log/gstr_import_log.py
+++ b/india_compliance/gst_india/doctype/gstr_import_log/gstr_import_log.py
@@ -72,7 +72,7 @@ def toggle_scheduled_jobs(stopped):
scheduled_job = frappe.db.get_value(
"Scheduled Job Type",
{
- "method": "india_compliance.gst_india.utils.gstr.download_queued_request",
+ "method": "india_compliance.gst_india.utils.gstr_utils.download_queued_request",
},
)
diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py
index 746bd7e8e..3beaffd8a 100644
--- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py
+++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py
@@ -16,10 +16,9 @@
from india_compliance.gst_india.utils import (
get_escaped_name,
get_gst_accounts_by_type,
- get_gstin_list,
get_party_for_gstin,
)
-from india_compliance.gst_india.utils.gstr import IMPORT_CATEGORY, ReturnType
+from india_compliance.gst_india.utils.gstr_2 import IMPORT_CATEGORY, ReturnType
class Fields(Enum):
diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js
index 49275b0ca..91487550f 100644
--- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js
+++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js
@@ -73,19 +73,19 @@ frappe.ui.form.on("Purchase Reconciliation Tool", {
new india_compliance.quick_info_popover(frm, tooltip_info);
await frappe.require("purchase_reconciliation_tool.bundle.js");
+ frm.trigger("company");
frm.purchase_reconciliation_tool = new PurchaseReconciliationTool(frm);
},
onload(frm) {
if (frm.doc.is_modified) frm.doc.reconciliation_data = null;
- frm.trigger("company");
add_gstr2b_alert(frm);
set_date_range_description(frm);
},
async company(frm) {
if (!frm.doc.company) return;
- const options = await set_gstin_options(frm);
+ const options = await india_compliance.set_gstin_options(frm);
if (!frm.doc.company_gstin) frm.set_value("company_gstin", options[0]);
},
@@ -746,8 +746,8 @@ class PurchaseReconciliationTool {
{
label: "Purchase
Invoice",
fieldname: "purchase_invoice_name",
- fieldtype: "Link",
- doctype: "Purchase Invoice",
+ fieldtype: "Dynamic Link",
+ options: "purchase_doctype",
align: "center",
width: 120,
},
@@ -1740,17 +1740,3 @@ async function create_new_purchase_invoice(row, company, company_gstin) {
frappe.new_doc("Purchase Invoice");
}
-async function set_gstin_options(frm) {
- const { query, params } = india_compliance.get_gstin_query(frm.doc.company);
- const { message } = await frappe.call({
- method: query,
- args: params,
- });
-
- if (!message) return [];
- message.unshift("All");
-
- const gstin_field = frm.get_field("company_gstin");
- gstin_field.set_data(message);
- return message;
-}
diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py
index 84aeada01..1558e70d5 100644
--- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py
+++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py
@@ -10,6 +10,7 @@
from frappe.utils import add_to_date, cint, now_datetime
from frappe.utils.response import json_handler
+from india_compliance.gst_india.api_classes.returns import ReturnsAPI
from india_compliance.gst_india.constants import ORIGINAL_VS_AMENDED
from india_compliance.gst_india.doctype.purchase_reconciliation_tool import (
BaseUtil,
@@ -24,11 +25,10 @@
is_api_enabled,
)
from india_compliance.gst_india.utils.exporter import ExcelExporter
-from india_compliance.gst_india.utils.gstr import (
+from india_compliance.gst_india.utils.gstr_2 import (
ACTIONS,
IMPORT_CATEGORY,
GSTRCategory,
- ReturnsAPI,
ReturnType,
download_gstr_2a,
download_gstr_2b,
diff --git a/india_compliance/gst_india/overrides/payment_entry.py b/india_compliance/gst_india/overrides/payment_entry.py
index 0038d3ee4..62d961468 100644
--- a/india_compliance/gst_india/overrides/payment_entry.py
+++ b/india_compliance/gst_india/overrides/payment_entry.py
@@ -8,10 +8,16 @@
from erpnext.controllers.accounts_controller import get_advance_payment_entries
from india_compliance.gst_india.overrides.transaction import get_gst_details
+from india_compliance.gst_india.overrides.transaction import (
+ validate_backdated_transaction as _validate_backdated_transaction,
+)
from india_compliance.gst_india.overrides.transaction import (
validate_transaction as validate_transaction_for_advance_payment,
)
-from india_compliance.gst_india.utils import get_all_gst_accounts
+from india_compliance.gst_india.utils import (
+ get_all_gst_accounts,
+ get_gst_accounts_by_type,
+)
@frappe.whitelist()
@@ -79,6 +85,8 @@ def validate(doc, method=None):
return
if doc.party_type == "Customer":
+ validate_backdated_transaction(doc)
+
# Presume is export with GST if GST accounts are present
doc.is_export_with_gst = 1
validate_transaction_for_advance_payment(doc, method)
@@ -100,6 +108,21 @@ def on_update_after_submit(doc, method=None):
make_gst_revesal_entry_from_advance_payment(doc)
+def before_cancel(doc, method=None):
+ if not doc.taxes:
+ return
+
+ validate_backdated_transaction(doc, action="cancel")
+
+
+def validate_backdated_transaction(doc, action="create"):
+ gst_accounts = get_gst_accounts_by_type(doc.company, "Output").values()
+ for row in doc.taxes:
+ if row.account_head in gst_accounts and row.tax_amount != 0:
+ _validate_backdated_transaction(doc, action=action)
+ break
+
+
@frappe.whitelist()
def update_party_details(party_details, doctype, company):
party_details = frappe.parse_json(party_details)
diff --git a/india_compliance/gst_india/overrides/sales_invoice.py b/india_compliance/gst_india/overrides/sales_invoice.py
index 06f9e4ca1..d41498245 100644
--- a/india_compliance/gst_india/overrides/sales_invoice.py
+++ b/india_compliance/gst_india/overrides/sales_invoice.py
@@ -5,6 +5,7 @@
from india_compliance.gst_india.overrides.payment_entry import get_taxes_summary
from india_compliance.gst_india.overrides.transaction import (
ignore_gst_validations,
+ validate_backdated_transaction,
validate_mandatory_fields,
validate_transaction,
)
@@ -57,6 +58,7 @@ def validate(doc, method=None):
gst_settings = frappe.get_cached_doc("GST Settings")
+ validate_backdated_transaction(doc, gst_settings)
validate_invoice_number(doc)
validate_credit_debit_note(doc)
validate_fields_and_set_status_for_e_invoice(doc, gst_settings)
@@ -174,6 +176,10 @@ def on_submit(doc, method=None):
def before_cancel(doc, method=None):
+ if ignore_gst_validations(doc):
+ return
+
+ validate_backdated_transaction(doc, action="cancel")
validate_fields_and_set_status_for_e_invoice(doc)
payment_references = frappe.get_all(
"Payment Entry Reference",
diff --git a/india_compliance/gst_india/overrides/transaction.py b/india_compliance/gst_india/overrides/transaction.py
index e4c1e5b9f..f43160dd2 100644
--- a/india_compliance/gst_india/overrides/transaction.py
+++ b/india_compliance/gst_india/overrides/transaction.py
@@ -3,7 +3,7 @@
import frappe
from frappe import _, bold
-from frappe.utils import cint, flt
+from frappe.utils import cint, flt, format_date
from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.taxes_and_totals import (
get_itemised_tax,
@@ -16,6 +16,9 @@
STATE_NUMBERS,
)
from india_compliance.gst_india.constants.custom_fields import E_WAYBILL_INV_FIELDS
+from india_compliance.gst_india.doctype.gst_settings.gst_settings import (
+ restrict_gstr_1_transaction_for,
+)
from india_compliance.gst_india.doctype.gstin.gstin import (
_validate_gst_transporter_id_info,
_validate_gstin_info,
@@ -656,6 +659,18 @@ def get_source_state_code(doc):
return (doc.supplier_gstin or doc.company_gstin)[:2]
+def validate_backdated_transaction(doc, gst_settings=None, action="create"):
+ if gstr_1_filed_upto := restrict_gstr_1_transaction_for(
+ doc.posting_date, doc.company_gstin, gst_settings
+ ):
+ frappe.throw(
+ _(
+ "You are not allowed to {0} {1} as GSTR-1 has been filed upto {2}"
+ ).format(action, doc.doctype, frappe.bold(format_date(gstr_1_filed_upto))),
+ title=_("Restricted Changes"),
+ )
+
+
def validate_hsn_codes(doc):
validate_hsn_code, valid_hsn_length = get_hsn_settings()
diff --git a/india_compliance/gst_india/report/gst_itemised_sales_register/gst_itemised_sales_register.py b/india_compliance/gst_india/report/gst_itemised_sales_register/gst_itemised_sales_register.py
index fa3a8ed4a..dba3cf0ec 100644
--- a/india_compliance/gst_india/report/gst_itemised_sales_register/gst_itemised_sales_register.py
+++ b/india_compliance/gst_india/report/gst_itemised_sales_register/gst_itemised_sales_register.py
@@ -39,10 +39,8 @@ def get_additional_table_columns():
def get_additional_conditions(filters):
- additional_conditions = ""
+ additional_conditions = {}
if filters.get("company_gstin"):
- additional_conditions += (
- " AND `tabSales Invoice`.company_gstin = %(company_gstin)s"
- )
+ additional_conditions.update({"company_gstin": filters.get("company_gstin")})
return additional_conditions
diff --git a/india_compliance/gst_india/report/gst_sales_register_beta/gst_sales_register_beta.py b/india_compliance/gst_india/report/gst_sales_register_beta/gst_sales_register_beta.py
index 423cd02ff..1fc832f0e 100644
--- a/india_compliance/gst_india/report/gst_sales_register_beta/gst_sales_register_beta.py
+++ b/india_compliance/gst_india/report/gst_sales_register_beta/gst_sales_register_beta.py
@@ -5,7 +5,7 @@
from frappe import _
from frappe.utils import getdate
-from india_compliance.gst_india.utils.gstr.gstr_1 import GSTR1Invoices
+from india_compliance.gst_india.utils.gstr_1.gstr_1_data import GSTR1Invoices
def execute(filters=None):
@@ -217,7 +217,7 @@ def get_columns(filters):
},
{
"label": _("UOM"),
- "fieldname": "uom",
+ "fieldname": "stock_uom",
"fieldtype": "Data",
"width": 100,
},
diff --git a/india_compliance/gst_india/report/gst_sales_register_beta/test_sales_register_beta.py b/india_compliance/gst_india/report/gst_sales_register_beta/test_sales_register_beta.py
index 10c2567f9..b01d9d881 100644
--- a/india_compliance/gst_india/report/gst_sales_register_beta/test_sales_register_beta.py
+++ b/india_compliance/gst_india/report/gst_sales_register_beta/test_sales_register_beta.py
@@ -33,7 +33,7 @@
"total_amount": -300000.0,
"total_tax_amount": 0.0,
"invoice_category": "Credit/Debit Notes (Unregistered)",
- "invoice_sub_category": "CDNUR",
+ "invoice_sub_category": "Credit/Debit Notes (Unregistered)",
"invoice_type": "EXPWOP",
},
{
@@ -52,7 +52,7 @@
"total_amount": -5000.0,
"total_tax_amount": 0.0,
"invoice_category": "Credit/Debit Notes (Unregistered)",
- "invoice_sub_category": "CDNUR",
+ "invoice_sub_category": "Credit/Debit Notes (Unregistered)",
"invoice_type": "EXPWOP",
},
{
@@ -71,7 +71,7 @@
"total_amount": 500000.0,
"total_tax_amount": 0.0,
"invoice_category": "Exports",
- "invoice_sub_category": "EXPWP",
+ "invoice_sub_category": "Export With Payment of Tax",
"invoice_type": "WPAY",
},
{
@@ -90,7 +90,7 @@
"total_amount": 45000.0,
"total_tax_amount": 0.0,
"invoice_category": "Exports",
- "invoice_sub_category": "EXPWP",
+ "invoice_sub_category": "Export With Payment of Tax",
"invoice_type": "WPAY",
},
{
@@ -109,7 +109,7 @@
"total_amount": 140000.0,
"total_tax_amount": 0.0,
"invoice_category": "Exports",
- "invoice_sub_category": "EXPWOP",
+ "invoice_sub_category": "Export Without Payment of Tax",
"invoice_type": "WOPAY",
},
{
@@ -128,7 +128,7 @@
"total_amount": 5000.0,
"total_tax_amount": 0.0,
"invoice_category": "Exports",
- "invoice_sub_category": "EXPWOP",
+ "invoice_sub_category": "Export Without Payment of Tax",
"invoice_type": "WOPAY",
},
{
@@ -148,7 +148,7 @@
"total_tax_amount": -40500.0,
"invoice_category": "Credit/Debit Notes (Registered)",
"invoice_type": "Regular B2B",
- "invoice_sub_category": "CDNR",
+ "invoice_sub_category": "Credit/Debit Notes (Registered)",
},
{
"item_code": "_Test Nil Rated Item",
@@ -166,8 +166,8 @@
"total_amount": -30000.0,
"total_tax_amount": 0.0,
"invoice_category": "Nil-Rated, Exempted, Non-GST",
- "invoice_type": "Inter-State to registered persons",
- "invoice_sub_category": "Nil-Rated",
+ "invoice_type": "Inter-State supplies to registered persons",
+ "invoice_sub_category": "Nil-Rated, Exempted, Non-GST",
},
{
"item_code": "_Test Service Item",
@@ -204,8 +204,8 @@
"total_amount": 60000.0,
"total_tax_amount": 0.0,
"invoice_category": "Nil-Rated, Exempted, Non-GST",
- "invoice_type": "Inter-State to registered persons",
- "invoice_sub_category": "Nil-Rated",
+ "invoice_type": "Inter-State supplies to registered persons",
+ "invoice_sub_category": "Nil-Rated, Exempted, Non-GST",
},
{
"item_code": "_Test Service Item",
@@ -241,8 +241,8 @@
"total_amount": 5000.0,
"total_tax_amount": 0.0,
"invoice_category": "Nil-Rated, Exempted, Non-GST",
- "invoice_type": "Intra-State to unregistered persons",
- "invoice_sub_category": "Nil-Rated",
+ "invoice_type": "Intra-State supplies to unregistered persons",
+ "invoice_sub_category": "Nil-Rated, Exempted, Non-GST",
},
{
"item_code": "_Test Service Item",
@@ -278,8 +278,8 @@
"total_amount": 5000.0,
"total_tax_amount": 0.0,
"invoice_category": "Nil-Rated, Exempted, Non-GST",
- "invoice_type": "Intra-State to unregistered persons",
- "invoice_sub_category": "Nil-Rated",
+ "invoice_type": "Intra-State supplies to unregistered persons",
+ "invoice_sub_category": "Nil-Rated, Exempted, Non-GST",
},
{
"item_code": "_Test Service Item",
@@ -315,8 +315,8 @@
"total_amount": 5000.0,
"total_tax_amount": 0.0,
"invoice_category": "Nil-Rated, Exempted, Non-GST",
- "invoice_type": "Inter-State to unregistered persons",
- "invoice_sub_category": "Nil-Rated",
+ "invoice_type": "Inter-State supplies to unregistered persons",
+ "invoice_sub_category": "Nil-Rated, Exempted, Non-GST",
},
]
@@ -349,7 +349,7 @@
"total_cess_amount": 0,
},
{
- "description": "SEZ with payment",
+ "description": "SEZ With Payment of Tax",
"indent": 1,
"taxable_value": 0,
"igst_amount": 0,
@@ -358,7 +358,7 @@
"total_cess_amount": 0,
},
{
- "description": "SEZ without payment",
+ "description": "SEZ Without Payment of Tax",
"indent": 1,
"taxable_value": 0,
"igst_amount": 0,
@@ -403,7 +403,7 @@
"total_cess_amount": 0.0,
},
{
- "description": "Exports with payment",
+ "description": "Export With Payment of Tax",
"indent": 1,
"taxable_value": 545000.0,
"igst_amount": 0.0,
@@ -412,7 +412,7 @@
"total_cess_amount": 0.0,
},
{
- "description": "Exports without payment",
+ "description": "Export Without Payment of Tax",
"indent": 1,
"taxable_value": 145000.0,
"igst_amount": 0.0,
@@ -448,7 +448,7 @@
"total_cess_amount": 0.0,
},
{
- "description": "Nil-Rated",
+ "description": "Nil-Rated, Exempted, Non-GST",
"indent": 1,
"taxable_value": 45000.0,
"igst_amount": 0.0,
@@ -456,24 +456,6 @@
"sgst_amount": 0.0,
"total_cess_amount": 0.0,
},
- {
- "description": "Exempted",
- "indent": 1,
- "taxable_value": 0,
- "igst_amount": 0,
- "cgst_amount": 0,
- "sgst_amount": 0,
- "total_cess_amount": 0,
- },
- {
- "description": "Non-GST",
- "indent": 1,
- "taxable_value": 0,
- "igst_amount": 0,
- "cgst_amount": 0,
- "sgst_amount": 0,
- "total_cess_amount": 0,
- },
{
"description": "Credit/Debit Notes (Registered)",
"indent": 0,
diff --git a/india_compliance/gst_india/report/gstr_1/gstr_1.js b/india_compliance/gst_india/report/gstr_1/gstr_1.js
index 136121ff3..e9b8286f1 100644
--- a/india_compliance/gst_india/report/gstr_1/gstr_1.js
+++ b/india_compliance/gst_india/report/gstr_1/gstr_1.js
@@ -89,7 +89,10 @@ frappe.query_reports["GSTR-1"] = {
},
},
],
- onload: create_download_buttons,
+ onload(report) {
+ create_download_buttons(report);
+ show_gstr_1_beta_alert(report);
+ },
};
function create_download_buttons(report) {
@@ -166,3 +169,19 @@ function download_full_report_excel(report) {
filters: JSON.stringify(report.get_values()),
});
}
+
+function show_gstr_1_beta_alert(report) {
+ if (report.page.wrapper.find(".alert").length) return;
+
+ const alert_message = `
+
+ GSTR-1 Beta
+
+ is released with improved features and user experience. Try it out now!
+ `;
+
+ india_compliance.show_dismissable_alert(
+ report.page.wrapper.find(".container.page-body"),
+ alert_message
+ );
+}
diff --git a/india_compliance/gst_india/report/gstr_1/gstr_1.py b/india_compliance/gst_india/report/gstr_1/gstr_1.py
index 3874d04c9..ed9903af1 100644
--- a/india_compliance/gst_india/report/gstr_1/gstr_1.py
+++ b/india_compliance/gst_india/report/gstr_1/gstr_1.py
@@ -1072,23 +1072,21 @@ def __init__(self, filters, gst_accounts):
def get_data(self):
if self.filters.get("type_of_business") == "Advances":
- records = self.get_11A_data()
-
+ records = self.get_11A_query().run(as_dict=True)
elif self.filters.get("type_of_business") == "Adjustment":
- records = self.get_11B_data()
+ records = self.get_11B_query().run(as_dict=True)
return self.process_data(records)
- def get_11A_data(self):
+ def get_11A_query(self):
return (
self.get_query()
.select(self.pe.paid_amount.as_("taxable_value"))
.groupby(self.pe.name)
- .run(as_dict=True)
)
- def get_11B_data(self):
- query = (
+ def get_11B_query(self):
+ return (
self.get_query()
.join(self.pe_ref)
.on(self.pe_ref.name == self.gl_entry.voucher_detail_no)
@@ -1096,8 +1094,6 @@ def get_11B_data(self):
.groupby(self.gl_entry.voucher_detail_no)
)
- return query.run(as_dict=True)
-
def get_query(self):
cr_or_dr = (
"credit" if self.filters.get("type_of_business") == "Advances" else "debit"
diff --git a/india_compliance/gst_india/report/gstr_3b_details/gstr_3b_details.py b/india_compliance/gst_india/report/gstr_3b_details/gstr_3b_details.py
index 22827f6f4..35a808298 100644
--- a/india_compliance/gst_india/report/gstr_3b_details/gstr_3b_details.py
+++ b/india_compliance/gst_india/report/gstr_3b_details/gstr_3b_details.py
@@ -270,7 +270,7 @@ def get_itc_from_journal_entry(self):
def get_ineligible_itc_from_purchase(self):
ineligible_itc = IneligibleITC(
self.company, self.company_gstin, self.filters.month, self.filters.year
- ).get_for_purchase_invoice()
+ ).get_ineligible_itc_us_17_5_for_purchase()
return self.process_ineligible_itc(ineligible_itc)
@@ -286,10 +286,6 @@ def process_ineligible_itc(self, ineligible_itc):
return []
for row in ineligible_itc.copy():
- if row.itc_classification == "ITC restricted due to PoS rules":
- ineligible_itc.remove(row)
- continue
-
for key in ["iamt", "camt", "samt", "csamt"]:
row[key] = row[key] * -1
@@ -429,11 +425,15 @@ def __init__(self, company, gstin, month, year) -> None:
self.year = year
self.gst_accounts = get_escaped_gst_accounts(company, "Input")
- def get_for_purchase_invoice(self, group_by="name"):
+ def get_ineligible_itc_us_17_5_for_purchase(self, group_by="name"):
+ """
+ - Ineligible As Per Section 17(5)
+ - ITC restricted due to ineligible items in purchase invoice
+ """
ineligible_transactions = self.get_vouchers_with_gst_expense("Purchase Invoice")
if not ineligible_transactions:
- return
+ return []
pi = frappe.qb.DocType("Purchase Invoice")
@@ -446,9 +446,10 @@ def get_for_purchase_invoice(self, group_by="name"):
pi.name.as_("voucher_no"),
pi.ineligibility_reason.as_("itc_classification"),
)
- .where(IfNull(pi.ineligibility_reason, "") != "")
+ .where(
+ IfNull(pi.ineligibility_reason, "") == "Ineligible As Per Section 17(5)"
+ )
.where(pi.name.isin(ineligible_transactions))
- .where(pi.company_gstin != IfNull(pi.supplier_gstin, ""))
.groupby(pi[group_by])
.run(as_dict=1)
)
@@ -465,15 +466,71 @@ def get_for_purchase_invoice(self, group_by="name"):
Sum(pi.itc_state_tax).as_("samt"),
Sum(pi.itc_cess_amount).as_("csamt"),
)
- .where(IfNull(pi.ineligibility_reason, "") != "")
+ .where(
+ IfNull(pi.ineligibility_reason, "") == "Ineligible As Per Section 17(5)"
+ )
.where(pi.name.isin(ineligible_transactions))
- .where(pi.company_gstin != IfNull(pi.supplier_gstin, ""))
.groupby(pi[group_by])
.run(as_dict=1)
)
return self.get_ineligible_credit(credit_availed, credit_available, group_by)
+ def get_ineligible_itc_due_to_pos_for_purchase(self, group_by="name"):
+ """
+ - ITC restricted due to PoS rules
+ """
+ ineligible_transactions = self.get_vouchers_with_gst_expense("Purchase Invoice")
+
+ if not ineligible_transactions:
+ return []
+
+ pi = frappe.qb.DocType("Purchase Invoice")
+ taxes = frappe.qb.DocType("Purchase Taxes and Charges")
+
+ # utility function
+ def get_tax_case_statement(account, alias):
+ return Sum(
+ Case()
+ .when(
+ taxes.account_head.isin(account),
+ taxes.base_tax_amount_after_discount_amount,
+ )
+ .else_(0)
+ ).as_(alias)
+
+ # Credit availed is not required as it will be always 0 for pos
+
+ ineligible_credit = (
+ frappe.qb.from_(pi)
+ .inner_join(taxes)
+ .on(pi.name == taxes.parent)
+ .select(
+ pi.name.as_("voucher_no"),
+ pi.posting_date,
+ pi.ineligibility_reason.as_("itc_classification"),
+ get_tax_case_statement([self.gst_accounts.igst_account], "iamt"),
+ get_tax_case_statement([self.gst_accounts.cgst_account], "camt"),
+ get_tax_case_statement([self.gst_accounts.sgst_account], "camt"),
+ get_tax_case_statement(
+ [
+ self.gst_accounts.cess_account,
+ self.gst_accounts.cess_non_advol_account,
+ ],
+ "csamt",
+ ),
+ )
+ .where(taxes.account_head.isin(list(self.gst_accounts.values())))
+ .where(
+ IfNull(pi.ineligibility_reason, "") == "ITC restricted due to PoS rules"
+ )
+ .where(pi.name.isin(ineligible_transactions))
+ .groupby(pi[group_by])
+ .run(as_dict=True)
+ )
+
+ return ineligible_credit
+
def get_for_bill_of_entry(self, group_by="name"):
ineligible_transactions = self.get_vouchers_with_gst_expense("Bill of Entry")
diff --git a/india_compliance/gst_india/setup/__init__.py b/india_compliance/gst_india/setup/__init__.py
index ce9ec7e28..a4fa622a8 100644
--- a/india_compliance/gst_india/setup/__init__.py
+++ b/india_compliance/gst_india/setup/__init__.py
@@ -215,6 +215,10 @@ def set_default_gst_settings():
"reconcile_on_friday": 1,
"reconcile_for_b2b": 1,
"reconcile_for_cdnr": 1,
+ # GSTR-1
+ "compare_gstr_1_data": 1,
+ "freeze_transactions": 1,
+ "filing_frequency": "Monthly",
}
if frappe.conf.developer_mode:
diff --git a/india_compliance/gst_india/utils/__init__.py b/india_compliance/gst_india/utils/__init__.py
index 4c3ad08f3..0609e897a 100644
--- a/india_compliance/gst_india/utils/__init__.py
+++ b/india_compliance/gst_india/utils/__init__.py
@@ -648,6 +648,13 @@ def get_titlecase_version(word, all_caps=False, **kwargs):
return word
+def is_production_api_enabled(settings=None):
+ if not settings:
+ settings = frappe.get_cached_doc("GST Settings")
+
+ return is_api_enabled(settings) and not settings.sandbox_mode
+
+
def is_api_enabled(settings=None):
if not settings:
settings = frappe.get_cached_value(
@@ -662,11 +669,7 @@ def is_api_enabled(settings=None):
def is_autofill_party_info_enabled():
settings = frappe.get_cached_doc("GST Settings")
- return (
- is_api_enabled(settings)
- and settings.autofill_party_info
- and not settings.sandbox_mode
- )
+ return is_production_api_enabled(settings) and settings.autofill_party_info
def can_enable_api(settings):
diff --git a/india_compliance/gst_india/utils/e_invoice.py b/india_compliance/gst_india/utils/e_invoice.py
index c18b52019..af7e0581f 100644
--- a/india_compliance/gst_india/utils/e_invoice.py
+++ b/india_compliance/gst_india/utils/e_invoice.py
@@ -25,6 +25,9 @@
CANCEL_REASON_CODES,
ITEM_LIMIT,
)
+from india_compliance.gst_india.doctype.gst_settings.gst_settings import (
+ get_e_invoice_applicability_date,
+)
from india_compliance.gst_india.overrides.transaction import (
_validate_hsn_codes,
validate_mandatory_fields,
@@ -413,24 +416,6 @@ def validate_taxable_item(doc, throw=True):
)
-def get_e_invoice_applicability_date(company, settings=None, throw=True):
- if not settings:
- settings = frappe.get_cached_doc("GST Settings")
-
- e_invoice_applicable_from = settings.e_invoice_applicable_from
-
- if settings.apply_e_invoice_only_for_selected_companies:
- for row in settings.e_invoice_applicable_companies:
- if company == row.company:
- e_invoice_applicable_from = row.applicable_from
- break
-
- else:
- return
-
- return e_invoice_applicable_from
-
-
def validate_if_e_invoice_can_be_cancelled(doc):
if not doc.irn:
frappe.throw(_("IRN not found"), title=_("Error Cancelling e-Invoice"))
diff --git a/india_compliance/gst_india/utils/exporter.py b/india_compliance/gst_india/utils/exporter.py
index 7610c9312..7ffeca8db 100644
--- a/india_compliance/gst_india/utils/exporter.py
+++ b/india_compliance/gst_india/utils/exporter.py
@@ -62,7 +62,6 @@ class Worksheet:
"bold": False,
"horizontal": "general",
"number_format": "General",
- "width": 20,
"height": 20,
"vertical": "center",
"wrap_text": False,
@@ -225,7 +224,9 @@ def apply_style(self, row, column, style):
if style.bg_color:
cell.fill = PatternFill(fill_type="solid", fgColor=style.bg_color)
- self.ws.column_dimensions[get_column_letter(column)].width = style.width
+ if style.get("width"):
+ self.ws.column_dimensions[get_column_letter(column)].width = style.width
+
self.ws.row_dimensions[row].height = style.height
def apply_conditional_formatting(self, has_totals):
diff --git a/india_compliance/gst_india/utils/gstin_info.py b/india_compliance/gst_india/utils/gstin_info.py
index a46d03c0c..5c9ffdc4e 100644
--- a/india_compliance/gst_india/utils/gstin_info.py
+++ b/india_compliance/gst_india/utils/gstin_info.py
@@ -4,9 +4,13 @@
import frappe
from frappe import _
+from frappe.utils import getdate
from india_compliance.gst_india.api_classes.base import BASE_URL
from india_compliance.gst_india.api_classes.public import PublicAPI
+from india_compliance.gst_india.doctype.gstr_1_log.gstr_1_log import (
+ process_gstr_1_returns_info,
+)
from india_compliance.gst_india.utils import titlecase, validate_gstin
GST_CATEGORIES = {
@@ -178,3 +182,56 @@ def _extract_address_lines(address):
# "Non Resident Taxable Person"
# "Government Department ID"
+
+
+####################################################################################################
+#### GSTIN RETURNS INFO ##########################################################################
+####################################################################################################
+
+
+def get_gstr_1_return_status(
+ company, gstin, period, process_info=True, year_increment=0
+):
+ """Returns Returns info for the given period"""
+ fy = get_fy(period, year_increment=year_increment)
+
+ response = PublicAPI().get_returns_info(gstin, fy)
+ if not response:
+ return
+
+ if process_info:
+ frappe.enqueue(
+ process_gstr_1_returns_info,
+ company=company,
+ gstin=gstin,
+ response=response,
+ enqueue_after_commit=True,
+ )
+
+ for info in response.get("EFiledlist"):
+ if info["rtntype"] == "GSTR1" and info["ret_prd"] == period:
+ return info["status"]
+
+ # late filing possibility (limitation: only checks for the next FY: good enough)
+ if not year_increment and get_current_fy() != fy:
+ get_gstr_1_return_status(
+ company, gstin, period, process_info=process_info, year_increment=1
+ )
+
+ return "Not Filed"
+
+
+def get_fy(period, year_increment=0):
+ month, year = period[:2], period[2:]
+ year = str(int(year) + year_increment)
+
+ # For the month of March, it's filed in the next FY
+ if int(month) < 3:
+ return f"{int(year) - 1}-{year[-2:]}"
+ else:
+ return f"{year}-{int(year[-2:]) + 1}"
+
+
+def get_current_fy():
+ period = getdate().strftime("%m%Y")
+ return get_fy(period)
diff --git a/india_compliance/gst_india/utils/gstr_1/__init__.py b/india_compliance/gst_india/utils/gstr_1/__init__.py
new file mode 100644
index 000000000..378c110eb
--- /dev/null
+++ b/india_compliance/gst_india/utils/gstr_1/__init__.py
@@ -0,0 +1,324 @@
+from enum import Enum
+
+
+class GSTR1_Category(Enum):
+ """
+ Overview Page of GSTR-1
+ """
+
+ # Invoice Items Bifurcation
+ B2B = "B2B, SEZ, DE"
+ EXP = "Exports"
+ B2CL = "B2C (Large)"
+ B2CS = "B2C (Others)"
+ NIL_EXEMPT = "Nil-Rated, Exempted, Non-GST"
+ CDNR = "Credit/Debit Notes (Registered)"
+ CDNUR = "Credit/Debit Notes (Unregistered)"
+
+ # Other Categories
+ AT = "Advances Received"
+ TXP = "Advances Adjusted"
+ HSN = "HSN Summary"
+ DOC_ISSUE = "Document Issued"
+ SUPECOM = "Supplies made through E-commerce Operators"
+
+
+class GSTR1_SubCategory(Enum):
+ """
+ Summary Page of GSTR-1
+ """
+
+ # Invoice Items Bifurcation
+ B2B_REGULAR = "B2B Regular"
+ B2B_REVERSE_CHARGE = "B2B Reverse Charge"
+ SEZWP = "SEZ With Payment of Tax"
+ SEZWOP = "SEZ Without Payment of Tax"
+ DE = "Deemed Exports"
+ EXPWP = "Export With Payment of Tax"
+ EXPWOP = "Export Without Payment of Tax"
+ B2CL = "B2C (Large)"
+ B2CS = "B2C (Others)"
+ NIL_EXEMPT = "Nil-Rated, Exempted, Non-GST"
+ CDNR = "Credit/Debit Notes (Registered)"
+ CDNUR = "Credit/Debit Notes (Unregistered)"
+
+ # Other Sub-Categories
+ AT = "Advances Received"
+ TXP = "Advances Adjusted"
+ HSN = "HSN Summary"
+ DOC_ISSUE = "Document Issued"
+
+ # E-Commerce
+ SUPECOM_52 = "TCS collected by E-commerce Operator u/s 52"
+ SUPECOM_9_5 = "GST Payable on RCM by E-commerce Operator u/s 9(5)"
+
+
+CATEGORY_SUB_CATEGORY_MAPPING = {
+ GSTR1_Category.B2B: (
+ GSTR1_SubCategory.B2B_REGULAR,
+ GSTR1_SubCategory.B2B_REVERSE_CHARGE,
+ GSTR1_SubCategory.SEZWP,
+ GSTR1_SubCategory.SEZWOP,
+ GSTR1_SubCategory.DE,
+ ),
+ GSTR1_Category.B2CL: (GSTR1_SubCategory.B2CL,),
+ GSTR1_Category.EXP: (GSTR1_SubCategory.EXPWP, GSTR1_SubCategory.EXPWOP),
+ GSTR1_Category.B2CS: (GSTR1_SubCategory.B2CS,),
+ GSTR1_Category.NIL_EXEMPT: (GSTR1_SubCategory.NIL_EXEMPT,),
+ GSTR1_Category.CDNR: (GSTR1_SubCategory.CDNR,),
+ GSTR1_Category.CDNUR: (GSTR1_SubCategory.CDNUR,),
+ GSTR1_Category.AT: (GSTR1_SubCategory.AT,),
+ GSTR1_Category.TXP: (GSTR1_SubCategory.TXP,),
+ GSTR1_Category.DOC_ISSUE: (GSTR1_SubCategory.DOC_ISSUE,),
+ GSTR1_Category.HSN: (GSTR1_SubCategory.HSN,),
+ GSTR1_Category.SUPECOM: (
+ GSTR1_SubCategory.SUPECOM_52,
+ GSTR1_SubCategory.SUPECOM_9_5,
+ ),
+}
+
+
+class GSTR1_DataField(Enum):
+ TRANSACTION_TYPE = "transaction_type"
+ CUST_GSTIN = "customer_gstin"
+ ECOMMERCE_GSTIN = "ecommerce_gstin"
+ CUST_NAME = "customer_name"
+ DOC_DATE = "document_date"
+ DOC_NUMBER = "document_number"
+ DOC_TYPE = "document_type"
+ DOC_VALUE = "document_value"
+ POS = "place_of_supply"
+ DIFF_PERCENTAGE = "diff_percentage"
+ REVERSE_CHARGE = "reverse_charge"
+ TAXABLE_VALUE = "total_taxable_value"
+ ITEMS = "items"
+ IGST = "total_igst_amount"
+ CGST = "total_cgst_amount"
+ SGST = "total_sgst_amount"
+ CESS = "total_cess_amount"
+ TAX_RATE = "tax_rate"
+
+ SHIPPING_BILL_NUMBER = "shipping_bill_number"
+ SHIPPING_BILL_DATE = "shipping_bill_date"
+ SHIPPING_PORT_CODE = "shipping_port_code"
+
+ EXEMPTED_AMOUNT = "exempted_amount"
+ NIL_RATED_AMOUNT = "nil_rated_amount"
+ NON_GST_AMOUNT = "non_gst_amount"
+
+ HSN_CODE = "hsn_code"
+ DESCRIPTION = "description"
+ UOM = "uom"
+ QUANTITY = "quantity"
+
+ FROM_SR = "from_sr_no"
+ TO_SR = "to_sr_no"
+ TOTAL_COUNT = "total_count"
+ DRAFT_COUNT = "draft_count"
+ CANCELLED_COUNT = "cancelled_count"
+ NET_ISSUE = "net_issue"
+ UPLOAD_STATUS = "upload_status"
+
+
+class GSTR1_ItemField(Enum):
+ INDEX = "idx"
+ TAXABLE_VALUE = "taxable_value"
+ IGST = "igst_amount"
+ CGST = "cgst_amount"
+ SGST = "sgst_amount"
+ CESS = "cess_amount"
+ TAX_RATE = "tax_rate"
+ ITEM_DETAILS = "item_details"
+ ADDITIONAL_AMOUNT = "additional_amount"
+
+
+class GovDataField(Enum):
+ CUST_GSTIN = "ctin"
+ ECOMMERCE_GSTIN = "etin"
+ DOC_DATE = "idt"
+ DOC_NUMBER = "inum"
+ DOC_VALUE = "val"
+ POS = "pos"
+ DIFF_PERCENTAGE = "diff_percent"
+ REVERSE_CHARGE = "rchrg"
+ TAXABLE_VALUE = "txval"
+ ITEMS = "itms"
+ IGST = "iamt"
+ CGST = "camt"
+ SGST = "samt"
+ CESS = "csamt"
+ TAX_RATE = "rt"
+ ITEM_DETAILS = "itm_det"
+ SHIPPING_BILL_NUMBER = "sbnum"
+ SHIPPING_BILL_DATE = "sbdt"
+ SHIPPING_PORT_CODE = "sbpcode"
+ SUPPLY_TYPE = "sply_ty"
+ NET_TAXABLE_VALUE = "suppval"
+
+ EXEMPTED_AMOUNT = "expt_amt"
+ NIL_RATED_AMOUNT = "nil_amt"
+ NON_GST_AMOUNT = "ngsup_amt"
+
+ HSN_DATA = "data"
+ HSN_CODE = "hsn_sc"
+ DESCRIPTION = "desc"
+ UOM = "uqc"
+ QUANTITY = "qty"
+ ADVANCE_AMOUNT = "ad_amt"
+
+ INDEX = "num"
+ FROM_SR = "from"
+ TO_SR = "to"
+ TOTAL_COUNT = "totnum"
+ CANCELLED_COUNT = "cancel"
+ DOC_ISSUE_DETAILS = "doc_det"
+ DOC_ISSUE_NUMBER = "doc_num"
+ DOC_ISSUE_LIST = "docs"
+ NET_ISSUE = "net_issue"
+
+ INVOICE_TYPE = "inv_typ"
+ INVOICES = "inv"
+ EXPORT_TYPE = "exp_typ"
+ TYPE = "typ"
+
+ NOTE_TYPE = "ntty"
+ NOTE_NUMBER = "nt_num"
+ NOTE_DATE = "nt_dt"
+ NOTE_DETAILS = "nt"
+
+ SUPECOM_52 = "clttx"
+ SUPECOM_9_5 = "paytx"
+
+ FLAG = "flag"
+
+
+class GovExcelField(Enum):
+ CUST_GSTIN = "GSTIN/UIN of Recipient"
+ CUST_NAME = "Receiver Name"
+ INVOICE_NUMBER = "Invoice Number"
+ INVOICE_DATE = "Invoice date"
+ INVOICE_VALUE = "Invoice Value"
+ POS = "Place Of Supply"
+ REVERSE_CHARGE = "Reverse Charge"
+ DIFF_PERCENTAGE = "Applicable % of Tax Rate"
+ INVOICE_TYPE = "Invoice Type"
+ TAXABLE_VALUE = "Taxable Value"
+ ECOMMERCE_GSTIN = "E-Commerce GSTIN"
+ TAX_RATE = "Rate"
+ IGST = "Integrated Tax Amount"
+ CGST = "Central Tax Amount"
+ SGST = "State/UT Tax Amount"
+ CESS = "Cess Amount"
+
+ NOTE_NO = "Note Number"
+ NOTE_DATE = "Note Date"
+ NOTE_TYPE = "Note Type"
+ NOTE_VALUE = "Note Value"
+
+ PORT_CODE = "Port Code"
+ SHIPPING_BILL_NO = "Shipping Bill Number"
+ SHIPPING_BILL_DATE = "Shipping Bill Date"
+
+ DESCRIPTION = "Description"
+ # NIL_RATED = "Nil Rated Supplies"
+ # EXEMPTED = "Exempted (other than nil rated/non-GST supplies)"
+ # NON_GST = "Non-GST Supplies"
+
+ HSN_CODE = "HSN"
+ UOM = "UQC"
+ QUANTITY = "Total Quantity"
+ TOTAL_VALUE = "Total Value"
+
+
+class GovJsonKey(Enum):
+ """
+ Categories / Keys as per Govt JSON file
+ """
+
+ B2B = "b2b"
+ EXP = "exp"
+ B2CL = "b2cl"
+ B2CS = "b2cs"
+ NIL_EXEMPT = "nil"
+ CDNR = "cdnr"
+ CDNUR = "cdnur"
+ AT = "at"
+ TXP = "txpd"
+ HSN = "hsn"
+ DOC_ISSUE = "doc_issue"
+ SUPECOM = "supeco"
+ RET_SUM = "sec_sum"
+
+
+class GovExcelSheetName(Enum):
+ """
+ Categories / Worksheets as per Gov Excel file
+ """
+
+ B2B = "b2b, sez, de"
+ EXP = "exp"
+ B2CL = "b2cl"
+ B2CS = "b2cs"
+ NIL_EXEMPT = "exemp"
+ CDNR = "cdnr"
+ CDNUR = "cdnur"
+ AT = "at"
+ TXP = "atadj"
+ HSN = "hsn"
+ DOC_ISSUE = "docs"
+
+
+SUB_CATEGORY_GOV_CATEGORY_MAPPING = {
+ GSTR1_SubCategory.B2B_REGULAR: GovJsonKey.B2B,
+ GSTR1_SubCategory.B2B_REVERSE_CHARGE: GovJsonKey.B2B,
+ GSTR1_SubCategory.SEZWP: GovJsonKey.B2B,
+ GSTR1_SubCategory.SEZWOP: GovJsonKey.B2B,
+ GSTR1_SubCategory.DE: GovJsonKey.B2B,
+ GSTR1_SubCategory.B2CL: GovJsonKey.B2CL,
+ GSTR1_SubCategory.EXPWP: GovJsonKey.EXP,
+ GSTR1_SubCategory.EXPWOP: GovJsonKey.EXP,
+ GSTR1_SubCategory.B2CS: GovJsonKey.B2CS,
+ GSTR1_SubCategory.NIL_EXEMPT: GovJsonKey.NIL_EXEMPT,
+ GSTR1_SubCategory.CDNR: GovJsonKey.CDNR,
+ GSTR1_SubCategory.CDNUR: GovJsonKey.CDNUR,
+ GSTR1_SubCategory.AT: GovJsonKey.AT,
+ GSTR1_SubCategory.TXP: GovJsonKey.TXP,
+ GSTR1_SubCategory.DOC_ISSUE: GovJsonKey.DOC_ISSUE,
+ GSTR1_SubCategory.HSN: GovJsonKey.HSN,
+ GSTR1_SubCategory.SUPECOM_52: GovJsonKey.SUPECOM,
+ GSTR1_SubCategory.SUPECOM_9_5: GovJsonKey.SUPECOM,
+}
+
+JSON_CATEGORY_EXCEL_CATEGORY_MAPPING = {
+ GovJsonKey.B2B.value: GovExcelSheetName.B2B.value,
+ GovJsonKey.EXP.value: GovExcelSheetName.EXP.value,
+ GovJsonKey.B2CL.value: GovExcelSheetName.B2CL.value,
+ GovJsonKey.B2CS.value: GovExcelSheetName.B2CS.value,
+ GovJsonKey.NIL_EXEMPT.value: GovExcelSheetName.NIL_EXEMPT.value,
+ GovJsonKey.CDNR.value: GovExcelSheetName.CDNR.value,
+ GovJsonKey.CDNUR.value: GovExcelSheetName.CDNUR.value,
+ GovJsonKey.AT.value: GovExcelSheetName.AT.value,
+ GovJsonKey.TXP.value: GovExcelSheetName.TXP.value,
+ GovJsonKey.HSN.value: GovExcelSheetName.HSN.value,
+ GovJsonKey.DOC_ISSUE.value: GovExcelSheetName.DOC_ISSUE.value,
+}
+
+
+class GSTR1_B2B_InvoiceType(Enum):
+ R = "Regular B2B"
+ SEWP = "SEZ supplies with payment"
+ SEWOP = "SEZ supplies without payment"
+ DE = "Deemed Exp"
+
+
+SUBCATEGORIES_NOT_CONSIDERED_IN_TOTAL_TAXABLE_VALUE = [
+ GSTR1_SubCategory.HSN.value,
+ GSTR1_SubCategory.DOC_ISSUE.value,
+ GSTR1_SubCategory.SUPECOM_52.value,
+ GSTR1_SubCategory.SUPECOM_9_5.value,
+]
+
+SUBCATEGORIES_NOT_CONSIDERED_IN_TOTAL_TAX = [
+ GSTR1_SubCategory.B2B_REVERSE_CHARGE.value,
+ *SUBCATEGORIES_NOT_CONSIDERED_IN_TOTAL_TAXABLE_VALUE,
+]
diff --git a/india_compliance/gst_india/utils/gstr/gstr_1.py b/india_compliance/gst_india/utils/gstr_1/gstr_1_data.py
similarity index 75%
rename from india_compliance/gst_india/utils/gstr/gstr_1.py
rename to india_compliance/gst_india/utils/gstr_1/gstr_1_data.py
index e0eebf75c..17f7dbd9c 100644
--- a/india_compliance/gst_india/utils/gstr/gstr_1.py
+++ b/india_compliance/gst_india/utils/gstr_1/gstr_1_data.py
@@ -1,8 +1,6 @@
# Copyright (c) 2024, Resilient Tech and contributors
# For license information, please see license.txt
-from enum import Enum
-
from pypika import Order
import frappe
@@ -10,114 +8,41 @@
from frappe.utils import getdate
from india_compliance.gst_india.utils import get_full_gst_uom
+from india_compliance.gst_india.utils.gstr_1 import (
+ CATEGORY_SUB_CATEGORY_MAPPING,
+ GSTR1_B2B_InvoiceType,
+ GSTR1_Category,
+ GSTR1_SubCategory,
+)
B2C_LIMIT = 2_50_000
-# TODO: Enum for Invoice Type
-
-
-class GSTR1_Categories(Enum):
- """
- Overview Page of GSTR-1
- """
-
- # Invoice Items Bifurcation
- B2B = "B2B, SEZ, DE"
- B2CL = "B2C (Large)"
- EXP = "Exports"
- B2CS = "B2C (Others)"
- NIL_EXEMPT = "Nil-Rated, Exempted, Non-GST"
- CDNR = "Credit/Debit Notes (Registered)"
- CDNUR = "Credit/Debit Notes (Unregistered)"
- # Other Categories
- AT = "Advances Received"
- TXP = "Advances Adjusted"
- DOC_ISSUE = "Document Issued"
- HSN = "HSN Summary"
-
-
-class GSTR1_SubCategories(Enum):
- """
- Summary Page of GSTR-1
- """
-
- # Invoice Items Bifurcation
- B2B_REGULAR = "B2B Regular" # Regular B2B
- B2B_REVERSE_CHARGE = "B2B Reverse Charge" # Regular B2B
- SEZWP = "SEZWP" # SEZ supplies with payment
- SEZWOP = "SEZWOP" # SEZ supplies without payment
- DE = "Deemed Exports" # Deemed Exp
- B2CL = "B2C (Large)" # NA
- EXPWP = "EXPWP" # WPAY
- EXPWOP = "EXPWOP" # WOPAY
- B2CS = "B2C (Others)" # NA
- NIL_RATED = "Nil-Rated" # Inter vs Intra & Regis vs UnRegis
- EXEMPTED = "Exempted" # Inter vs Intra & Regis vs UnRegis
- NON_GST = "Non-GST" # Inter vs Intra & Regis vs UnRegis
- CDNR = "CDNR" # Like B2B
- CDNUR = "CDNUR" # B2CL vs EXPWP vs EXPWOP
- # Other Sub-Categories
- # AT = "Advances Received"
- # TXP = "Advances Adjusted"
- # HSN = "HSN Summary"
- # DOC_ISSUE = "Document Issued"
-
-
-CATEGORY_SUB_CATEGORY_MAPPING = {
- GSTR1_Categories.B2B: (
- GSTR1_SubCategories.B2B_REGULAR,
- GSTR1_SubCategories.B2B_REVERSE_CHARGE,
- GSTR1_SubCategories.SEZWP,
- GSTR1_SubCategories.SEZWOP,
- GSTR1_SubCategories.DE,
- ),
- GSTR1_Categories.B2CL: (GSTR1_SubCategories.B2CL,),
- GSTR1_Categories.EXP: (GSTR1_SubCategories.EXPWP, GSTR1_SubCategories.EXPWOP),
- GSTR1_Categories.B2CS: (GSTR1_SubCategories.B2CS,),
- GSTR1_Categories.NIL_EXEMPT: (
- GSTR1_SubCategories.NIL_RATED,
- GSTR1_SubCategories.EXEMPTED,
- GSTR1_SubCategories.NON_GST,
- ),
- GSTR1_Categories.CDNR: (GSTR1_SubCategories.CDNR,),
- GSTR1_Categories.CDNUR: (GSTR1_SubCategories.CDNUR,),
-}
-
-SUB_CATEGORIES_DESCRIPTION = {
- GSTR1_SubCategories.SEZWP: "SEZ with payment",
- GSTR1_SubCategories.SEZWOP: "SEZ without payment",
- GSTR1_SubCategories.EXPWP: "Exports with payment",
- GSTR1_SubCategories.EXPWOP: "Exports without payment",
- GSTR1_SubCategories.CDNR: "Credit/Debit Notes (Registered)",
- GSTR1_SubCategories.CDNUR: "Credit/Debit Notes (Unregistered)",
-}
-
CATEGORY_CONDITIONS = {
- GSTR1_Categories.B2B.value: {
+ GSTR1_Category.B2B.value: {
"category": "is_b2b_invoice",
"sub_category": "set_for_b2b",
},
- GSTR1_Categories.B2CL.value: {
+ GSTR1_Category.B2CL.value: {
"category": "is_b2cl_invoice",
"sub_category": "set_for_b2cl",
},
- GSTR1_Categories.EXP.value: {
+ GSTR1_Category.EXP.value: {
"category": "is_export_invoice",
"sub_category": "set_for_exports",
},
- GSTR1_Categories.B2CS.value: {
+ GSTR1_Category.B2CS.value: {
"category": "is_b2cs_invoice",
"sub_category": "set_for_b2cs",
},
- GSTR1_Categories.NIL_EXEMPT.value: {
+ GSTR1_Category.NIL_EXEMPT.value: {
"category": "is_nil_rated_exempted_non_gst_invoice",
"sub_category": "set_for_nil_exp_non_gst",
},
- GSTR1_Categories.CDNR.value: {
+ GSTR1_Category.CDNR.value: {
"category": "is_cdnr_invoice",
"sub_category": "set_for_cdnr",
},
- GSTR1_Categories.CDNUR.value: {
+ GSTR1_Category.CDNUR.value: {
"category": "is_cdnur_invoice",
"sub_category": "set_for_cdnur",
},
@@ -145,8 +70,9 @@ def get_base_query(self):
.on(self.si.return_against == returned_si.name)
.select(
IfNull(self.si_item.item_code, self.si_item.item_name).as_("item_code"),
+ self.si_item.qty,
self.si_item.gst_hsn_code,
- self.si_item.uom,
+ self.si_item.stock_uom,
self.si.billing_address_gstin,
self.si.company_gstin,
self.si.customer_name,
@@ -371,20 +297,20 @@ def set_for_b2b(self, invoice):
def set_for_b2cl(self, invoice):
# NO INVOICE VALUE
- invoice.invoice_sub_category = GSTR1_SubCategories.B2CL.value
+ invoice.invoice_sub_category = GSTR1_SubCategory.B2CL.value
def set_for_exports(self, invoice):
if invoice.is_export_with_gst:
- invoice.invoice_sub_category = GSTR1_SubCategories.EXPWP.value
+ invoice.invoice_sub_category = GSTR1_SubCategory.EXPWP.value
invoice.invoice_type = "WPAY"
else:
- invoice.invoice_sub_category = GSTR1_SubCategories.EXPWOP.value
+ invoice.invoice_sub_category = GSTR1_SubCategory.EXPWOP.value
invoice.invoice_type = "WOPAY"
def set_for_b2cs(self, invoice):
# NO INVOICE VALUE
- invoice.invoice_sub_category = GSTR1_SubCategories.B2CS.value
+ invoice.invoice_sub_category = GSTR1_SubCategory.B2CS.value
def set_for_nil_exp_non_gst(self, invoice):
# INVOICE TYPE
@@ -394,24 +320,15 @@ def set_for_nil_exp_non_gst(self, invoice):
gst_registration = "registered" if is_registered else "unregistered"
supply_type = "Inter-State" if is_interstate else "Intra-State"
- invoice.invoice_type = f"{supply_type} to {gst_registration} persons"
-
- # INVOICE SUB CATEGORY
- if self.is_nil_rated(invoice):
- invoice.invoice_sub_category = GSTR1_SubCategories.NIL_RATED.value
-
- elif self.is_exempted(invoice):
- invoice.invoice_sub_category = GSTR1_SubCategories.EXEMPTED.value
-
- elif self.is_non_gst(invoice):
- invoice.invoice_sub_category = GSTR1_SubCategories.NON_GST.value
+ invoice.invoice_type = f"{supply_type} supplies to {gst_registration} persons"
+ invoice.invoice_sub_category = GSTR1_SubCategory.NIL_EXEMPT.value
def set_for_cdnr(self, invoice):
self._set_invoice_type_for_b2b_and_cdnr(invoice)
- invoice.invoice_sub_category = GSTR1_SubCategories.CDNR.value
+ invoice.invoice_sub_category = GSTR1_SubCategory.CDNR.value
def set_for_cdnur(self, invoice):
- invoice.invoice_sub_category = GSTR1_SubCategories.CDNUR.value
+ invoice.invoice_sub_category = GSTR1_SubCategory.CDNUR.value
if self.is_export(invoice):
if invoice.is_export_with_gst:
invoice.invoice_type = "EXPWP"
@@ -425,25 +342,25 @@ def set_for_cdnur(self, invoice):
def _set_invoice_type_for_b2b_and_cdnr(self, invoice):
if invoice.gst_category == "Deemed Export":
- invoice.invoice_type = "Deemed Exp"
- invoice.invoice_sub_category = GSTR1_SubCategories.DE.value
+ invoice.invoice_type = GSTR1_B2B_InvoiceType.DE.value
+ invoice.invoice_sub_category = GSTR1_SubCategory.DE.value
elif invoice.gst_category == "SEZ":
if invoice.is_export_with_gst:
- invoice.invoice_type = "SEZ supplies with payment"
- invoice.invoice_sub_category = GSTR1_SubCategories.SEZWP.value
+ invoice.invoice_type = GSTR1_B2B_InvoiceType.SEWP.value
+ invoice.invoice_sub_category = GSTR1_SubCategory.SEZWP.value
else:
- invoice.invoice_type = "SEZ supplies without payment"
- invoice.invoice_sub_category = GSTR1_SubCategories.SEZWOP.value
+ invoice.invoice_type = GSTR1_B2B_InvoiceType.SEWOP.value
+ invoice.invoice_sub_category = GSTR1_SubCategory.SEZWOP.value
elif invoice.is_reverese_charge:
- invoice.invoice_type = "Regular B2B"
- invoice.invoice_sub_category = GSTR1_SubCategories.B2B_REVERSE_CHARGE.value
+ invoice.invoice_type = GSTR1_B2B_InvoiceType.R.value
+ invoice.invoice_sub_category = GSTR1_SubCategory.B2B_REVERSE_CHARGE.value
else:
- invoice.invoice_type = "Regular B2B"
- invoice.invoice_sub_category = GSTR1_SubCategories.B2B_REGULAR.value
+ invoice.invoice_type = GSTR1_B2B_InvoiceType.R.value
+ invoice.invoice_sub_category = GSTR1_SubCategory.B2B_REGULAR.value
class GSTR1Invoices(GSTR1Query, GSTR1Subcategory):
@@ -460,11 +377,23 @@ def __init__(self, filters=None):
def process_invoices(self, invoices):
settings = frappe.get_cached_doc("GST Settings")
-
+ identified_uom = {}
for invoice in invoices:
self.invoice_conditions = {}
self.assign_categories(invoice)
- invoice["uom"] = get_full_gst_uom(invoice.get("uom"), settings)
+
+ if invoice.gst_hsn_code and invoice.gst_hsn_code.startswith("99"):
+ invoice["stock_uom"] = "OTH-OTHERS"
+ invoice["qty"] = 0
+ continue
+
+ stock_uom = invoice.get("stock_uom", "")
+ if stock_uom in identified_uom:
+ invoice["stock_uom"] = identified_uom[stock_uom]
+ else:
+ gst_uom = get_full_gst_uom(stock_uom, settings)
+ identified_uom[stock_uom] = gst_uom
+ invoice["stock_uom"] = gst_uom
def assign_categories(self, invoice):
@@ -507,7 +436,7 @@ def get_invoices_for_hsn_wise_summary(self):
query.gst_hsn_code,
query.gst_rate,
query.gst_treatment,
- query.uom,
+ query.stock_uom,
)
.orderby(
query.posting_date, query.invoice_no, query.item_code, order=Order.desc
@@ -544,7 +473,18 @@ def get_overview(self):
final_summary = []
sub_category_summary = self.get_sub_category_summary()
+ IGNORED_CATEGORIES = (
+ GSTR1_Category.AT,
+ GSTR1_Category.TXP,
+ GSTR1_Category.DOC_ISSUE,
+ GSTR1_Category.HSN,
+ GSTR1_Category.SUPECOM,
+ )
+
for category, sub_categories in CATEGORY_SUB_CATEGORY_MAPPING.items():
+ if category in IGNORED_CATEGORIES:
+ continue
+
category_summary = {
"description": category.value,
"no_of_records": 0,
@@ -572,9 +512,10 @@ def get_sub_category_summary(self):
summary = {}
- for category in GSTR1_SubCategories:
- summary[category.value] = {
- "description": SUB_CATEGORIES_DESCRIPTION.get(category, category.value),
+ for category in GSTR1_SubCategory:
+ category = category.value
+ summary[category] = {
+ "description": category,
"no_of_records": 0,
"indent": 1,
"unique_records": set(),
@@ -597,27 +538,19 @@ def get_sub_category_summary(self):
return summary
def update_overlaping_invoice_summary(self, sub_category_summary, final_summary):
- nil_exempt_non_gst = (
- GSTR1_SubCategories.NIL_RATED.value,
- GSTR1_SubCategories.EXEMPTED.value,
- GSTR1_SubCategories.NON_GST.value,
- )
+ nil_exempt = GSTR1_SubCategory.NIL_EXEMPT.value
# Get Unique Taxable Invoices
unique_invoices = set()
for category, row in sub_category_summary.items():
- if category in nil_exempt_non_gst:
+ if category == nil_exempt:
continue
unique_invoices.update(row["unique_records"])
# Get Overlaping Invoices
- overlaping_invoices = set()
- for category in nil_exempt_non_gst:
- category_invoices = sub_category_summary[category]["unique_records"]
-
- overlaping_invoices.update(category_invoices.intersection(unique_invoices))
- unique_invoices.update(category_invoices)
+ category_invoices = sub_category_summary[nil_exempt]["unique_records"]
+ overlaping_invoices = category_invoices.intersection(unique_invoices)
# Update Summary
if overlaping_invoices:
diff --git a/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py b/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py
new file mode 100644
index 000000000..94a4d2102
--- /dev/null
+++ b/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py
@@ -0,0 +1,123 @@
+import frappe
+from frappe import _
+from frappe.utils import cint
+
+from india_compliance.gst_india.api_classes.returns import GSTR1API
+from india_compliance.gst_india.doctype.gstr_import_log.gstr_import_log import (
+ create_import_log,
+)
+from india_compliance.gst_india.utils.gstr_1.gstr_1_json_map import (
+ convert_to_internal_data_format,
+)
+
+UNFILED_ACTIONS = [
+ "B2B",
+ "B2CL",
+ "B2CS",
+ "CDNR",
+ "CDNUR",
+ "EXP",
+ "NIL",
+ "AT",
+ "TXP",
+ # "SUPECO", # 403 Forbidden TODO: Check when this is active
+ "HSNSUM",
+ "DOCISS",
+]
+
+FILED_ACTIONS = [*UNFILED_ACTIONS, "RETSUM"]
+
+
+def download_gstr1_json_data(gstr1_log):
+ """
+ Download GSTR-1 and Unfiled GSTR1 data from GST Portal
+ """
+ gstin = gstr1_log.gstin
+ return_period = gstr1_log.return_period
+
+ is_queued = False
+ json_data = frappe._dict()
+ api = GSTR1API(gstin)
+
+ if gstr1_log.filing_status == "Filed":
+ return_type = "GSTR1"
+ actions = FILED_ACTIONS
+ data_field = "filed"
+
+ else:
+ return_type = "Unfiled GSTR1"
+ actions = UNFILED_ACTIONS
+ data_field = "unfiled"
+
+ # download data
+ for action in actions:
+ response = api.get_gstr_1_data(action, return_period)
+
+ if response.error_type in ["otp_requested", "invalid_otp"]:
+ return response, None
+
+ if response.error_type == "no_docs_found":
+ continue
+
+ # Queued
+ if response.token:
+ create_import_log(
+ gstin,
+ return_type,
+ return_period,
+ classification=action,
+ request_id=response.token,
+ retry_after_mins=cint(response.est),
+ )
+ is_queued = True
+ continue
+
+ if response.error_type:
+ continue
+
+ json_data.update(response)
+
+ mapped_data = convert_to_internal_data_format(json_data)
+ gstr1_log.update_json_for(data_field, mapped_data, reset_reconcile=True)
+
+ if is_queued:
+ gstr1_log.update_status("Queued")
+
+ frappe.publish_realtime(
+ "gstr1_queued",
+ message={"gstin": gstin, "return_period": return_period},
+ user=frappe.session.user,
+ doctype="GSTR-1 Beta",
+ )
+
+ return mapped_data, is_queued
+
+
+def save_gstr_1(gstin, return_period, json_data, return_type):
+ if return_type == "GSTR1":
+ data_field = "filed"
+
+ elif return_type == "Unfiled GSTR1":
+ data_field = "unfiled"
+
+ if not json_data:
+ frappe.throw(
+ _(
+ "Data received seems to be invalid from the GST Portal. Please try"
+ " again or raise support ticket."
+ ),
+ title=_("Invalid Response Received."),
+ )
+
+ mapped_data = convert_to_internal_data_format(json_data)
+
+ gstr1_log = frappe.get_doc("GSTR-1 Log", f"{return_period}-{gstin}")
+ gstr1_log.update_json_for(data_field, mapped_data, overwrite=False)
+
+
+def save_gstr_1_filed_data(gstin, return_period, json_data):
+ save_gstr_1(gstin, return_period, json_data, "GSTR1")
+
+
+def save_gstr_1_unfiled_data(gstin, return_period, json_data):
+ save_gstr_1(gstin, return_period, json_data, "Unfiled GSTR1")
diff --git a/india_compliance/gst_india/utils/gstr_1/gstr_1_json_map.py b/india_compliance/gst_india/utils/gstr_1/gstr_1_json_map.py
new file mode 100644
index 000000000..20666dac1
--- /dev/null
+++ b/india_compliance/gst_india/utils/gstr_1/gstr_1_json_map.py
@@ -0,0 +1,2358 @@
+from datetime import datetime
+
+import frappe
+from frappe.utils import flt
+
+from india_compliance.gst_india.constants import STATE_NUMBERS, UOM_MAP
+from india_compliance.gst_india.report.gstr_1.gstr_1 import (
+ GSTR1DocumentIssuedSummary,
+ GSTR11A11BData,
+)
+from india_compliance.gst_india.utils import get_gst_accounts_by_type
+from india_compliance.gst_india.utils.__init__ import get_party_for_gstin
+from india_compliance.gst_india.utils.gstr_1 import (
+ CATEGORY_SUB_CATEGORY_MAPPING,
+ SUB_CATEGORY_GOV_CATEGORY_MAPPING,
+ SUBCATEGORIES_NOT_CONSIDERED_IN_TOTAL_TAX,
+ SUBCATEGORIES_NOT_CONSIDERED_IN_TOTAL_TAXABLE_VALUE,
+ GovDataField,
+ GovJsonKey,
+ GSTR1_B2B_InvoiceType,
+ GSTR1_Category,
+ GSTR1_DataField,
+ GSTR1_ItemField,
+ GSTR1_SubCategory,
+)
+from india_compliance.gst_india.utils.gstr_1.gstr_1_data import GSTR1Invoices
+
+############################################################################################################
+### Map Govt JSON to Internal Data Structure ###############################################################
+############################################################################################################
+
+
+class GovDataMapper:
+ """
+ GST Developer API Documentation for Returns - https://developer.gst.gov.in/apiportal/taxpayer/returns
+
+ GSTR-1 JSON format - https://developer.gst.gov.in/pages/apiportal/data/Returns/GSTR1%20-%20Save%20GSTR1%20data/v4.0/GSTR1%20-%20Save%20GSTR1%20data%20attributes.xlsx
+ """
+
+ KEY_MAPPING = {}
+ # default item amounts
+ DEFAULT_ITEM_AMOUNTS = {
+ GSTR1_ItemField.TAXABLE_VALUE.value: 0,
+ GSTR1_ItemField.IGST.value: 0,
+ GSTR1_ItemField.CGST.value: 0,
+ GSTR1_ItemField.SGST.value: 0,
+ GSTR1_ItemField.CESS.value: 0,
+ }
+
+ FLOAT_FIELDS = {
+ GovDataField.DOC_VALUE.value,
+ GovDataField.TAXABLE_VALUE.value,
+ GovDataField.DIFF_PERCENTAGE.value,
+ GovDataField.IGST.value,
+ GovDataField.CGST.value,
+ GovDataField.SGST.value,
+ GovDataField.CESS.value,
+ GovDataField.NET_TAXABLE_VALUE.value,
+ GovDataField.EXEMPTED_AMOUNT.value,
+ GovDataField.NIL_RATED_AMOUNT.value,
+ GovDataField.NON_GST_AMOUNT.value,
+ GovDataField.QUANTITY.value,
+ GovDataField.ADVANCE_AMOUNT.value,
+ }
+
+ DISCARD_IF_ZERO_FIELDS = {
+ GovDataField.DIFF_PERCENTAGE.value,
+ }
+
+ def __init__(self):
+ self.set_total_defaults()
+
+ self.value_formatters_for_internal = {}
+ self.value_formatters_for_gov = {}
+ self.gstin_party_map = {}
+ # value formatting constants
+
+ self.STATE_NUMBERS = self.reverse_dict(STATE_NUMBERS)
+
+ def format_data(
+ self, data: dict, default_data: dict = None, for_gov: bool = False
+ ) -> dict:
+ """
+ Objective: Convert Object from one format to another.
+ eg: Govt JSON to Internal Data Structure
+
+ Args:
+ data (dict): Data to be converted
+ default_data (dict, optional): Default Data to be added. Hardcoded values.
+ for_gov (bool, optional): If the data is to be converted to Govt JSON. Defaults to False.
+ else it will be converted to Internal Data Structure.
+
+ Steps:
+ 1. Use key mapping to map the keys from one format to another.
+ 2. Use value formatters to format the values of the keys.
+ 3. Round values
+ """
+ output = {}
+
+ if default_data:
+ output.update(default_data)
+
+ key_mapping = self.KEY_MAPPING.copy()
+
+ if for_gov:
+ key_mapping = self.reverse_dict(key_mapping)
+
+ value_formatters = (
+ self.value_formatters_for_gov
+ if for_gov
+ else self.value_formatters_for_internal
+ )
+
+ for old_key, new_key in key_mapping.items():
+ invoice_data_value = data.get(old_key, "")
+
+ if not for_gov and old_key == "flag":
+ continue
+
+ if new_key in self.DISCARD_IF_ZERO_FIELDS and not invoice_data_value:
+ continue
+
+ if not (invoice_data_value or invoice_data_value == 0):
+ # continue if value is None or empty object
+ continue
+
+ value_formatter = value_formatters.get(old_key)
+
+ if callable(value_formatter):
+ output[new_key] = value_formatter(invoice_data_value, data)
+ else:
+ output[new_key] = invoice_data_value
+
+ if new_key in self.FLOAT_FIELDS:
+ output[new_key] = flt(output[new_key], 2)
+
+ return output
+
+ # common utils
+
+ def update_totals(self, invoice, items):
+ """
+ Update item totals to the invoice row
+ """
+ total_data = self.TOTAL_DEFAULTS.copy()
+
+ for item in items:
+ for field, value in item.items():
+ total_field = f"total_{field}"
+
+ if total_field not in total_data:
+ continue
+
+ invoice[total_field] = invoice.setdefault(total_field, 0) + value
+
+ def set_total_defaults(self):
+ self.TOTAL_DEFAULTS = {
+ f"total_{key}": 0 for key in self.DEFAULT_ITEM_AMOUNTS.keys()
+ }
+
+ def reverse_dict(self, data):
+ return {v: k for k, v in data.items()}
+
+ # common value formatters
+ def map_place_of_supply(self, pos, *args):
+ if pos.isnumeric():
+ return f"{pos}-{self.STATE_NUMBERS.get(pos)}"
+
+ return pos.split("-")[0]
+
+ def format_item_for_internal(self, items, *args):
+ return [
+ {
+ **self.DEFAULT_ITEM_AMOUNTS.copy(),
+ **self.format_data(item.get(GovDataField.ITEM_DETAILS.value, {})),
+ }
+ for item in items
+ ]
+
+ def format_item_for_gov(self, items, *args):
+ return [
+ {
+ GovDataField.INDEX.value: index + 1,
+ GovDataField.ITEM_DETAILS.value: self.format_data(item, for_gov=True),
+ }
+ for index, item in enumerate(items)
+ ]
+
+ def guess_customer_name(self, gstin):
+ if party := self.gstin_party_map.get(gstin):
+ return party
+
+ return self.gstin_party_map.setdefault(
+ gstin, get_party_for_gstin(gstin, "Customer") or "Unknown"
+ )
+
+ def format_date_for_internal(self, date, *args):
+ return datetime.strptime(date, "%d-%m-%Y").strftime("%Y-%m-%d")
+
+ def format_date_for_gov(self, date, *args):
+ return datetime.strptime(date, "%Y-%m-%d").strftime("%d-%m-%Y")
+
+
+class B2B(GovDataMapper):
+ """
+ GST API Version - v4.0
+
+ Government Data Format:
+ [
+ {
+ 'ctin': '24AANFA2641L1ZF',
+ 'inv': [
+ {
+ 'inum': 'S008400',
+ 'itms': [
+ {'num': 1, 'itm_det': {'txval': 10000,
+ ...
+ }}
+ ]
+ }
+ ...
+ ]
+ }
+ ]
+
+ Internal Data Format:
+ {
+ 'B2B Regular': {'S008400': {
+ 'customer_gstin': '24AANFA2641L1ZF',
+ 'document_number': 'S008400',
+ 'items': [
+ {
+ 'taxable_value': 10000,
+ ...
+ }
+ ],
+ ...
+ }}
+ }
+
+ """
+
+ KEY_MAPPING = {
+ # GovDataFields.CUST_GSTIN.value: DataFields.CUST_GSTIN.value,
+ # GovDataFields.INVOICES.value: "invoices",
+ GovDataField.FLAG.value: "flag",
+ GovDataField.DOC_NUMBER.value: GSTR1_DataField.DOC_NUMBER.value,
+ GovDataField.DOC_DATE.value: GSTR1_DataField.DOC_DATE.value,
+ GovDataField.DOC_VALUE.value: GSTR1_DataField.DOC_VALUE.value,
+ GovDataField.POS.value: GSTR1_DataField.POS.value,
+ GovDataField.REVERSE_CHARGE.value: GSTR1_DataField.REVERSE_CHARGE.value,
+ # GovDataFields.ECOMMERCE_GSTIN.value: GSTR1_DataFields.ECOMMERCE_GSTIN.value,
+ GovDataField.INVOICE_TYPE.value: GSTR1_DataField.DOC_TYPE.value,
+ GovDataField.DIFF_PERCENTAGE.value: GSTR1_DataField.DIFF_PERCENTAGE.value,
+ GovDataField.ITEMS.value: GSTR1_DataField.ITEMS.value,
+ # GovDataFields.INDEX.value: ItemFields.INDEX.value,
+ GovDataField.ITEM_DETAILS.value: GSTR1_ItemField.ITEM_DETAILS.value,
+ GovDataField.TAX_RATE.value: GSTR1_ItemField.TAX_RATE.value,
+ GovDataField.TAXABLE_VALUE.value: GSTR1_ItemField.TAXABLE_VALUE.value,
+ GovDataField.IGST.value: GSTR1_ItemField.IGST.value,
+ GovDataField.CGST.value: GSTR1_ItemField.CGST.value,
+ GovDataField.SGST.value: GSTR1_ItemField.SGST.value,
+ GovDataField.CESS.value: GSTR1_ItemField.CESS.value,
+ }
+
+ # value formatting constants
+ DOCUMENT_CATEGORIES = {
+ "R": GSTR1_B2B_InvoiceType.R.value,
+ "SEWP": GSTR1_B2B_InvoiceType.SEWP.value,
+ "SEWOP": GSTR1_B2B_InvoiceType.SEWOP.value,
+ "DE": GSTR1_B2B_InvoiceType.DE.value,
+ }
+
+ SUBCATEGORIES = {
+ # "B2B": GSTR1_SubCategories.B2B_REGULAR.value,
+ # "B2B": GSTR1_SubCategories.B2B_REVERSE_CHARGE.value,
+ "SEWP": GSTR1_SubCategory.SEZWP.value,
+ "SEWOP": GSTR1_SubCategory.SEZWOP.value,
+ "DE": GSTR1_SubCategory.DE.value,
+ }
+
+ def __init__(self):
+ super().__init__()
+
+ self.value_formatters_for_internal = {
+ GovDataField.ITEMS.value: self.format_item_for_internal,
+ GovDataField.INVOICE_TYPE.value: self.document_category_mapping,
+ GovDataField.POS.value: self.map_place_of_supply,
+ GovDataField.DOC_DATE.value: self.format_date_for_internal,
+ }
+
+ self.value_formatters_for_gov = {
+ GSTR1_DataField.ITEMS.value: self.format_item_for_gov,
+ GSTR1_DataField.DOC_TYPE.value: self.document_category_mapping,
+ GSTR1_DataField.POS.value: self.map_place_of_supply,
+ GSTR1_DataField.DOC_DATE.value: self.format_date_for_gov,
+ }
+
+ def convert_to_internal_data_format(self, input_data):
+ """
+ Objective: Convert Govt JSON to Internal Data Structure
+ Args:
+ input_data (list): Govt JSON Data
+ Returns:
+ dict: Internal Data Structure
+ """
+
+ output = {}
+
+ for customer_data in input_data:
+ customer_gstin = customer_data.get(GovDataField.CUST_GSTIN.value)
+
+ default_invoice_data = {
+ GSTR1_DataField.CUST_GSTIN.value: customer_gstin,
+ GSTR1_DataField.CUST_NAME.value: self.guess_customer_name(
+ customer_gstin
+ ),
+ }
+
+ for invoice in customer_data.get(GovDataField.INVOICES.value):
+ invoice_data = self.format_data(invoice, default_invoice_data)
+ self.update_totals(
+ invoice_data, invoice_data.get(GSTR1_DataField.ITEMS.value)
+ )
+
+ subcategory_data = output.setdefault(
+ self.get_document_subcategory(invoice), {}
+ )
+ subcategory_data[invoice_data[GSTR1_DataField.DOC_NUMBER.value]] = (
+ invoice_data
+ )
+
+ return output
+
+ def convert_to_gov_data_format(self, input_data, **kwargs):
+ """
+ Objective: Convert Internal Data Structure to Govt JSON
+ Args:
+ input_data (dict): Internal Data Structure
+ Returns:
+ list: Govt JSON Data
+ """
+ customer_data = {}
+
+ self.DOCUMENT_CATEGORIES = self.reverse_dict(self.DOCUMENT_CATEGORIES)
+
+ for invoice in input_data:
+ customer = customer_data.setdefault(
+ invoice[GSTR1_DataField.CUST_GSTIN.value],
+ {
+ GovDataField.CUST_GSTIN.value: invoice[
+ GSTR1_DataField.CUST_GSTIN.value
+ ],
+ GovDataField.INVOICES.value: [],
+ },
+ )
+
+ customer[GovDataField.INVOICES.value].append(
+ self.format_data(invoice, for_gov=True)
+ )
+
+ return list(customer_data.values())
+
+ def get_document_subcategory(self, invoice_data):
+ if invoice_data.get(GovDataField.INVOICE_TYPE.value) in self.SUBCATEGORIES:
+ return self.SUBCATEGORIES[invoice_data[GovDataField.INVOICE_TYPE.value]]
+
+ if invoice_data.get(GovDataField.REVERSE_CHARGE.value) == "Y":
+ return GSTR1_SubCategory.B2B_REVERSE_CHARGE.value
+
+ return GSTR1_SubCategory.B2B_REGULAR.value
+
+ # value formatting methods
+
+ def document_category_mapping(self, sub_category, data):
+ return self.DOCUMENT_CATEGORIES.get(sub_category, sub_category)
+
+
+class B2CL(GovDataMapper):
+ """
+ GST API Version - v4.0
+
+ Government Data Format:
+ {
+ 'pos': '05',
+ 'inv': [
+ {
+ 'inum': '92661',
+ 'itms': [
+ {'num': 1,'itm_det': {'txval': 10000,
+ ...
+ }},
+ ...
+ ]
+ }
+ ...
+ ],
+ ...
+ }
+
+ Internal Data Format:
+
+ {
+ 'B2C (Large)': {
+ '92661': {
+ 'place_of_supply': '05-Uttarakhand',
+ 'document_number': '92661',
+ 'items': [
+ {
+ 'taxable_value': 10000,
+ ...
+ },
+ ...
+ ],
+ 'total_taxable_value': 10000,
+ ...
+ }
+ ...
+ }
+ }
+ """
+
+ DOCUMENT_CATEGORY = "B2C (Large)"
+ SUBCATEGORY = GSTR1_SubCategory.B2CL.value
+ DEFAULT_ITEM_AMOUNTS = {
+ GSTR1_ItemField.TAXABLE_VALUE.value: 0,
+ GSTR1_ItemField.IGST.value: 0,
+ GSTR1_ItemField.CESS.value: 0,
+ }
+ KEY_MAPPING = {
+ # GovDataFields.POS.value: DataFields.POS.value,
+ # GovDataFields.INVOICES.value: "invoices",
+ GovDataField.FLAG.value: "flag",
+ GovDataField.DOC_NUMBER.value: GSTR1_DataField.DOC_NUMBER.value,
+ GovDataField.DOC_DATE.value: GSTR1_DataField.DOC_DATE.value,
+ GovDataField.DOC_VALUE.value: GSTR1_DataField.DOC_VALUE.value,
+ # GovDataFields.ECOMMERCE_GSTIN.value: GSTR1_DataFields.ECOMMERCE_GSTIN.value,
+ GovDataField.DIFF_PERCENTAGE.value: GSTR1_DataField.DIFF_PERCENTAGE.value,
+ GovDataField.ITEMS.value: GSTR1_DataField.ITEMS.value,
+ # GovDataFields.INDEX.value: ItemFields.INDEX.value,
+ GovDataField.ITEM_DETAILS.value: GSTR1_ItemField.ITEM_DETAILS.value,
+ GovDataField.TAX_RATE.value: GSTR1_ItemField.TAX_RATE.value,
+ GovDataField.TAXABLE_VALUE.value: GSTR1_ItemField.TAXABLE_VALUE.value,
+ GovDataField.IGST.value: GSTR1_ItemField.IGST.value,
+ GovDataField.CESS.value: GSTR1_ItemField.CESS.value,
+ }
+
+ def __init__(self):
+ super().__init__()
+
+ self.value_formatters_for_internal = {
+ GovDataField.ITEMS.value: self.format_item_for_internal,
+ GovDataField.DOC_DATE.value: self.format_date_for_internal,
+ }
+ self.value_formatters_for_gov = {
+ GSTR1_DataField.ITEMS.value: self.format_item_for_gov,
+ GSTR1_DataField.DOC_DATE.value: self.format_date_for_gov,
+ }
+
+ def convert_to_internal_data_format(self, input_data):
+ output = {}
+
+ for pos_data in input_data:
+ pos = self.map_place_of_supply(pos_data.get(GovDataField.POS.value))
+
+ default_invoice_data = {
+ GSTR1_DataField.POS.value: pos,
+ GSTR1_DataField.DOC_TYPE.value: self.DOCUMENT_CATEGORY,
+ }
+
+ for invoice in pos_data.get(GovDataField.INVOICES.value):
+ invoice_level_data = self.format_data(invoice, default_invoice_data)
+ self.update_totals(
+ invoice_level_data,
+ invoice_level_data.get(GSTR1_DataField.ITEMS.value),
+ )
+
+ output[invoice_level_data[GSTR1_DataField.DOC_NUMBER.value]] = (
+ invoice_level_data
+ )
+
+ return {self.SUBCATEGORY: output}
+
+ def convert_to_gov_data_format(self, input_data, **kwargs):
+ pos_data = {}
+
+ for invoice in input_data:
+ pos = pos_data.setdefault(
+ invoice[GSTR1_DataField.POS.value],
+ {
+ GovDataField.POS.value: self.map_place_of_supply(
+ invoice[GSTR1_DataField.POS.value]
+ ),
+ GovDataField.INVOICES.value: [],
+ },
+ )
+
+ pos[GovDataField.INVOICES.value].append(
+ self.format_data(invoice, for_gov=True)
+ )
+
+ return list(pos_data.values())
+
+
+class Exports(GovDataMapper):
+ """
+ GST API Version - v4.0
+
+ Government Data Format:
+ {
+ 'exp_typ': 'WPAY',
+ 'inv': [
+ {
+ 'inum': '81542',
+ 'val': 995048.36,
+ 'itms': [
+ {
+ 'txval': 10000,
+ ...
+ },
+ ...
+ ],
+ ...
+ },
+ ...
+ ]
+ }
+
+ Internal Data Format:
+ {
+ 'Export With Payment of Tax': {
+ '81542': {
+ 'document_number': '81542',
+ 'document_value': 995048.36,
+ 'items': [
+ {
+ 'taxable_value': 10000,
+ ...
+ },
+ ...
+ ],
+ 'total_taxable_value': 10000,
+ ...
+ },
+ ...
+ }
+ }
+ """
+
+ DEFAULT_ITEM_AMOUNTS = {
+ GSTR1_ItemField.TAXABLE_VALUE.value: 0,
+ GSTR1_ItemField.IGST.value: 0,
+ GSTR1_ItemField.CESS.value: 0,
+ }
+ KEY_MAPPING = {
+ # GovDataFields.POS.value: DataFields.POS.value,
+ # GovDataFields.INVOICES.value: "invoices",
+ GovDataField.FLAG.value: "flag",
+ # GovDataFields.EXPORT_TYPE.value: DataFields.DOC_TYPE.value,
+ GovDataField.DOC_NUMBER.value: GSTR1_DataField.DOC_NUMBER.value,
+ GovDataField.DOC_DATE.value: GSTR1_DataField.DOC_DATE.value,
+ GovDataField.DOC_VALUE.value: GSTR1_DataField.DOC_VALUE.value,
+ GovDataField.SHIPPING_PORT_CODE.value: GSTR1_DataField.SHIPPING_PORT_CODE.value,
+ GovDataField.SHIPPING_BILL_NUMBER.value: GSTR1_DataField.SHIPPING_BILL_NUMBER.value,
+ GovDataField.SHIPPING_BILL_DATE.value: GSTR1_DataField.SHIPPING_BILL_DATE.value,
+ GovDataField.ITEMS.value: GSTR1_DataField.ITEMS.value,
+ GovDataField.TAXABLE_VALUE.value: GSTR1_ItemField.TAXABLE_VALUE.value,
+ GovDataField.TAX_RATE.value: GSTR1_ItemField.TAX_RATE.value,
+ GovDataField.IGST.value: GSTR1_ItemField.IGST.value,
+ GovDataField.CESS.value: GSTR1_ItemField.CESS.value,
+ }
+
+ SUBCATEGORIES = {
+ "WPAY": GSTR1_SubCategory.EXPWP.value,
+ "WOPAY": GSTR1_SubCategory.EXPWOP.value,
+ }
+
+ def __init__(self):
+ super().__init__()
+
+ self.value_formatters_for_internal = {
+ GovDataField.ITEMS.value: self.format_item_for_internal,
+ GovDataField.DOC_DATE.value: self.format_date_for_internal,
+ GovDataField.SHIPPING_BILL_DATE.value: self.format_date_for_internal,
+ }
+ self.value_formatters_for_gov = {
+ GSTR1_DataField.ITEMS.value: self.format_item_for_gov,
+ GSTR1_DataField.DOC_DATE.value: self.format_date_for_gov,
+ GSTR1_DataField.SHIPPING_BILL_DATE.value: self.format_date_for_gov,
+ }
+
+ def convert_to_internal_data_format(self, input_data):
+ output = {}
+
+ for export_category in input_data:
+ document_type = export_category.get(GovDataField.EXPORT_TYPE.value)
+ subcategory_data = output.setdefault(
+ self.SUBCATEGORIES.get(document_type, document_type), {}
+ )
+
+ default_invoice_data = {
+ GSTR1_DataField.DOC_TYPE.value: document_type,
+ }
+
+ for invoice in export_category.get(GovDataField.INVOICES.value):
+ invoice_level_data = self.format_data(invoice, default_invoice_data)
+
+ self.update_totals(
+ invoice_level_data,
+ invoice_level_data.get(GSTR1_DataField.ITEMS.value),
+ )
+ subcategory_data[
+ invoice_level_data[GSTR1_DataField.DOC_NUMBER.value]
+ ] = invoice_level_data
+
+ return output
+
+ def convert_to_gov_data_format(self, input_data, **kwargs):
+ export_category_wise_data = {}
+
+ for invoice in input_data:
+ export_category = export_category_wise_data.setdefault(
+ invoice[GSTR1_DataField.DOC_TYPE.value],
+ {
+ GovDataField.EXPORT_TYPE.value: invoice[
+ GSTR1_DataField.DOC_TYPE.value
+ ],
+ GovDataField.INVOICES.value: [],
+ },
+ )
+
+ export_category[GovDataField.INVOICES.value].append(
+ self.format_data(invoice, for_gov=True)
+ )
+
+ return list(export_category_wise_data.values())
+
+ def format_item_for_internal(self, items, *args):
+ return [
+ {
+ **self.DEFAULT_ITEM_AMOUNTS.copy(),
+ **self.format_data(item),
+ }
+ for item in items
+ ]
+
+ def format_item_for_gov(self, items, *args):
+ return [self.format_data(item, for_gov=True) for item in items]
+
+
+class B2CS(GovDataMapper):
+ """
+ GST API Version - v4.0
+
+ Government Data Format:
+ [
+ {
+ 'typ': 'OE',
+ 'pos': '05',
+ 'txval': 110,
+ ...
+ },
+ ...
+ ]
+
+ Internal Data Format:
+ {
+ 'B2C (Others)': {
+ '05-Uttarakhand - 5.0': [
+ {
+ 'total_taxable_value': 110,
+ 'document_type': 'OE',
+ 'place_of_supply': '05-Uttarakhand',
+ ...
+ },
+ ...
+ ],
+ ...
+ }
+ }
+ """
+
+ SUBCATEGORY = GSTR1_SubCategory.B2CS.value
+ KEY_MAPPING = {
+ GovDataField.FLAG.value: "flag",
+ # GovDataFields.SUPPLY_TYPE.value: "supply_type",
+ GovDataField.TAXABLE_VALUE.value: GSTR1_DataField.TAXABLE_VALUE.value,
+ GovDataField.TYPE.value: GSTR1_DataField.DOC_TYPE.value,
+ # GovDataFields.ECOMMERCE_GSTIN.value: GSTR1_DataFields.ECOMMERCE_GSTIN.value,
+ GovDataField.DIFF_PERCENTAGE.value: GSTR1_DataField.DIFF_PERCENTAGE.value,
+ GovDataField.POS.value: GSTR1_DataField.POS.value,
+ GovDataField.TAX_RATE.value: GSTR1_DataField.TAX_RATE.value,
+ GovDataField.IGST.value: GSTR1_DataField.IGST.value,
+ GovDataField.CGST.value: GSTR1_DataField.CGST.value,
+ GovDataField.SGST.value: GSTR1_DataField.SGST.value,
+ GovDataField.CESS.value: GSTR1_DataField.CESS.value,
+ }
+
+ def __init__(self):
+ super().__init__()
+
+ self.value_formatters_for_internal = {
+ GovDataField.ITEMS.value: self.format_item_for_internal,
+ GovDataField.POS.value: self.map_place_of_supply,
+ }
+ self.value_formatters_for_gov = {
+ GSTR1_DataField.ITEMS.value: self.format_item_for_gov,
+ GSTR1_DataField.POS.value: self.map_place_of_supply,
+ }
+
+ def convert_to_internal_data_format(self, input_data):
+ output = {}
+
+ for invoice in input_data:
+ invoice_data = self.format_data(invoice)
+
+ output.setdefault(
+ " - ".join(
+ (
+ invoice_data.get(GSTR1_DataField.POS.value, ""),
+ str(flt(invoice_data.get(GSTR1_DataField.TAX_RATE.value, ""))),
+ # invoice_data.get(GSTR1_DataFields.ECOMMERCE_GSTIN.value, ""),
+ )
+ ),
+ [],
+ ).append(invoice_data)
+
+ return {self.SUBCATEGORY: output}
+
+ def convert_to_gov_data_format(self, input_data, **kwargs):
+ self.company_gstin = kwargs.get("company_gstin", "")
+ return [self.format_data(invoice, for_gov=True) for invoice in input_data]
+
+ def format_data(self, data, default_data=None, for_gov=False):
+ data = super().format_data(data, default_data, for_gov)
+ if not for_gov:
+ return data
+
+ data[GovDataField.SUPPLY_TYPE.value] = (
+ "INTRA"
+ if data[GovDataField.POS.value] == self.company_gstin[:2]
+ else "INTER"
+ )
+ return data
+
+
+class NilRated(GovDataMapper):
+ """
+ GST API Version - v4.0
+
+ Government Data Format:
+ {
+ 'inv': [
+ {
+ 'sply_ty': 'INTRB2B',
+ 'expt_amt': 123.45,
+ 'nil_amt': 1470.85,
+ 'ngsup_amt': 1258.5
+ }
+ ]
+ }
+
+ Internal Data Format:
+ {
+ 'Nil-Rated, Exempted, Non-GST': {
+ 'Inter-State supplies to registered persons': [
+ {
+ 'document_type': 'Inter-State supplies to registered persons',
+ 'exempted_amount': 123.45,
+ 'nil_rated_amount': 1470.85,
+ 'non_gst_amount': 1258.5,
+ 'total_taxable_value': 2852.8
+ }
+ ]
+ }
+ }
+ """
+
+ SUBCATEGORY = GSTR1_SubCategory.NIL_EXEMPT.value
+ KEY_MAPPING = {
+ GovDataField.SUPPLY_TYPE.value: GSTR1_DataField.DOC_TYPE.value,
+ GovDataField.EXEMPTED_AMOUNT.value: GSTR1_DataField.EXEMPTED_AMOUNT.value,
+ GovDataField.NIL_RATED_AMOUNT.value: GSTR1_DataField.NIL_RATED_AMOUNT.value,
+ GovDataField.NON_GST_AMOUNT.value: GSTR1_DataField.NON_GST_AMOUNT.value,
+ }
+
+ DOCUMENT_CATEGORIES = {
+ "INTRB2B": "Inter-State supplies to registered persons",
+ "INTRB2C": "Inter-State supplies to unregistered persons",
+ "INTRAB2B": "Intra-State supplies to registered persons",
+ "INTRAB2C": "Intra-State supplies to unregistered persons",
+ }
+
+ def __init__(self):
+ super().__init__()
+
+ self.value_formatters_for_internal = {
+ GovDataField.SUPPLY_TYPE.value: self.document_category_mapping
+ }
+ self.value_formatters_for_gov = {
+ GSTR1_DataField.DOC_TYPE.value: self.document_category_mapping
+ }
+
+ def convert_to_internal_data_format(self, input_data):
+ output = {}
+
+ for invoice in input_data[GovDataField.INVOICES.value]:
+ invoice_data = self.format_data(invoice)
+
+ if not invoice_data:
+ continue
+
+ output.setdefault(
+ invoice_data.get(GSTR1_DataField.DOC_TYPE.value), []
+ ).append(invoice_data)
+
+ return {self.SUBCATEGORY: output}
+
+ def convert_to_gov_data_format(self, input_data, **kwargs):
+ self.DOCUMENT_CATEGORIES = self.reverse_dict(self.DOCUMENT_CATEGORIES)
+
+ return {
+ GovDataField.INVOICES.value: [
+ self.format_data(invoice, for_gov=True) for invoice in input_data
+ ]
+ }
+
+ def format_data(self, data, default_data=None, for_gov=False):
+ invoice_data = super().format_data(data, default_data, for_gov)
+
+ if for_gov:
+ return invoice_data
+
+ # No need to discard if zero fields
+ amounts = [
+ invoice_data.get(GSTR1_DataField.EXEMPTED_AMOUNT.value, 0),
+ invoice_data.get(GSTR1_DataField.NIL_RATED_AMOUNT.value, 0),
+ invoice_data.get(GSTR1_DataField.NON_GST_AMOUNT.value, 0),
+ ]
+
+ if all(amount == 0 for amount in amounts):
+ return
+
+ invoice_data[GSTR1_DataField.TAXABLE_VALUE.value] = sum(amounts)
+ return invoice_data
+
+ # value formatters
+ def document_category_mapping(self, doc_category, data):
+ return self.DOCUMENT_CATEGORIES.get(doc_category, doc_category)
+
+
+class CDNR(GovDataMapper):
+ """
+ GST API Version - v4.0
+
+ Government Data Format:
+ [
+ {
+ 'ctin': '24AANFA2641L1ZF',
+ 'nt': [
+ {
+ 'ntty': 'C',
+ 'nt_num': '533515',
+ 'val': 123123,
+ 'itms': [
+ {'num': 1,'itm_det': {'txval': 5225.28,
+ ...
+ }},
+ ...
+ ],
+ ...
+ },
+ ...
+ ]
+ },
+ ...
+ ]
+
+ Internal Data Format:
+ {
+ 'Credit/Debit Notes (Registered)': {
+ '533515': {
+ 'transaction_type': 'Credit Note',
+ 'document_number': '533515',
+ 'items': [
+ {
+ 'taxable_value': -5225.28,
+ ...
+ },
+ ...
+ ],
+ 'total_taxable_value': -10450.56,
+ ...
+ },
+ ...
+ }
+ }
+ """
+
+ SUBCATEGORY = GSTR1_SubCategory.CDNR.value
+ KEY_MAPPING = {
+ # GovDataFields.CUST_GSTIN.value: DataFields.CUST_GSTIN.value,
+ GovDataField.FLAG.value: "flag",
+ # GovDataFields.NOTE_DETAILS.value: "credit_debit_note_details",
+ GovDataField.NOTE_TYPE.value: GSTR1_DataField.TRANSACTION_TYPE.value,
+ GovDataField.NOTE_NUMBER.value: GSTR1_DataField.DOC_NUMBER.value,
+ GovDataField.NOTE_DATE.value: GSTR1_DataField.DOC_DATE.value,
+ GovDataField.POS.value: GSTR1_DataField.POS.value,
+ GovDataField.REVERSE_CHARGE.value: GSTR1_DataField.REVERSE_CHARGE.value,
+ GovDataField.INVOICE_TYPE.value: GSTR1_DataField.DOC_TYPE.value,
+ GovDataField.DOC_VALUE.value: GSTR1_DataField.DOC_VALUE.value,
+ GovDataField.DIFF_PERCENTAGE.value: GSTR1_DataField.DIFF_PERCENTAGE.value,
+ GovDataField.ITEMS.value: GSTR1_DataField.ITEMS.value,
+ # GovDataFields.INDEX.value: ItemFields.INDEX.value,
+ # GovDataFields.ITEM_DETAILS.value: "item_details",
+ GovDataField.TAX_RATE.value: GSTR1_ItemField.TAX_RATE.value,
+ GovDataField.TAXABLE_VALUE.value: GSTR1_ItemField.TAXABLE_VALUE.value,
+ GovDataField.IGST.value: GSTR1_ItemField.IGST.value,
+ GovDataField.SGST.value: GSTR1_ItemField.SGST.value,
+ GovDataField.CGST.value: GSTR1_ItemField.CGST.value,
+ GovDataField.CESS.value: GSTR1_ItemField.CESS.value,
+ }
+
+ DOCUMENT_CATEGORIES = {
+ "R": "Regular B2B",
+ "SEWP": "SEZ supplies with payment",
+ "SEWOP": "SEZ supplies without payment",
+ "DE": "Deemed Exports",
+ }
+
+ DOCUMENT_TYPES = {
+ "C": "Credit Note",
+ "D": "Debit Note",
+ }
+
+ def __init__(self):
+ super().__init__()
+
+ self.value_formatters_for_internal = {
+ GovDataField.ITEMS.value: self.format_item_for_internal,
+ GovDataField.NOTE_TYPE.value: self.document_type_mapping,
+ GovDataField.POS.value: self.map_place_of_supply,
+ GovDataField.INVOICE_TYPE.value: self.document_category_mapping,
+ GovDataField.DOC_VALUE.value: self.format_doc_value,
+ GovDataField.NOTE_DATE.value: self.format_date_for_internal,
+ }
+
+ self.value_formatters_for_gov = {
+ GSTR1_DataField.ITEMS.value: self.format_item_for_gov,
+ GSTR1_DataField.TRANSACTION_TYPE.value: self.document_type_mapping,
+ GSTR1_DataField.POS.value: self.map_place_of_supply,
+ GSTR1_DataField.DOC_TYPE.value: self.document_category_mapping,
+ GSTR1_DataField.DOC_VALUE.value: lambda val, *args: abs(val),
+ GSTR1_DataField.DOC_DATE.value: self.format_date_for_gov,
+ }
+
+ def convert_to_internal_data_format(self, input_data):
+ output = {}
+
+ for customer_data in input_data:
+ customer_gstin = customer_data.get(GovDataField.CUST_GSTIN.value)
+
+ for document in customer_data.get(GovDataField.NOTE_DETAILS.value):
+ document_data = self.format_data(
+ document,
+ {
+ GSTR1_DataField.CUST_GSTIN.value: customer_gstin,
+ GSTR1_DataField.CUST_NAME.value: self.guess_customer_name(
+ customer_gstin
+ ),
+ },
+ )
+ self.update_totals(
+ document_data, document_data.get(GSTR1_DataField.ITEMS.value)
+ )
+ output[document_data[GSTR1_DataField.DOC_NUMBER.value]] = document_data
+
+ return {self.SUBCATEGORY: output}
+
+ def convert_to_gov_data_format(self, input_data, **kwargs):
+ customer_data = {}
+
+ self.DOCUMENT_CATEGORIES = self.reverse_dict(self.DOCUMENT_CATEGORIES)
+ self.DOCUMENT_TYPES = self.reverse_dict(self.DOCUMENT_TYPES)
+
+ for document in input_data:
+ customer_gstin = document[GSTR1_DataField.CUST_GSTIN.value]
+ customer = customer_data.setdefault(
+ customer_gstin,
+ {
+ GovDataField.CUST_GSTIN.value: customer_gstin,
+ GovDataField.NOTE_DETAILS.value: [],
+ },
+ )
+ customer[GovDataField.NOTE_DETAILS.value].append(
+ self.format_data(document, for_gov=True)
+ )
+
+ return list(customer_data.values())
+
+ def format_item_for_internal(self, items, *args):
+ formatted_items = super().format_item_for_internal(items, *args)
+
+ data = args[0]
+ if data[GovDataField.NOTE_TYPE.value] == "D":
+ return formatted_items
+
+ # for credit notes amounts -ve
+ for item in formatted_items:
+ item.update(
+ {
+ key: value * -1
+ for key, value in item.items()
+ if key in list(self.DEFAULT_ITEM_AMOUNTS.keys())
+ }
+ )
+
+ return formatted_items
+
+ def format_item_for_gov(self, items, *args):
+ keys = set((self.DEFAULT_ITEM_AMOUNTS.keys()))
+ # for credit notes amounts -ve
+ for item in items:
+ for key, value in item.items():
+ if key in keys:
+ item[key] = abs(value)
+
+ return super().format_item_for_gov(items, *args)
+
+ def document_type_mapping(self, doc_type, data):
+ return self.DOCUMENT_TYPES.get(doc_type, doc_type)
+
+ def document_category_mapping(self, doc_category, data):
+ return self.DOCUMENT_CATEGORIES.get(doc_category, doc_category)
+
+ def format_doc_value(self, value, data):
+ return value * -1 if data[GovDataField.NOTE_TYPE.value] == "C" else value
+
+
+class CDNUR(GovDataMapper):
+ """
+ GST API Version - v4.0
+
+ Government Data Format:
+ [
+ {
+ 'ntty': 'C',
+ 'nt_num': '533515',
+ 'itms': [
+ {'num': 1,'itm_det': { 'txval': 5225.28,
+ ...
+ }},
+ ...
+ ],
+ ...
+ },
+ ...
+ ]
+
+ Internal Data Format:
+ {
+ 'Credit/Debit Notes (Unregistered)': {
+ '533515': {
+ 'transaction_type': 'Credit Note',
+ 'document_number': '533515',
+ 'items': [
+ {
+ 'taxable_value': -5225.28,
+ ...
+ }
+ ],
+ 'total_taxable_value': -5225.28,
+ ...
+ },
+ ...
+ }
+ }
+ """
+
+ SUBCATEGORY = GSTR1_SubCategory.CDNUR.value
+ DEFAULT_ITEM_AMOUNTS = {
+ GSTR1_ItemField.TAXABLE_VALUE.value: 0,
+ GSTR1_ItemField.IGST.value: 0,
+ GSTR1_ItemField.CESS.value: 0,
+ }
+ KEY_MAPPING = {
+ GovDataField.FLAG.value: "flag",
+ GovDataField.TYPE.value: GSTR1_DataField.DOC_TYPE.value,
+ GovDataField.NOTE_TYPE.value: GSTR1_DataField.TRANSACTION_TYPE.value,
+ GovDataField.NOTE_NUMBER.value: GSTR1_DataField.DOC_NUMBER.value,
+ GovDataField.NOTE_DATE.value: GSTR1_DataField.DOC_DATE.value,
+ GovDataField.DOC_VALUE.value: GSTR1_DataField.DOC_VALUE.value,
+ GovDataField.POS.value: GSTR1_DataField.POS.value,
+ GovDataField.DIFF_PERCENTAGE.value: GSTR1_DataField.DIFF_PERCENTAGE.value,
+ GovDataField.ITEMS.value: GSTR1_DataField.ITEMS.value,
+ GovDataField.TAX_RATE.value: GSTR1_ItemField.TAX_RATE.value,
+ GovDataField.TAXABLE_VALUE.value: GSTR1_ItemField.TAXABLE_VALUE.value,
+ GovDataField.IGST.value: GSTR1_ItemField.IGST.value,
+ GovDataField.CESS.value: GSTR1_ItemField.CESS.value,
+ }
+ DOCUMENT_TYPES = {
+ "C": "Credit Note",
+ "D": "Debit Note",
+ }
+
+ def __init__(self):
+ super().__init__()
+
+ self.value_formatters_for_internal = {
+ GovDataField.ITEMS.value: self.format_item_for_internal,
+ GovDataField.NOTE_TYPE.value: self.document_type_mapping,
+ GovDataField.POS.value: self.map_place_of_supply,
+ GovDataField.DOC_VALUE.value: self.format_doc_value,
+ GovDataField.NOTE_DATE.value: self.format_date_for_internal,
+ }
+
+ self.value_formatters_for_gov = {
+ GSTR1_DataField.ITEMS.value: self.format_item_for_gov,
+ GSTR1_DataField.TRANSACTION_TYPE.value: self.document_type_mapping,
+ GSTR1_DataField.POS.value: self.map_place_of_supply,
+ GSTR1_DataField.DOC_VALUE.value: lambda x, *args: abs(x),
+ GSTR1_DataField.DOC_DATE.value: self.format_date_for_gov,
+ }
+
+ def convert_to_internal_data_format(self, input_data):
+ output = {}
+
+ for invoice in input_data:
+ invoice_data = self.format_data(invoice)
+ self.update_totals(
+ invoice_data, invoice_data.get(GSTR1_DataField.ITEMS.value)
+ )
+ output[invoice_data[GSTR1_DataField.DOC_NUMBER.value]] = invoice_data
+
+ return {self.SUBCATEGORY: output}
+
+ def convert_to_gov_data_format(self, input_data, **kwargs):
+ self.DOCUMENT_TYPES = self.reverse_dict(self.DOCUMENT_TYPES)
+ return [self.format_data(invoice, for_gov=True) for invoice in input_data]
+
+ def format_item_for_internal(self, items, *args):
+ formatted_items = super().format_item_for_internal(items, *args)
+
+ data = args[0]
+ if data[GovDataField.NOTE_TYPE.value] == "D":
+ return formatted_items
+
+ # for credit notes amounts -ve
+ for item in formatted_items:
+ item.update(
+ {
+ key: value * -1
+ for key, value in item.items()
+ if key in list(self.DEFAULT_ITEM_AMOUNTS.keys())
+ }
+ )
+
+ return formatted_items
+
+ def format_item_for_gov(self, items, *args):
+ keys = set(self.DEFAULT_ITEM_AMOUNTS.keys())
+ # for credit notes amounts -ve
+ for item in items:
+ for key, value in item.items():
+ if key in keys:
+ item[key] = abs(value)
+
+ return super().format_item_for_gov(items, *args)
+
+ # value formatters
+ def document_type_mapping(self, doc_type, data):
+ return self.DOCUMENT_TYPES.get(doc_type, doc_type)
+
+ def format_doc_value(self, value, data):
+ return value * -1 if data[GovDataField.NOTE_TYPE.value] == "C" else value
+
+
+class HSNSUM(GovDataMapper):
+ """
+ GST API Version - v4.0
+
+ Government Data Format:
+ {
+ 'data': [
+ {
+ 'num': 1,
+ 'hsn_sc': '1010',
+ 'desc': 'Goods Description',
+ 'uqc': 'KGS',
+ 'qty': 2.05,
+ 'txval': 10.23,
+ 'iamt': 14.52,
+ 'csamt': 500,
+ 'rt': 0.1
+ }
+ ]
+ }
+
+ Internal Data Format:
+ {
+ 'HSN Summary': {
+ '1010 - KGS-KILOGRAMS - 0.1': {
+ 'hsn_code': '1010',
+ 'description': 'Goods Description',
+ 'uom': 'KGS-KILOGRAMS',
+ 'quantity': 2.05,
+ 'total_taxable_value': 10.23,
+ 'total_igst_amount': 14.52,
+ 'total_cess_amount': 500,
+ 'tax_rate': 0.1
+ }
+ }
+ }
+ """
+
+ SUBCATEGORY = GSTR1_SubCategory.HSN.value
+ KEY_MAPPING = {
+ # GovDataFields.INDEX.value: ItemFields.INDEX.value,
+ GovDataField.HSN_CODE.value: GSTR1_DataField.HSN_CODE.value,
+ GovDataField.DESCRIPTION.value: GSTR1_DataField.DESCRIPTION.value,
+ GovDataField.UOM.value: GSTR1_DataField.UOM.value,
+ GovDataField.QUANTITY.value: GSTR1_DataField.QUANTITY.value,
+ GovDataField.TAXABLE_VALUE.value: GSTR1_DataField.TAXABLE_VALUE.value,
+ GovDataField.IGST.value: GSTR1_DataField.IGST.value,
+ GovDataField.CGST.value: GSTR1_DataField.CGST.value,
+ GovDataField.SGST.value: GSTR1_DataField.SGST.value,
+ GovDataField.CESS.value: GSTR1_DataField.CESS.value,
+ GovDataField.TAX_RATE.value: GSTR1_ItemField.TAX_RATE.value,
+ }
+
+ def __init__(self):
+ super().__init__()
+ self.value_formatters_for_internal = {GovDataField.UOM.value: self.map_uom}
+ self.value_formatters_for_gov = {
+ GSTR1_DataField.UOM.value: self.map_uom,
+ GSTR1_DataField.DESCRIPTION.value: lambda x, *args: x[:30],
+ }
+
+ def convert_to_internal_data_format(self, input_data):
+ output = {}
+
+ for invoice in input_data[GovDataField.HSN_DATA.value]:
+ output[
+ " - ".join(
+ (
+ invoice.get(GovDataField.HSN_CODE.value, ""),
+ self.map_uom(invoice.get(GovDataField.UOM.value, "")),
+ str(flt(invoice.get(GovDataField.TAX_RATE.value))),
+ )
+ )
+ ] = self.format_data(invoice)
+
+ return {self.SUBCATEGORY: output}
+
+ def convert_to_gov_data_format(self, input_data, **kwargs):
+ return {
+ GovDataField.HSN_DATA.value: [
+ self.format_data(
+ invoice, {GovDataField.INDEX.value: index + 1}, for_gov=True
+ )
+ for index, invoice in enumerate(input_data)
+ ]
+ }
+
+ def format_data(self, data, default_data=None, for_gov=False):
+ data = super().format_data(data, default_data, for_gov)
+
+ if for_gov:
+ return data
+
+ data[GSTR1_DataField.DOC_VALUE.value] = sum(
+ (
+ data.get(GSTR1_DataField.TAXABLE_VALUE.value, 0),
+ data.get(GSTR1_DataField.IGST.value, 0),
+ data.get(GSTR1_DataField.CGST.value, 0),
+ data.get(GSTR1_DataField.SGST.value, 0),
+ data.get(GSTR1_DataField.CESS.value, 0),
+ )
+ )
+
+ return data
+
+ def map_uom(self, uom, data=None):
+ uom = uom.upper()
+
+ if "-" in uom:
+ if data and data.get(GSTR1_DataField.HSN_CODE.value, "").startswith("99"):
+ return "NA"
+ else:
+ return uom.split("-")[0]
+
+ if uom in UOM_MAP:
+ return f"{uom}-{UOM_MAP[uom]}"
+
+ return f"OTH-{UOM_MAP.get('OTH')}"
+
+
+class AT(GovDataMapper):
+ """
+ GST API Version - v4.0
+
+ Government Data Format:
+ [
+ {
+ 'pos': '05',
+ 'itms': [
+ {
+ 'rt': 5,
+ 'ad_amt': 100,
+ ...
+ },
+ ...
+ ],
+ ...
+ },
+ ...
+ ]
+
+ Internal Data Format:
+ {
+ 'Advances Received': {
+ '05-Uttarakhand - 5.0': [
+ {
+ 'place_of_supply': '05-Uttarakhand',
+ 'total_taxable_value': 100,
+ 'tax_rate': 5,
+ ...
+ },
+ ...
+ ],
+ ...
+ }
+ }
+ """
+
+ SUBCATEGORY = GSTR1_SubCategory.AT.value
+ KEY_MAPPING = {
+ GovDataField.FLAG.value: "flag",
+ GovDataField.POS.value: GSTR1_DataField.POS.value,
+ GovDataField.DIFF_PERCENTAGE.value: GSTR1_DataField.DIFF_PERCENTAGE.value,
+ GovDataField.ITEMS.value: GSTR1_DataField.ITEMS.value,
+ GovDataField.TAX_RATE.value: GSTR1_ItemField.TAX_RATE.value,
+ GovDataField.ADVANCE_AMOUNT.value: GSTR1_DataField.TAXABLE_VALUE.value,
+ GovDataField.IGST.value: GSTR1_DataField.IGST.value,
+ GovDataField.CGST.value: GSTR1_DataField.CGST.value,
+ GovDataField.SGST.value: GSTR1_DataField.SGST.value,
+ GovDataField.CESS.value: GSTR1_DataField.CESS.value,
+ }
+ DEFAULT_ITEM_AMOUNTS = {
+ GSTR1_DataField.IGST.value: 0,
+ GSTR1_DataField.CESS.value: 0,
+ GSTR1_DataField.CGST.value: 0,
+ GSTR1_DataField.SGST.value: 0,
+ GSTR1_DataField.TAXABLE_VALUE.value: 0,
+ }
+ MULTIPLIER = 1
+
+ def __init__(self):
+ super().__init__()
+
+ self.value_formatters_for_internal = {
+ GovDataField.ITEMS.value: self.format_item_for_internal,
+ GovDataField.POS.value: self.map_place_of_supply,
+ }
+
+ self.value_formatters_for_gov = {
+ # GSTR1_DataField.ITEMS.value: self.format_item_for_gov,
+ GSTR1_DataField.POS.value: self.map_place_of_supply,
+ }
+
+ def convert_to_internal_data_format(self, input_data):
+ output = {}
+
+ for invoice in input_data:
+ invoice_data = self.format_data(invoice)
+ items = invoice_data.pop(GSTR1_DataField.ITEMS.value)
+
+ for item in items:
+ if self.MULTIPLIER != 1:
+ item.update(
+ {
+ key: value * self.MULTIPLIER
+ for key, value in item.items()
+ if key in self.DEFAULT_ITEM_AMOUNTS
+ }
+ )
+
+ item_data = invoice_data.copy()
+ item_data.update(item)
+ output[
+ " - ".join(
+ (
+ invoice_data.get(GSTR1_DataField.POS.value, ""),
+ str(flt(item_data.get(GSTR1_DataField.TAX_RATE.value, ""))),
+ )
+ )
+ ] = [item_data]
+
+ return {self.SUBCATEGORY: output}
+
+ def convert_to_gov_data_format(self, input_data, **kwargs):
+ self.company_gstin = kwargs.get("company_gstin", "")
+ pos_wise_data = {}
+
+ for invoice in input_data:
+ formatted_data = self.format_data(invoice, for_gov=True)
+ rate_wise_taxes = self.get_item_details(formatted_data)
+
+ pos_data = pos_wise_data.setdefault(
+ invoice[GSTR1_DataField.POS.value], formatted_data
+ )
+
+ pos_data.setdefault(GovDataField.ITEMS.value, []).extend(
+ rate_wise_taxes[GovDataField.ITEMS.value]
+ )
+
+ return list(pos_wise_data.values())
+
+ def get_item_details(self, invoice):
+ """
+ Transfer document values to item level (by POS and tax rate)
+ """
+ return {
+ GovDataField.ITEMS.value: [
+ {
+ key: invoice.pop(key)
+ for key in [
+ GovDataField.IGST.value,
+ GovDataField.CESS.value,
+ GovDataField.CGST.value,
+ GovDataField.SGST.value,
+ GovDataField.ADVANCE_AMOUNT.value,
+ GovDataField.TAX_RATE.value,
+ ]
+ }
+ ]
+ }
+
+ def format_data(self, data, default_data=None, for_gov=False):
+ if self.MULTIPLIER != 1 and for_gov:
+ data.update(
+ {
+ key: value * self.MULTIPLIER
+ for key, value in data.items()
+ if key in self.DEFAULT_ITEM_AMOUNTS
+ }
+ )
+
+ data = super().format_data(data, default_data, for_gov)
+
+ if not for_gov:
+ return data
+
+ data[GovDataField.SUPPLY_TYPE.value] = (
+ "INTRA"
+ if data[GovDataField.POS.value] == self.company_gstin[:2]
+ else "INTER"
+ )
+ return data
+
+ def format_item_for_internal(self, items, *args):
+ return [
+ {
+ **self.DEFAULT_ITEM_AMOUNTS.copy(),
+ **self.format_data(item),
+ }
+ for item in items
+ ]
+
+ def format_item_for_gov(self, items, *args):
+ return [self.format_data(item, for_gov=True) for item in items]
+
+
+class TXPD(AT):
+ SUBCATEGORY = GSTR1_SubCategory.TXP.value
+ MULTIPLIER = -1
+
+
+class DOC_ISSUE(GovDataMapper):
+ """
+ GST API Version - v4.0
+
+ Government Data Format:
+ {
+ 'doc_det': [
+ {
+ 'doc_num': 1,
+ 'docs': [
+ {
+ 'num': 1,
+ 'from': '1',
+ 'to': '10',
+ 'totnum': 10,
+ 'cancel': 0,
+ 'net_issue': 10
+ }
+ ]
+ }
+ ]
+ }
+
+ Internal Data Format:
+ {
+ 'Document Issued': {
+ 'Invoices for outward supply - 1': {
+ 'document_type': 'Invoices for outward supply',
+ 'from_sr_no': '1',
+ 'to_sr_no': '10',
+ 'total_count': 10,
+ 'cancelled_count': 0,
+ 'net_issue': 10
+ }
+ }
+ }
+ """
+
+ KEY_MAPPING = {
+ # GovDataFields.INDEX.value: ItemFields.INDEX.value,
+ GovDataField.FROM_SR.value: GSTR1_DataField.FROM_SR.value,
+ GovDataField.TO_SR.value: GSTR1_DataField.TO_SR.value,
+ GovDataField.TOTAL_COUNT.value: GSTR1_DataField.TOTAL_COUNT.value,
+ GovDataField.CANCELLED_COUNT.value: GSTR1_DataField.CANCELLED_COUNT.value,
+ GovDataField.NET_ISSUE.value: GSTR1_DataField.NET_ISSUE.value,
+ }
+ DOCUMENT_NATURE = {
+ 1: "Invoices for outward supply",
+ 2: "Invoices for inward supply from unregistered person",
+ 3: "Revised Invoice",
+ 4: "Debit Note",
+ 5: "Credit Note",
+ 6: "Receipt voucher",
+ 7: "Payment Voucher",
+ 8: "Refund voucher",
+ 9: "Delivery Challan for job work",
+ 10: "Delivery Challan for supply on approval",
+ 11: "Delivery Challan in case of liquid gas",
+ 12: "Delivery Challan in cases other than by way of supply (excluding at S no. 9 to 11)",
+ }
+
+ def __init__(self):
+ super().__init__()
+
+ def convert_to_internal_data_format(self, input_data):
+ output = {}
+
+ for document in input_data[GovDataField.DOC_ISSUE_DETAILS.value]:
+ document_nature = self.get_document_nature(
+ document.get(GovDataField.DOC_ISSUE_NUMBER.value, "")
+ )
+ output.update(
+ {
+ " - ".join(
+ (document_nature, doc.get(GovDataField.FROM_SR.value))
+ ): self.format_data(
+ doc, {GSTR1_DataField.DOC_TYPE.value: document_nature}
+ )
+ for doc in document[GovDataField.DOC_ISSUE_LIST.value]
+ }
+ )
+
+ return {GSTR1_SubCategory.DOC_ISSUE.value: output}
+
+ def convert_to_gov_data_format(self, input_data, **kwargs):
+ self.DOCUMENT_NATURE = self.reverse_dict(self.DOCUMENT_NATURE)
+
+ output = {GovDataField.DOC_ISSUE_DETAILS.value: []}
+ doc_nature_wise_data = {}
+
+ for invoice in input_data:
+ doc_nature_wise_data.setdefault(
+ invoice[GSTR1_DataField.DOC_TYPE.value], []
+ ).append(invoice)
+
+ input_data = doc_nature_wise_data
+
+ output = {
+ GovDataField.DOC_ISSUE_DETAILS.value: [
+ {
+ GovDataField.DOC_ISSUE_NUMBER.value: self.get_document_nature(
+ doc_nature
+ ),
+ GovDataField.DOC_ISSUE_LIST.value: [
+ self.format_data(
+ document,
+ {GovDataField.INDEX.value: index + 1},
+ for_gov=True,
+ )
+ for index, document in enumerate(documents)
+ ],
+ }
+ for doc_nature, documents in doc_nature_wise_data.items()
+ ]
+ }
+
+ return output
+
+ def format_data(self, data, additional_data=None, for_gov=False):
+ if not for_gov:
+ return super().format_data(data, additional_data)
+
+ # compute additional data
+ data[GSTR1_DataField.CANCELLED_COUNT.value] += data.get(
+ GSTR1_DataField.DRAFT_COUNT.value, 0
+ )
+ data["net_issue"] = data[GSTR1_DataField.TOTAL_COUNT.value] - data.get(
+ GSTR1_DataField.CANCELLED_COUNT.value, 0
+ )
+
+ return super().format_data(data, additional_data, for_gov)
+
+ def get_document_nature(self, doc_nature, *args):
+ return self.DOCUMENT_NATURE.get(doc_nature, doc_nature)
+
+
+class SUPECOM(GovDataMapper):
+ """
+ GST API Version - v4.0
+
+ Government Data Format:
+ {
+ 'clttx': [
+ {
+ 'etin': '20ALYPD6528PQC5',
+ 'suppval': 10000,
+ 'igst': 1000,
+ 'cgst': 0,
+ 'sgst': 0,
+ 'cess': 0
+ }
+ ]
+ }
+
+ Internal Data Format:
+ {
+ 'TCS collected by E-commerce Operator u/s 52': {
+ '20ALYPD6528PQC5': {
+ 'document_type': 'TCS collected by E-commerce Operator u/s 52',
+ 'ecommerce_gstin': '20ALYPD6528PQC5',
+ 'total_taxable_value': 10000,
+ 'igst_amount': 1000,
+ 'cgst_amount': 0,
+ 'sgst_amount': 0,
+ 'cess_amount': 0
+ }
+ }
+ }
+ """
+
+ KEY_MAPPING = {
+ GovDataField.ECOMMERCE_GSTIN.value: GSTR1_DataField.ECOMMERCE_GSTIN.value,
+ GovDataField.NET_TAXABLE_VALUE.value: GSTR1_DataField.TAXABLE_VALUE.value,
+ "igst": GSTR1_ItemField.IGST.value,
+ "cgst": GSTR1_ItemField.CGST.value,
+ "sgst": GSTR1_ItemField.SGST.value,
+ "cess": GSTR1_ItemField.CESS.value,
+ GovDataField.FLAG.value: "flag",
+ }
+ DOCUMENT_CATEGORIES = {
+ GovDataField.SUPECOM_52.value: GSTR1_SubCategory.SUPECOM_52.value,
+ GovDataField.SUPECOM_9_5.value: GSTR1_SubCategory.SUPECOM_9_5.value,
+ }
+
+ def __init__(self):
+ super().__init__()
+
+ def convert_to_internal_data_format(self, input_data):
+ output = {}
+
+ for section, invoices in input_data.items():
+ document_type = self.DOCUMENT_CATEGORIES.get(section, section)
+ output[document_type] = {
+ invoice.get(GovDataField.ECOMMERCE_GSTIN.value, ""): self.format_data(
+ invoice, {GSTR1_DataField.DOC_TYPE.value: document_type}
+ )
+ for invoice in invoices
+ }
+
+ return output
+
+ def convert_to_gov_data_format(self, input_data, **kwargs):
+ output = {}
+ self.DOCUMENT_CATEGORIES = self.reverse_dict(self.DOCUMENT_CATEGORIES)
+
+ for invoice in input_data:
+ section = invoice[GSTR1_DataField.DOC_TYPE.value]
+ output.setdefault(
+ self.DOCUMENT_CATEGORIES.get(section, section), []
+ ).append(self.format_data(invoice, for_gov=True))
+ return output
+
+
+class RETSUM(GovDataMapper):
+ """
+ Convert GSTR-1 Summary as returned by the API to the internal format
+
+ Usecase: Compute amendment liability for GSTR-1 Summary
+
+ Exceptions:
+ - Only supports latest summary format v4.0 and above
+ """
+
+ KEY_MAPPING = {
+ "sec_nm": GSTR1_DataField.DESCRIPTION.value,
+ "typ": GSTR1_DataField.DESCRIPTION.value,
+ "ttl_rec": "no_of_records",
+ "ttl_val": "total_document_value",
+ "ttl_igst": GSTR1_DataField.IGST.value,
+ "ttl_cgst": GSTR1_DataField.CGST.value,
+ "ttl_sgst": GSTR1_DataField.SGST.value,
+ "ttl_cess": GSTR1_DataField.CESS.value,
+ "ttl_tax": GSTR1_DataField.TAXABLE_VALUE.value,
+ "act_val": "actual_document_value",
+ "act_igst": "actual_igst",
+ "act_sgst": "actual_sgst",
+ "act_cgst": "actual_cgst",
+ "act_cess": "actual_cess",
+ "act_tax": "actual_taxable_value",
+ "ttl_expt_amt": f"total_{GSTR1_DataField.EXEMPTED_AMOUNT.value}",
+ "ttl_ngsup_amt": f"total_{GSTR1_DataField.NON_GST_AMOUNT.value}",
+ "ttl_nilsup_amt": f"total_{GSTR1_DataField.NIL_RATED_AMOUNT.value}",
+ "ttl_doc_issued": GSTR1_DataField.TOTAL_COUNT.value,
+ "ttl_doc_cancelled": GSTR1_DataField.CANCELLED_COUNT.value,
+ }
+
+ SECTION_NAMES = {
+ "AT": GSTR1_Category.AT.value,
+ "B2B_4A": GSTR1_SubCategory.B2B_REGULAR.value,
+ "B2B_4B": GSTR1_SubCategory.B2B_REVERSE_CHARGE.value,
+ "B2B_6C": GSTR1_SubCategory.DE.value,
+ "B2B_SEZWOP": GSTR1_SubCategory.SEZWOP.value,
+ "B2B_SEZWP": GSTR1_SubCategory.SEZWP.value,
+ "B2B": GSTR1_Category.B2B.value,
+ "B2CL": GSTR1_Category.B2CL.value,
+ "B2CS": GSTR1_Category.B2CS.value,
+ "TXPD": GSTR1_Category.TXP.value,
+ "EXP": GSTR1_Category.EXP.value,
+ "CDNR": GSTR1_Category.CDNR.value,
+ "CDNUR": GSTR1_Category.CDNUR.value,
+ "SUPECOM": GSTR1_Category.SUPECOM.value,
+ "ECOM": "ECOM",
+ "ECOM_REG": "ECOM_REG",
+ "ECOM_DE": "ECOM_DE",
+ "ECOM_SEZWOP": "ECOM_SEZWOP",
+ "ECOM_SEZWP": "ECOM_SEZWP",
+ "ECOM_UNREG": "ECOM_UNREG",
+ "ATA": f"{GSTR1_Category.AT.value} (Amended)",
+ "B2BA_4A": f"{GSTR1_SubCategory.B2B_REGULAR.value} (Amended)",
+ "B2BA_4B": f"{GSTR1_SubCategory.B2B_REVERSE_CHARGE.value} (Amended)",
+ "B2BA_6C": f"{GSTR1_SubCategory.DE.value} (Amended)",
+ "B2BA_SEZWOP": f"{GSTR1_SubCategory.SEZWOP.value} (Amended)",
+ "B2BA_SEZWP": f"{GSTR1_SubCategory.SEZWP.value} (Amended)",
+ "B2BA": f"{GSTR1_Category.B2B.value} (Amended)",
+ "B2CLA": f"{GSTR1_Category.B2CL.value} (Amended)",
+ "B2CSA": f"{GSTR1_Category.B2CS.value} (Amended)",
+ "TXPDA": f"{GSTR1_Category.TXP.value} (Amended)",
+ "EXPA": f"{GSTR1_Category.EXP.value} (Amended)",
+ "CDNRA": f"{GSTR1_Category.CDNR.value} (Amended)",
+ "CDNURA": f"{GSTR1_Category.CDNUR.value} (Amended)",
+ "SUPECOMA": f"{GSTR1_Category.SUPECOM.value} (Amended)",
+ "ECOMA": "ECOMA",
+ "ECOMA_REG": "ECOMA_REG",
+ "ECOMA_DE": "ECOMA_DE",
+ "ECOMA_SEZWOP": "ECOMA_SEZWOP",
+ "ECOMA_SEZWP": "ECOMA_SEZWP",
+ "ECOMA_UNREG": "ECOMA_UNREG",
+ "HSN": GSTR1_Category.HSN.value,
+ "NIL": GSTR1_Category.NIL_EXEMPT.value,
+ "DOC_ISSUE": GSTR1_Category.DOC_ISSUE.value,
+ "TTL_LIAB": "Total Liability",
+ }
+
+ SECTIONS_WITH_SUBSECTIONS = {
+ "SUPECOM": {
+ "SUPECOM_14A": GSTR1_SubCategory.SUPECOM_52.value,
+ "SUPECOM_14B": GSTR1_SubCategory.SUPECOM_9_5.value,
+ },
+ "SUPECOMA": {
+ "SUPECOMA_14A": f"{GSTR1_SubCategory.SUPECOM_52.value} (Amended)",
+ "SUPECOMA_14B": f"{GSTR1_SubCategory.SUPECOM_9_5.value} (Amended)",
+ },
+ "EXP": {
+ "EXPWP": GSTR1_SubCategory.EXPWP.value,
+ "EXPWOP": GSTR1_SubCategory.EXPWOP.value,
+ },
+ "EXPA": {
+ "EXPWP": f"{GSTR1_SubCategory.EXPWP.value} (Amended)",
+ "EXPWOP": f"{GSTR1_SubCategory.EXPWOP.value} (Amended)",
+ },
+ }
+
+ def __init__(self):
+ super().__init__()
+
+ self.value_formatters_for_internal = {
+ "sec_nm": self.map_document_types,
+ "typ": self.map_document_types,
+ }
+
+ def convert_to_internal_data_format(self, input_data):
+ output = {}
+
+ for section_data in input_data:
+ section = section_data.get("sec_nm")
+ output[self.SECTION_NAMES.get(section, section)] = self.format_data(
+ section_data
+ )
+
+ if section not in self.SECTIONS_WITH_SUBSECTIONS:
+ continue
+
+ # Unsupported Legacy Summary API. Fallback to self-calculated summary.
+ sub_sections = section_data.get("sub_sections", {})
+ if not sub_sections:
+ return {}
+
+ for subsection_data in sub_sections:
+ formatted_data = self.format_subsection_data(section, subsection_data)
+ output[formatted_data[GSTR1_DataField.DESCRIPTION.value]] = (
+ formatted_data
+ )
+
+ return {"summary": output}
+
+ def format_subsection_data(self, section, subsection_data):
+ subsection = subsection_data.get("typ") or subsection_data.get("sec_nm")
+ formatted_data = self.format_data(subsection_data)
+
+ formatted_data[GSTR1_DataField.DESCRIPTION.value] = (
+ self.SECTIONS_WITH_SUBSECTIONS[section].get(subsection, subsection)
+ )
+ return formatted_data
+
+ def map_document_types(self, doc_type, *args):
+ return self.SECTION_NAMES.get(doc_type, doc_type)
+
+
+CLASS_MAP = {
+ GovJsonKey.B2B.value: B2B,
+ GovJsonKey.B2CL.value: B2CL,
+ GovJsonKey.EXP.value: Exports,
+ GovJsonKey.B2CS.value: B2CS,
+ GovJsonKey.NIL_EXEMPT.value: NilRated,
+ GovJsonKey.CDNR.value: CDNR,
+ GovJsonKey.CDNUR.value: CDNUR,
+ GovJsonKey.HSN.value: HSNSUM,
+ GovJsonKey.DOC_ISSUE.value: DOC_ISSUE,
+ GovJsonKey.AT.value: AT,
+ GovJsonKey.TXP.value: TXPD,
+ GovJsonKey.SUPECOM.value: SUPECOM,
+ GovJsonKey.RET_SUM.value: RETSUM,
+}
+
+
+def convert_to_internal_data_format(gov_data):
+ """
+ Converts Gov data format to internal data format for all categories
+ """
+ output = {}
+
+ for category, mapper_class in CLASS_MAP.items():
+ if not gov_data.get(category):
+ continue
+
+ output.update(
+ mapper_class().convert_to_internal_data_format(gov_data.get(category))
+ )
+
+ return output
+
+
+def get_category_wise_data(
+ subcategory_wise_data: dict,
+ mapping: dict = SUB_CATEGORY_GOV_CATEGORY_MAPPING,
+) -> dict:
+ """
+ returns category wise data from subcategory wise data
+
+ Args:
+ subcategory_wise_data (dict): subcategory wise data
+ mapping (dict): subcategory to category mapping
+ with_subcategory (bool): include subcategory level data
+
+ Returns:
+ dict: category wise data
+
+ Example (with_subcategory=True):
+ {
+ "B2B, SEZ, DE": {
+ "B2B": data,
+ ...
+ }
+ ...
+ }
+
+ Example (with_subcategory=False):
+ {
+ "B2B, SEZ, DE": data,
+ ...
+ }
+ """
+ category_wise_data = {}
+ for subcategory, category in mapping.items():
+ if not subcategory_wise_data.get(subcategory.value):
+ continue
+
+ category_wise_data.setdefault(category.value, []).extend(
+ subcategory_wise_data.get(subcategory.value, [])
+ )
+
+ return category_wise_data
+
+
+def convert_to_gov_data_format(internal_data: dict, company_gstin: str) -> dict:
+ """
+ converts internal data format to Gov data format for all categories
+ """
+
+ category_wise_data = get_category_wise_data(internal_data)
+
+ output = {}
+ for category, mapper_class in CLASS_MAP.items():
+ if not category_wise_data.get(category):
+ continue
+
+ output[category] = mapper_class().convert_to_gov_data_format(
+ category_wise_data.get(category), company_gstin=company_gstin
+ )
+
+ return output
+
+
+def summarize_retsum_data(input_data):
+ if not input_data:
+ return []
+
+ summarized_data = []
+ total_values_keys = [
+ "total_igst_amount",
+ "total_cgst_amount",
+ "total_sgst_amount",
+ "total_cess_amount",
+ "total_taxable_value",
+ ]
+ amended_data = {key: 0 for key in total_values_keys}
+
+ input_data = {row.get("description"): row for row in input_data}
+
+ def _sum(row):
+ return flt(sum([row.get(key, 0) for key in total_values_keys]), 2)
+
+ for category, sub_categories in CATEGORY_SUB_CATEGORY_MAPPING.items():
+ category = category.value
+ if category not in input_data:
+ continue
+
+ # compute total liability and total amended data
+ amended_category_data = input_data.get(f"{category} (Amended)", {})
+ for key in total_values_keys:
+ amended_data[key] += amended_category_data.get(key, 0)
+
+ # add category data
+ if _sum(input_data[category]) == 0:
+ continue
+
+ summarized_data.append({**input_data.get(category), "indent": 0})
+
+ # add subcategory data
+ for sub_category in sub_categories:
+ sub_category = sub_category.value
+ if sub_category not in input_data:
+ continue
+
+ if _sum(input_data[sub_category]) == 0:
+ continue
+
+ summarized_data.append(
+ {
+ **input_data.get(sub_category),
+ "indent": 1,
+ "consider_in_total_taxable_value": (
+ False
+ if sub_category
+ in SUBCATEGORIES_NOT_CONSIDERED_IN_TOTAL_TAXABLE_VALUE
+ else True
+ ),
+ "consider_in_total_tax": (
+ False
+ if sub_category in SUBCATEGORIES_NOT_CONSIDERED_IN_TOTAL_TAX
+ else True
+ ),
+ }
+ )
+
+ # add total amendment liability
+ if _sum(amended_data) != 0:
+ summarized_data.extend(
+ [
+ {
+ "description": "Net Liability from Amendments",
+ **amended_data,
+ "indent": 0,
+ "consider_in_total_taxable_value": True,
+ "consider_in_total_tax": True,
+ "no_of_records": 0,
+ }
+ ]
+ )
+
+ return summarized_data
+
+
+####################################################################################################
+### Map Books Data to Internal Data Structure ######################################################
+####################################################################################################
+
+
+class BooksDataMapper:
+ def get_transaction_type(self, invoice):
+ if invoice.is_debit_note:
+ return "Debit Note"
+ elif invoice.is_return:
+ return "Credit Note"
+ else:
+ return "Invoice"
+
+ def process_data_for_invoice_no_key(self, invoice, prepared_data):
+ invoice_sub_category = invoice.invoice_sub_category
+ invoice_no = invoice.invoice_no
+
+ mapped_dict = prepared_data.setdefault(invoice_sub_category, {}).setdefault(
+ invoice_no,
+ {
+ GSTR1_DataField.TRANSACTION_TYPE.value: self.get_transaction_type(
+ invoice
+ ),
+ GSTR1_DataField.CUST_GSTIN.value: invoice.billing_address_gstin,
+ GSTR1_DataField.CUST_NAME.value: invoice.customer_name,
+ GSTR1_DataField.DOC_DATE.value: invoice.posting_date,
+ GSTR1_DataField.DOC_NUMBER.value: invoice.invoice_no,
+ GSTR1_DataField.DOC_VALUE.value: invoice.invoice_total,
+ GSTR1_DataField.POS.value: invoice.place_of_supply,
+ GSTR1_DataField.REVERSE_CHARGE.value: (
+ "Y" if invoice.is_reverse_charge else "N"
+ ),
+ GSTR1_DataField.DOC_TYPE.value: invoice.invoice_type,
+ GSTR1_DataField.TAXABLE_VALUE.value: 0,
+ GSTR1_DataField.IGST.value: 0,
+ GSTR1_DataField.CGST.value: 0,
+ GSTR1_DataField.SGST.value: 0,
+ GSTR1_DataField.CESS.value: 0,
+ GSTR1_DataField.DIFF_PERCENTAGE.value: 0,
+ "items": [],
+ },
+ )
+
+ items = mapped_dict["items"]
+
+ for item in items:
+ if item[GSTR1_ItemField.TAX_RATE.value] == invoice.gst_rate:
+ item[GSTR1_ItemField.TAXABLE_VALUE.value] += invoice.taxable_value
+ item[GSTR1_ItemField.IGST.value] += invoice.igst_amount
+ item[GSTR1_ItemField.CGST.value] += invoice.cgst_amount
+ item[GSTR1_ItemField.SGST.value] += invoice.sgst_amount
+ item[GSTR1_ItemField.CESS.value] += invoice.total_cess_amount
+ self.update_totals(mapped_dict, invoice)
+ return
+
+ items.append(
+ {
+ GSTR1_ItemField.TAXABLE_VALUE.value: invoice.taxable_value,
+ GSTR1_ItemField.IGST.value: invoice.igst_amount,
+ GSTR1_ItemField.CGST.value: invoice.cgst_amount,
+ GSTR1_ItemField.SGST.value: invoice.sgst_amount,
+ GSTR1_ItemField.CESS.value: invoice.total_cess_amount,
+ GSTR1_ItemField.TAX_RATE.value: invoice.gst_rate,
+ }
+ )
+
+ self.update_totals(mapped_dict, invoice)
+
+ def process_data_for_nil_exempt(self, invoice, prepared_data):
+ key = invoice.invoice_category
+ invoices_by_type = prepared_data.setdefault(key, {}).setdefault(
+ invoice.invoice_type, []
+ )
+
+ for mapped_dict in invoices_by_type:
+ if mapped_dict[GSTR1_DataField.DOC_NUMBER.value] == invoice.invoice_no:
+ break
+
+ else:
+ mapped_dict = {
+ GSTR1_DataField.TRANSACTION_TYPE.value: self.get_transaction_type(
+ invoice
+ ),
+ GSTR1_DataField.CUST_GSTIN.value: invoice.billing_address_gstin,
+ GSTR1_DataField.CUST_NAME.value: invoice.customer_name,
+ GSTR1_DataField.DOC_NUMBER.value: invoice.invoice_no,
+ GSTR1_DataField.DOC_DATE.value: invoice.posting_date,
+ GSTR1_DataField.DOC_VALUE.value: invoice.invoice_total,
+ GSTR1_DataField.POS.value: invoice.place_of_supply,
+ GSTR1_DataField.REVERSE_CHARGE.value: (
+ "Y" if invoice.is_reverse_charge else "N"
+ ),
+ GSTR1_DataField.DOC_TYPE.value: invoice.invoice_type,
+ GSTR1_DataField.TAXABLE_VALUE.value: 0,
+ GSTR1_DataField.NIL_RATED_AMOUNT.value: 0,
+ GSTR1_DataField.EXEMPTED_AMOUNT.value: 0,
+ GSTR1_DataField.NON_GST_AMOUNT.value: 0,
+ }
+ invoices_by_type.append(mapped_dict)
+
+ mapped_dict[GSTR1_DataField.TAXABLE_VALUE.value] += invoice.taxable_value
+
+ if invoice.gst_treatment == "Nil-Rated":
+ mapped_dict[GSTR1_DataField.NIL_RATED_AMOUNT.value] += invoice.taxable_value
+ elif invoice.gst_treatment == "Exempted":
+ mapped_dict[GSTR1_DataField.EXEMPTED_AMOUNT.value] += invoice.taxable_value
+ elif invoice.gst_treatment == "Non-GST":
+ mapped_dict[GSTR1_DataField.NON_GST_AMOUNT.value] += invoice.taxable_value
+
+ def process_data_for_b2cs(self, invoice, prepared_data):
+ key = f"{invoice.place_of_supply} - {flt(invoice.gst_rate)}"
+ mapped_dict = prepared_data.setdefault("B2C (Others)", {}).setdefault(key, [])
+
+ for row in mapped_dict:
+ if row[GSTR1_DataField.DOC_NUMBER.value] == invoice.invoice_no:
+ self.update_totals(row, invoice)
+ return
+
+ mapped_dict.append(
+ {
+ GSTR1_DataField.DOC_DATE.value: invoice.posting_date,
+ GSTR1_DataField.DOC_NUMBER.value: invoice.invoice_no,
+ GSTR1_DataField.DOC_VALUE.value: invoice.invoice_total,
+ GSTR1_DataField.CUST_NAME.value: invoice.customer_name,
+ # currently other value is not supported in GSTR-1
+ GSTR1_DataField.DOC_TYPE.value: "OE",
+ GSTR1_DataField.TRANSACTION_TYPE.value: self.get_transaction_type(
+ invoice
+ ),
+ GSTR1_DataField.POS.value: invoice.place_of_supply,
+ GSTR1_DataField.TAX_RATE.value: invoice.gst_rate,
+ GSTR1_DataField.ECOMMERCE_GSTIN.value: invoice.ecommerce_gstin,
+ **self.get_invoice_values(invoice),
+ }
+ )
+
+ def process_data_for_hsn_summary(self, invoice, prepared_data):
+ key = f"{invoice.gst_hsn_code} - {invoice.stock_uom} - {flt(invoice.gst_rate)}"
+
+ if key not in prepared_data:
+ mapped_dict = prepared_data.setdefault(
+ key,
+ {
+ GSTR1_DataField.HSN_CODE.value: invoice.gst_hsn_code,
+ GSTR1_DataField.DESCRIPTION.value: frappe.db.get_value(
+ "GST HSN Code", invoice.gst_hsn_code, "description"
+ ),
+ GSTR1_DataField.UOM.value: invoice.stock_uom,
+ GSTR1_DataField.QUANTITY.value: 0,
+ GSTR1_DataField.TAX_RATE.value: invoice.gst_rate,
+ GSTR1_DataField.TAXABLE_VALUE.value: 0,
+ GSTR1_DataField.IGST.value: 0,
+ GSTR1_DataField.CGST.value: 0,
+ GSTR1_DataField.SGST.value: 0,
+ GSTR1_DataField.CESS.value: 0,
+ },
+ )
+
+ else:
+ mapped_dict = prepared_data[key]
+
+ self.update_totals(mapped_dict, invoice, for_qty=True)
+
+ mapped_dict[GSTR1_DataField.DOC_VALUE.value] = sum(
+ (
+ mapped_dict.get(GSTR1_DataField.TAXABLE_VALUE.value, 0),
+ mapped_dict.get(GSTR1_DataField.IGST.value, 0),
+ mapped_dict.get(GSTR1_DataField.CGST.value, 0),
+ mapped_dict.get(GSTR1_DataField.SGST.value, 0),
+ mapped_dict.get(GSTR1_DataField.CESS.value, 0),
+ )
+ )
+
+ def process_data_for_document_issued_summary(self, row, prepared_data):
+ key = f"{row['nature_of_document']} - {row['from_serial_no']}"
+ prepared_data.setdefault(
+ key,
+ {
+ GSTR1_DataField.DOC_TYPE.value: row["nature_of_document"],
+ GSTR1_DataField.FROM_SR.value: row["from_serial_no"],
+ GSTR1_DataField.TO_SR.value: row["to_serial_no"],
+ GSTR1_DataField.TOTAL_COUNT.value: row["total_issued"],
+ GSTR1_DataField.DRAFT_COUNT.value: row["total_draft"],
+ GSTR1_DataField.CANCELLED_COUNT.value: row["cancelled"],
+ GSTR1_DataField.NET_ISSUE.value: row["total_submitted"],
+ },
+ )
+
+ def process_data_for_advances_received_or_adjusted(
+ self, row, prepared_data, multiplier=1
+ ):
+ advances = {}
+ tax_rate = round(((row["tax_amount"] / row["taxable_value"]) * 100))
+ key = f"{row['place_of_supply']} - {flt(tax_rate)}"
+
+ mapped_dict = prepared_data.setdefault(key, [])
+
+ advances[GSTR1_DataField.CUST_NAME.value] = row["party"]
+ advances[GSTR1_DataField.DOC_NUMBER.value] = row["name"]
+ advances[GSTR1_DataField.DOC_DATE.value] = row["posting_date"]
+ advances[GSTR1_DataField.POS.value] = row["place_of_supply"]
+ advances[GSTR1_DataField.TAXABLE_VALUE.value] = (
+ row["taxable_value"] * multiplier
+ )
+ advances[GSTR1_DataField.TAX_RATE.value] = tax_rate
+ advances[GSTR1_DataField.CESS.value] = row["cess_amount"] * multiplier
+
+ if row.get("reference_name"):
+ advances["against_voucher"] = row["reference_name"]
+
+ if row["place_of_supply"][0:2] == row["company_gstin"][0:2]:
+ advances[GSTR1_DataField.CGST.value] = row["tax_amount"] / 2 * multiplier
+ advances[GSTR1_DataField.SGST.value] = row["tax_amount"] / 2 * multiplier
+ advances[GSTR1_DataField.IGST.value] = 0
+
+ else:
+ advances[GSTR1_DataField.IGST.value] = row["tax_amount"] * multiplier
+ advances[GSTR1_DataField.CGST.value] = 0
+ advances[GSTR1_DataField.SGST.value] = 0
+
+ mapped_dict.append(advances)
+
+ # utils
+
+ def update_totals(self, mapped_dict, invoice, for_qty=False):
+ data_invoice_amount_map = {
+ GSTR1_DataField.TAXABLE_VALUE.value: GSTR1_ItemField.TAXABLE_VALUE.value,
+ GSTR1_DataField.IGST.value: GSTR1_ItemField.IGST.value,
+ GSTR1_DataField.CGST.value: GSTR1_ItemField.CGST.value,
+ GSTR1_DataField.SGST.value: GSTR1_ItemField.SGST.value,
+ GSTR1_DataField.CESS.value: GSTR1_ItemField.CESS.value,
+ }
+
+ if for_qty:
+ data_invoice_amount_map[GSTR1_DataField.QUANTITY.value] = "qty"
+
+ for key, field in data_invoice_amount_map.items():
+ mapped_dict[key] += invoice.get(field, 0)
+
+ def get_invoice_values(self, invoice):
+ return {
+ GSTR1_DataField.TAXABLE_VALUE.value: invoice.taxable_value,
+ GSTR1_DataField.IGST.value: invoice.igst_amount,
+ GSTR1_DataField.CGST.value: invoice.cgst_amount,
+ GSTR1_DataField.SGST.value: invoice.sgst_amount,
+ GSTR1_DataField.CESS.value: invoice.total_cess_amount,
+ }
+
+
+class GSTR1BooksData(BooksDataMapper):
+ def __init__(self, filters):
+ self.filters = filters
+
+ def prepare_mapped_data(self):
+ prepared_data = {}
+
+ _class = GSTR1Invoices(self.filters)
+ data = _class.get_invoices_for_item_wise_summary()
+ _class.process_invoices(data)
+
+ for invoice in data:
+ if invoice["invoice_category"] in (
+ GSTR1_Category.B2B.value,
+ GSTR1_Category.EXP.value,
+ GSTR1_Category.B2CL.value,
+ GSTR1_Category.CDNR.value,
+ GSTR1_Category.CDNUR.value,
+ ):
+ self.process_data_for_invoice_no_key(invoice, prepared_data)
+ elif invoice["invoice_category"] == GSTR1_Category.NIL_EXEMPT.value:
+ self.process_data_for_nil_exempt(invoice, prepared_data)
+ elif invoice["invoice_category"] == GSTR1_Category.B2CS.value:
+ self.process_data_for_b2cs(invoice, prepared_data)
+
+ other_categories = {
+ GSTR1_Category.AT.value: self.prepare_advances_recevied_data(),
+ GSTR1_Category.TXP.value: self.prepare_advances_adjusted_data(),
+ GSTR1_Category.HSN.value: self.prepare_hsn_data(data),
+ GSTR1_Category.DOC_ISSUE.value: self.prepare_document_issued_data(),
+ }
+
+ for category, data in other_categories.items():
+ if data:
+ prepared_data[category] = data
+
+ return prepared_data
+
+ def prepare_document_issued_data(self):
+ doc_issued_data = {}
+ data = GSTR1DocumentIssuedSummary(self.filters).get_data()
+
+ for row in data:
+ self.process_data_for_document_issued_summary(row, doc_issued_data)
+
+ return doc_issued_data
+
+ def prepare_hsn_data(self, data):
+ hsn_summary_data = {}
+
+ for row in data:
+ self.process_data_for_hsn_summary(row, hsn_summary_data)
+
+ return hsn_summary_data
+
+ def prepare_advances_recevied_data(self):
+ return self.prepare_advances_received_or_adjusted_data("Advances")
+
+ def prepare_advances_adjusted_data(self):
+ return self.prepare_advances_received_or_adjusted_data("Adjustment")
+
+ def prepare_advances_received_or_adjusted_data(self, type_of_business):
+ advances_data = {}
+ self.filters.type_of_business = type_of_business
+ gst_accounts = get_gst_accounts_by_type(self.filters.company, "Output")
+ _class = GSTR11A11BData(self.filters, gst_accounts)
+
+ if type_of_business == "Advances":
+ query = _class.get_11A_query()
+ fields = (
+ _class.pe.name,
+ _class.pe.party,
+ _class.pe.posting_date,
+ _class.pe.company_gstin,
+ )
+ multipler = 1
+
+ elif type_of_business == "Adjustment":
+ query = _class.get_11B_query()
+ fields = (
+ _class.pe.name,
+ _class.pe.party,
+ _class.pe.posting_date,
+ _class.pe.company_gstin,
+ _class.pe_ref.reference_name,
+ )
+ multipler = -1
+
+ query = query.select(*fields)
+ data = query.run(as_dict=True)
+
+ for row in data:
+ self.process_data_for_advances_received_or_adjusted(
+ row, advances_data, multipler
+ )
+
+ return advances_data
diff --git a/india_compliance/gst_india/utils/gstr_1/test_gstr_1_json_map.py b/india_compliance/gst_india/utils/gstr_1/test_gstr_1_json_map.py
new file mode 100644
index 000000000..a602bcbb0
--- /dev/null
+++ b/india_compliance/gst_india/utils/gstr_1/test_gstr_1_json_map.py
@@ -0,0 +1,1366 @@
+import copy
+
+from frappe.tests.utils import FrappeTestCase
+
+from india_compliance.gst_india.doctype.gstr_1_log.gstr_1_log import GenerateGSTR1
+from india_compliance.gst_india.utils import get_party_for_gstin as _get_party_for_gstin
+from india_compliance.gst_india.utils.gstr_1 import (
+ SUB_CATEGORY_GOV_CATEGORY_MAPPING,
+ GovDataField,
+ GSTR1_B2B_InvoiceType,
+ GSTR1_DataField,
+ GSTR1_ItemField,
+ GSTR1_SubCategory,
+)
+from india_compliance.gst_india.utils.gstr_1.gstr_1_json_map import (
+ AT,
+ B2B,
+ B2CL,
+ B2CS,
+ CDNR,
+ CDNUR,
+ DOC_ISSUE,
+ HSNSUM,
+ SUPECOM,
+ TXPD,
+ Exports,
+ NilRated,
+ get_category_wise_data,
+)
+
+
+def get_party_for_gstin(gstin):
+ return _get_party_for_gstin(gstin, "Customer") or "Unknown"
+
+
+def normalize_data(data):
+ return GenerateGSTR1().normalize_data(data)
+
+
+def process_mapped_data(data):
+ return list(
+ get_category_wise_data(
+ normalize_data(copy.deepcopy(data)), SUB_CATEGORY_GOV_CATEGORY_MAPPING
+ ).values()
+ )[0]
+
+
+class TestB2B(FrappeTestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.json_data = [
+ {
+ GovDataField.CUST_GSTIN.value: "24AANFA2641L1ZF",
+ GovDataField.INVOICES.value: [
+ {
+ GovDataField.DOC_NUMBER.value: "S008400",
+ GovDataField.DOC_DATE.value: "24-11-2016",
+ GovDataField.DOC_VALUE.value: 729248.16,
+ GovDataField.POS.value: "06",
+ GovDataField.REVERSE_CHARGE.value: "N",
+ GovDataField.INVOICE_TYPE.value: "R",
+ GovDataField.DIFF_PERCENTAGE.value: 0.65,
+ GovDataField.ITEMS.value: [
+ {
+ GovDataField.INDEX.value: 1,
+ GovDataField.ITEM_DETAILS.value: {
+ GovDataField.TAX_RATE.value: 5,
+ GovDataField.TAXABLE_VALUE.value: 10000,
+ GovDataField.IGST.value: 325,
+ GovDataField.CGST.value: 0,
+ GovDataField.SGST.value: 0,
+ GovDataField.CESS.value: 500,
+ },
+ },
+ {
+ GovDataField.INDEX.value: 2,
+ GovDataField.ITEM_DETAILS.value: {
+ GovDataField.TAX_RATE.value: 5,
+ GovDataField.TAXABLE_VALUE.value: 10000,
+ GovDataField.IGST.value: 325,
+ GovDataField.CGST.value: 0,
+ GovDataField.SGST.value: 0,
+ GovDataField.CESS.value: 500,
+ },
+ },
+ ],
+ },
+ {
+ GovDataField.DOC_NUMBER.value: "S008401",
+ GovDataField.DOC_DATE.value: "24-11-2016",
+ GovDataField.DOC_VALUE.value: 729248.16,
+ GovDataField.POS.value: "06",
+ GovDataField.REVERSE_CHARGE.value: "Y",
+ GovDataField.INVOICE_TYPE.value: "R",
+ GovDataField.DIFF_PERCENTAGE.value: 0.65,
+ GovDataField.ITEMS.value: [
+ {
+ GovDataField.INDEX.value: 1,
+ GovDataField.ITEM_DETAILS.value: {
+ GovDataField.TAX_RATE.value: 5,
+ GovDataField.TAXABLE_VALUE.value: 10000,
+ GovDataField.IGST.value: 325,
+ GovDataField.CGST.value: 0,
+ GovDataField.SGST.value: 0,
+ GovDataField.CESS.value: 500,
+ },
+ }
+ ],
+ },
+ ],
+ },
+ {
+ GovDataField.CUST_GSTIN.value: "29AABCR1718E1ZL",
+ GovDataField.INVOICES.value: [
+ {
+ GovDataField.DOC_NUMBER.value: "S008402",
+ GovDataField.DOC_DATE.value: "24-11-2016",
+ GovDataField.DOC_VALUE.value: 729248.16,
+ GovDataField.POS.value: "06",
+ GovDataField.REVERSE_CHARGE.value: "N",
+ GovDataField.INVOICE_TYPE.value: "SEWP",
+ GovDataField.DIFF_PERCENTAGE.value: 0.65,
+ GovDataField.ITEMS.value: [
+ {
+ GovDataField.INDEX.value: 1,
+ GovDataField.ITEM_DETAILS.value: {
+ GovDataField.TAX_RATE.value: 5,
+ GovDataField.TAXABLE_VALUE.value: 10000,
+ GovDataField.IGST.value: 325,
+ GovDataField.CGST.value: 0,
+ GovDataField.SGST.value: 0,
+ GovDataField.CESS.value: 500,
+ },
+ }
+ ],
+ },
+ {
+ GovDataField.DOC_NUMBER.value: "S008403",
+ GovDataField.DOC_DATE.value: "24-11-2016",
+ GovDataField.DOC_VALUE.value: 729248.16,
+ GovDataField.POS.value: "06",
+ GovDataField.REVERSE_CHARGE.value: "N",
+ GovDataField.INVOICE_TYPE.value: "DE",
+ GovDataField.DIFF_PERCENTAGE.value: 0.65,
+ GovDataField.ITEMS.value: [
+ {
+ GovDataField.INDEX.value: 1,
+ GovDataField.ITEM_DETAILS.value: {
+ GovDataField.TAX_RATE.value: 5,
+ GovDataField.TAXABLE_VALUE.value: 10000,
+ GovDataField.IGST.value: 325,
+ GovDataField.CGST.value: 0,
+ GovDataField.SGST.value: 0,
+ GovDataField.CESS.value: 500,
+ },
+ }
+ ],
+ },
+ ],
+ },
+ ]
+ cls.mapped_data = {
+ GSTR1_SubCategory.B2B_REGULAR.value: {
+ "S008400": {
+ GSTR1_DataField.CUST_GSTIN.value: "24AANFA2641L1ZF",
+ GSTR1_DataField.CUST_NAME.value: get_party_for_gstin(
+ "24AANFA2641L1ZF"
+ ),
+ GSTR1_DataField.DOC_NUMBER.value: "S008400",
+ GSTR1_DataField.DOC_DATE.value: "2016-11-24",
+ GSTR1_DataField.DOC_VALUE.value: 729248.16,
+ GSTR1_DataField.POS.value: "06-Haryana",
+ GSTR1_DataField.REVERSE_CHARGE.value: "N",
+ GSTR1_DataField.DOC_TYPE.value: GSTR1_B2B_InvoiceType.R.value,
+ GSTR1_DataField.DIFF_PERCENTAGE.value: 0.65,
+ GSTR1_DataField.ITEMS.value: [
+ {
+ GSTR1_ItemField.TAXABLE_VALUE.value: 10000,
+ GSTR1_ItemField.IGST.value: 325,
+ GSTR1_ItemField.CGST.value: 0,
+ GSTR1_ItemField.SGST.value: 0,
+ GSTR1_ItemField.CESS.value: 500,
+ GSTR1_DataField.TAX_RATE.value: 5,
+ },
+ {
+ GSTR1_ItemField.TAXABLE_VALUE.value: 10000,
+ GSTR1_ItemField.IGST.value: 325,
+ GSTR1_ItemField.CGST.value: 0,
+ GSTR1_ItemField.SGST.value: 0,
+ GSTR1_ItemField.CESS.value: 500,
+ GSTR1_DataField.TAX_RATE.value: 5,
+ },
+ ],
+ GSTR1_DataField.TAXABLE_VALUE.value: 20000,
+ GSTR1_DataField.IGST.value: 650,
+ GSTR1_DataField.CGST.value: 0,
+ GSTR1_DataField.SGST.value: 0,
+ GSTR1_DataField.CESS.value: 1000,
+ }
+ },
+ GSTR1_SubCategory.B2B_REVERSE_CHARGE.value: {
+ "S008401": {
+ GSTR1_DataField.CUST_GSTIN.value: "24AANFA2641L1ZF",
+ GSTR1_DataField.CUST_NAME.value: get_party_for_gstin(
+ "24AANFA2641L1ZF"
+ ),
+ GSTR1_DataField.DOC_NUMBER.value: "S008401",
+ GSTR1_DataField.DOC_DATE.value: "2016-11-24",
+ GSTR1_DataField.DOC_VALUE.value: 729248.16,
+ GSTR1_DataField.POS.value: "06-Haryana",
+ GSTR1_DataField.REVERSE_CHARGE.value: "Y",
+ GSTR1_DataField.DOC_TYPE.value: GSTR1_B2B_InvoiceType.R.value,
+ GSTR1_DataField.DIFF_PERCENTAGE.value: 0.65,
+ GSTR1_DataField.ITEMS.value: [
+ {
+ GSTR1_ItemField.TAXABLE_VALUE.value: 10000,
+ GSTR1_ItemField.IGST.value: 325,
+ GSTR1_ItemField.CGST.value: 0,
+ GSTR1_ItemField.SGST.value: 0,
+ GSTR1_ItemField.CESS.value: 500,
+ GSTR1_DataField.TAX_RATE.value: 5,
+ }
+ ],
+ GSTR1_DataField.TAXABLE_VALUE.value: 10000,
+ GSTR1_DataField.IGST.value: 325,
+ GSTR1_DataField.CGST.value: 0,
+ GSTR1_DataField.SGST.value: 0,
+ GSTR1_DataField.CESS.value: 500,
+ }
+ },
+ GSTR1_SubCategory.SEZWP.value: {
+ "S008402": {
+ GSTR1_DataField.CUST_GSTIN.value: "29AABCR1718E1ZL",
+ GSTR1_DataField.CUST_NAME.value: get_party_for_gstin(
+ "29AABCR1718E1ZL"
+ ),
+ GSTR1_DataField.DOC_NUMBER.value: "S008402",
+ GSTR1_DataField.DOC_DATE.value: "2016-11-24",
+ GSTR1_DataField.DOC_VALUE.value: 729248.16,
+ GSTR1_DataField.POS.value: "06-Haryana",
+ GSTR1_DataField.REVERSE_CHARGE.value: "N",
+ GSTR1_DataField.DOC_TYPE.value: GSTR1_B2B_InvoiceType.SEWP.value,
+ GSTR1_DataField.DIFF_PERCENTAGE.value: 0.65,
+ GSTR1_DataField.ITEMS.value: [
+ {
+ GSTR1_ItemField.TAXABLE_VALUE.value: 10000,
+ GSTR1_ItemField.IGST.value: 325,
+ GSTR1_ItemField.CGST.value: 0,
+ GSTR1_ItemField.SGST.value: 0,
+ GSTR1_ItemField.CESS.value: 500,
+ GSTR1_DataField.TAX_RATE.value: 5,
+ }
+ ],
+ GSTR1_DataField.TAXABLE_VALUE.value: 10000,
+ GSTR1_DataField.IGST.value: 325,
+ GSTR1_DataField.CGST.value: 0,
+ GSTR1_DataField.SGST.value: 0,
+ GSTR1_DataField.CESS.value: 500,
+ }
+ },
+ GSTR1_SubCategory.DE.value: {
+ "S008403": {
+ GSTR1_DataField.CUST_GSTIN.value: "29AABCR1718E1ZL",
+ GSTR1_DataField.CUST_NAME.value: get_party_for_gstin(
+ "29AABCR1718E1ZL"
+ ),
+ GSTR1_DataField.DOC_NUMBER.value: "S008403",
+ GSTR1_DataField.DOC_DATE.value: "2016-11-24",
+ GSTR1_DataField.DOC_VALUE.value: 729248.16,
+ GSTR1_DataField.POS.value: "06-Haryana",
+ GSTR1_DataField.REVERSE_CHARGE.value: "N",
+ GSTR1_DataField.DOC_TYPE.value: GSTR1_B2B_InvoiceType.DE.value,
+ GSTR1_DataField.DIFF_PERCENTAGE.value: 0.65,
+ GSTR1_DataField.ITEMS.value: [
+ {
+ GSTR1_ItemField.TAXABLE_VALUE.value: 10000,
+ GSTR1_ItemField.IGST.value: 325,
+ GSTR1_ItemField.CGST.value: 0,
+ GSTR1_ItemField.SGST.value: 0,
+ GSTR1_ItemField.CESS.value: 500,
+ GSTR1_DataField.TAX_RATE.value: 5,
+ }
+ ],
+ GSTR1_DataField.TAXABLE_VALUE.value: 10000,
+ GSTR1_DataField.IGST.value: 325,
+ GSTR1_DataField.CGST.value: 0,
+ GSTR1_DataField.SGST.value: 0,
+ GSTR1_DataField.CESS.value: 500,
+ }
+ },
+ }
+
+ def test_convert_to_internal_data_format(self):
+ output = B2B().convert_to_internal_data_format(self.json_data)
+ self.assertDictEqual(self.mapped_data, output)
+
+ def test_convert_to_gov_data_format(self):
+ output = B2B().convert_to_gov_data_format(process_mapped_data(self.mapped_data))
+ self.assertListEqual(self.json_data, output)
+
+
+class TestB2CL(FrappeTestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.json_data = [
+ {
+ GovDataField.POS.value: "05",
+ GovDataField.INVOICES.value: [
+ {
+ GovDataField.DOC_NUMBER.value: "92661",
+ GovDataField.DOC_DATE.value: "10-01-2016",
+ GovDataField.DOC_VALUE.value: 784586.33,
+ GovDataField.DIFF_PERCENTAGE.value: 0.65,
+ GovDataField.ITEMS.value: [
+ {
+ GovDataField.INDEX.value: 1,
+ GovDataField.ITEM_DETAILS.value: {
+ GovDataField.TAX_RATE.value: 5,
+ GovDataField.TAXABLE_VALUE.value: 10000,
+ GovDataField.IGST.value: 325,
+ GovDataField.CESS.value: 500,
+ },
+ },
+ {
+ GovDataField.INDEX.value: 2,
+ GovDataField.ITEM_DETAILS.value: {
+ GovDataField.TAX_RATE.value: 5,
+ GovDataField.TAXABLE_VALUE.value: 10000,
+ GovDataField.IGST.value: 325,
+ GovDataField.CESS.value: 500,
+ },
+ },
+ ],
+ },
+ {
+ GovDataField.DOC_NUMBER.value: "92662",
+ GovDataField.DOC_DATE.value: "10-01-2016",
+ GovDataField.DOC_VALUE.value: 784586.33,
+ GovDataField.DIFF_PERCENTAGE.value: 0.65,
+ GovDataField.ITEMS.value: [
+ {
+ GovDataField.INDEX.value: 1,
+ GovDataField.ITEM_DETAILS.value: {
+ GovDataField.TAX_RATE.value: 5,
+ GovDataField.TAXABLE_VALUE.value: 10000,
+ GovDataField.IGST.value: 325,
+ GovDataField.CESS.value: 500,
+ },
+ }
+ ],
+ },
+ ],
+ },
+ {
+ GovDataField.POS.value: "24",
+ GovDataField.INVOICES.value: [
+ {
+ GovDataField.DOC_NUMBER.value: "92663",
+ GovDataField.DOC_DATE.value: "10-01-2016",
+ GovDataField.DOC_VALUE.value: 784586.33,
+ GovDataField.DIFF_PERCENTAGE.value: 0.65,
+ GovDataField.ITEMS.value: [
+ {
+ GovDataField.INDEX.value: 1,
+ GovDataField.ITEM_DETAILS.value: {
+ GovDataField.TAX_RATE.value: 5,
+ GovDataField.TAXABLE_VALUE.value: 10000,
+ GovDataField.IGST.value: 325,
+ GovDataField.CESS.value: 500,
+ },
+ },
+ {
+ GovDataField.INDEX.value: 2,
+ GovDataField.ITEM_DETAILS.value: {
+ GovDataField.TAX_RATE.value: 5,
+ GovDataField.TAXABLE_VALUE.value: 10000,
+ GovDataField.IGST.value: 325,
+ GovDataField.CESS.value: 500,
+ },
+ },
+ ],
+ },
+ {
+ GovDataField.DOC_NUMBER.value: "92664",
+ GovDataField.DOC_DATE.value: "10-01-2016",
+ GovDataField.DOC_VALUE.value: 784586.33,
+ GovDataField.DIFF_PERCENTAGE.value: 0.65,
+ GovDataField.ITEMS.value: [
+ {
+ GovDataField.INDEX.value: 1,
+ GovDataField.ITEM_DETAILS.value: {
+ GovDataField.TAX_RATE.value: 5,
+ GovDataField.TAXABLE_VALUE.value: 10000,
+ GovDataField.IGST.value: 325,
+ GovDataField.CESS.value: 500,
+ },
+ }
+ ],
+ },
+ ],
+ },
+ ]
+ cls.mapped_data = {
+ GSTR1_SubCategory.B2CL.value: {
+ "92661": {
+ GSTR1_DataField.POS.value: "05-Uttarakhand",
+ GSTR1_DataField.DOC_TYPE.value: "B2C (Large)",
+ GSTR1_DataField.DOC_NUMBER.value: "92661",
+ GSTR1_DataField.DOC_DATE.value: "2016-01-10",
+ GSTR1_DataField.DOC_VALUE.value: 784586.33,
+ GSTR1_DataField.DIFF_PERCENTAGE.value: 0.65,
+ GSTR1_DataField.ITEMS.value: [
+ {
+ GSTR1_ItemField.TAXABLE_VALUE.value: 10000,
+ GSTR1_ItemField.IGST.value: 325,
+ GSTR1_ItemField.CESS.value: 500,
+ GSTR1_DataField.TAX_RATE.value: 5,
+ },
+ {
+ GSTR1_ItemField.TAXABLE_VALUE.value: 10000,
+ GSTR1_ItemField.IGST.value: 325,
+ GSTR1_ItemField.CESS.value: 500,
+ GSTR1_DataField.TAX_RATE.value: 5,
+ },
+ ],
+ GSTR1_DataField.TAXABLE_VALUE.value: 20000,
+ GSTR1_DataField.IGST.value: 650,
+ GSTR1_DataField.CESS.value: 1000,
+ },
+ "92662": {
+ GSTR1_DataField.POS.value: "05-Uttarakhand",
+ GSTR1_DataField.DOC_TYPE.value: "B2C (Large)",
+ GSTR1_DataField.DOC_NUMBER.value: "92662",
+ GSTR1_DataField.DOC_DATE.value: "2016-01-10",
+ GSTR1_DataField.DOC_VALUE.value: 784586.33,
+ GSTR1_DataField.DIFF_PERCENTAGE.value: 0.65,
+ GSTR1_DataField.ITEMS.value: [
+ {
+ GSTR1_ItemField.TAXABLE_VALUE.value: 10000,
+ GSTR1_ItemField.IGST.value: 325,
+ GSTR1_ItemField.CESS.value: 500,
+ GSTR1_DataField.TAX_RATE.value: 5,
+ }
+ ],
+ GSTR1_DataField.TAXABLE_VALUE.value: 10000,
+ GSTR1_DataField.IGST.value: 325,
+ GSTR1_DataField.CESS.value: 500,
+ },
+ "92663": {
+ GSTR1_DataField.POS.value: "24-Gujarat",
+ GSTR1_DataField.DOC_TYPE.value: "B2C (Large)",
+ GSTR1_DataField.DOC_NUMBER.value: "92663",
+ GSTR1_DataField.DOC_DATE.value: "2016-01-10",
+ GSTR1_DataField.DOC_VALUE.value: 784586.33,
+ GSTR1_DataField.DIFF_PERCENTAGE.value: 0.65,
+ GSTR1_DataField.ITEMS.value: [
+ {
+ GSTR1_ItemField.TAXABLE_VALUE.value: 10000,
+ GSTR1_ItemField.IGST.value: 325,
+ GSTR1_ItemField.CESS.value: 500,
+ GSTR1_DataField.TAX_RATE.value: 5,
+ },
+ {
+ GSTR1_ItemField.TAXABLE_VALUE.value: 10000,
+ GSTR1_ItemField.IGST.value: 325,
+ GSTR1_ItemField.CESS.value: 500,
+ GSTR1_DataField.TAX_RATE.value: 5,
+ },
+ ],
+ GSTR1_DataField.TAXABLE_VALUE.value: 20000,
+ GSTR1_DataField.IGST.value: 650,
+ GSTR1_DataField.CESS.value: 1000,
+ },
+ "92664": {
+ GSTR1_DataField.POS.value: "24-Gujarat",
+ GSTR1_DataField.DOC_TYPE.value: "B2C (Large)",
+ GSTR1_DataField.DOC_NUMBER.value: "92664",
+ GSTR1_DataField.DOC_DATE.value: "2016-01-10",
+ GSTR1_DataField.DOC_VALUE.value: 784586.33,
+ GSTR1_DataField.DIFF_PERCENTAGE.value: 0.65,
+ GSTR1_DataField.ITEMS.value: [
+ {
+ GSTR1_ItemField.TAXABLE_VALUE.value: 10000,
+ GSTR1_ItemField.IGST.value: 325,
+ GSTR1_ItemField.CESS.value: 500,
+ GSTR1_DataField.TAX_RATE.value: 5,
+ }
+ ],
+ GSTR1_DataField.TAXABLE_VALUE.value: 10000,
+ GSTR1_DataField.IGST.value: 325,
+ GSTR1_DataField.CESS.value: 500,
+ },
+ }
+ }
+
+ def test_convert_to_internal_data_format(self):
+ output = B2CL().convert_to_internal_data_format(self.json_data)
+ self.assertDictEqual(self.mapped_data, output)
+
+ def test_convert_to_gov_data_format(self):
+ output = B2CL().convert_to_gov_data_format(
+ process_mapped_data(self.mapped_data)
+ )
+ self.assertListEqual(self.json_data, output)
+
+
+class TestExports(FrappeTestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.json_data = [
+ {
+ GovDataField.EXPORT_TYPE.value: "WPAY",
+ GovDataField.INVOICES.value: [
+ {
+ GovDataField.DOC_NUMBER.value: "81542",
+ GovDataField.DOC_DATE.value: "12-02-2016",
+ GovDataField.DOC_VALUE.value: 995048.36,
+ GovDataField.SHIPPING_PORT_CODE.value: "ASB991",
+ GovDataField.SHIPPING_BILL_NUMBER.value: "7896542",
+ GovDataField.SHIPPING_BILL_DATE.value: "04-10-2016",
+ GovDataField.ITEMS.value: [
+ {
+ GovDataField.TAXABLE_VALUE.value: 10000,
+ GovDataField.TAX_RATE.value: 5,
+ GovDataField.IGST.value: 833.33,
+ GovDataField.CESS.value: 100,
+ }
+ ],
+ }
+ ],
+ },
+ {
+ GovDataField.EXPORT_TYPE.value: "WOPAY",
+ GovDataField.INVOICES.value: [
+ {
+ GovDataField.DOC_NUMBER.value: "81543",
+ GovDataField.DOC_DATE.value: "12-02-2016",
+ GovDataField.DOC_VALUE.value: 995048.36,
+ GovDataField.SHIPPING_PORT_CODE.value: "ASB981",
+ GovDataField.SHIPPING_BILL_NUMBER.value: "7896542",
+ GovDataField.SHIPPING_BILL_DATE.value: "04-10-2016",
+ GovDataField.ITEMS.value: [
+ {
+ GovDataField.TAXABLE_VALUE.value: 10000,
+ GovDataField.TAX_RATE.value: 0,
+ GovDataField.IGST.value: 0,
+ GovDataField.CESS.value: 100,
+ }
+ ],
+ }
+ ],
+ },
+ ]
+ cls.mapped_data = {
+ GSTR1_SubCategory.EXPWP.value: {
+ "81542": {
+ GSTR1_DataField.DOC_TYPE.value: "WPAY",
+ GSTR1_DataField.DOC_NUMBER.value: "81542",
+ GSTR1_DataField.DOC_DATE.value: "2016-02-12",
+ GSTR1_DataField.DOC_VALUE.value: 995048.36,
+ GSTR1_DataField.SHIPPING_PORT_CODE.value: "ASB991",
+ GSTR1_DataField.SHIPPING_BILL_NUMBER.value: "7896542",
+ GSTR1_DataField.SHIPPING_BILL_DATE.value: "2016-10-04",
+ GSTR1_DataField.ITEMS.value: [
+ {
+ GSTR1_ItemField.TAXABLE_VALUE.value: 10000,
+ GSTR1_ItemField.IGST.value: 833.33,
+ GSTR1_ItemField.CESS.value: 100,
+ GSTR1_DataField.TAX_RATE.value: 5,
+ }
+ ],
+ GSTR1_DataField.TAXABLE_VALUE.value: 10000,
+ GSTR1_DataField.IGST.value: 833.33,
+ GSTR1_DataField.CESS.value: 100,
+ }
+ },
+ GSTR1_SubCategory.EXPWOP.value: {
+ "81543": {
+ GSTR1_DataField.DOC_TYPE.value: "WOPAY",
+ GSTR1_DataField.DOC_NUMBER.value: "81543",
+ GSTR1_DataField.DOC_DATE.value: "2016-02-12",
+ GSTR1_DataField.DOC_VALUE.value: 995048.36,
+ GSTR1_DataField.SHIPPING_PORT_CODE.value: "ASB981",
+ GSTR1_DataField.SHIPPING_BILL_NUMBER.value: "7896542",
+ GSTR1_DataField.SHIPPING_BILL_DATE.value: "2016-10-04",
+ GSTR1_DataField.ITEMS.value: [
+ {
+ GSTR1_ItemField.TAXABLE_VALUE.value: 10000,
+ GSTR1_ItemField.IGST.value: 0,
+ GSTR1_ItemField.CESS.value: 100,
+ GSTR1_DataField.TAX_RATE.value: 0,
+ }
+ ],
+ GSTR1_DataField.TAXABLE_VALUE.value: 10000,
+ GSTR1_DataField.IGST.value: 0,
+ GSTR1_DataField.CESS.value: 100,
+ }
+ },
+ }
+
+ def test_convert_to_internal_data_format(self):
+ output = Exports().convert_to_internal_data_format(self.json_data)
+ self.assertDictEqual(self.mapped_data, output)
+
+ def test_convert_to_gov_data_format(self):
+ output = Exports().convert_to_gov_data_format(
+ process_mapped_data(self.mapped_data)
+ )
+ self.assertListEqual(self.json_data, output)
+
+
+class TestB2CS(FrappeTestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.json_data = [
+ {
+ GovDataField.SUPPLY_TYPE.value: "INTER",
+ GovDataField.DIFF_PERCENTAGE.value: 0.65,
+ GovDataField.TAX_RATE.value: 5,
+ GovDataField.TYPE.value: "OE",
+ GovDataField.POS.value: "05",
+ GovDataField.TAXABLE_VALUE.value: 110,
+ GovDataField.IGST.value: 10,
+ GovDataField.CGST.value: 0,
+ GovDataField.SGST.value: 0,
+ GovDataField.CESS.value: 10,
+ },
+ {
+ GovDataField.SUPPLY_TYPE.value: "INTER",
+ GovDataField.DIFF_PERCENTAGE.value: 0.65,
+ GovDataField.TAX_RATE.value: 5,
+ GovDataField.TYPE.value: "OE",
+ GovDataField.TAXABLE_VALUE.value: 100,
+ GovDataField.IGST.value: 10,
+ GovDataField.CGST.value: 0,
+ GovDataField.SGST.value: 0,
+ GovDataField.CESS.value: 10,
+ GovDataField.POS.value: "06",
+ },
+ ]
+ cls.mapped_data = {
+ GSTR1_SubCategory.B2CS.value: {
+ "05-Uttarakhand - 5.0": [
+ {
+ GSTR1_DataField.TAXABLE_VALUE.value: 110,
+ GSTR1_DataField.DOC_TYPE.value: "OE",
+ GSTR1_DataField.DIFF_PERCENTAGE.value: 0.65,
+ GSTR1_DataField.POS.value: "05-Uttarakhand",
+ GSTR1_DataField.TAX_RATE.value: 5,
+ GSTR1_DataField.IGST.value: 10,
+ GSTR1_DataField.CESS.value: 10,
+ GSTR1_DataField.CGST.value: 0,
+ GSTR1_DataField.SGST.value: 0,
+ },
+ ],
+ "06-Haryana - 5.0": [
+ {
+ GSTR1_DataField.TAXABLE_VALUE.value: 100,
+ GSTR1_DataField.DOC_TYPE.value: "OE",
+ GSTR1_DataField.DIFF_PERCENTAGE.value: 0.65,
+ GSTR1_DataField.POS.value: "06-Haryana",
+ GSTR1_DataField.TAX_RATE.value: 5,
+ GSTR1_DataField.IGST.value: 10,
+ GSTR1_DataField.CESS.value: 10,
+ GSTR1_DataField.CGST.value: 0,
+ GSTR1_DataField.SGST.value: 0,
+ }
+ ],
+ }
+ }
+
+ def test_convert_to_internal_data_format(self):
+ output = B2CS().convert_to_internal_data_format(self.json_data)
+ self.assertDictEqual(self.mapped_data, output)
+
+ def test_convert_to_gov_data_format(self):
+ output = B2CS().convert_to_gov_data_format(
+ process_mapped_data(self.mapped_data)
+ )
+ self.assertListEqual(self.json_data, output)
+
+
+class TestNilRated(FrappeTestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.json_data = {
+ GovDataField.INVOICES.value: [
+ {
+ GovDataField.SUPPLY_TYPE.value: "INTRB2B",
+ GovDataField.EXEMPTED_AMOUNT.value: 123.45,
+ GovDataField.NIL_RATED_AMOUNT.value: 1470.85,
+ GovDataField.NON_GST_AMOUNT.value: 1258.5,
+ },
+ {
+ GovDataField.SUPPLY_TYPE.value: "INTRB2C",
+ GovDataField.EXEMPTED_AMOUNT.value: 123.45,
+ GovDataField.NIL_RATED_AMOUNT.value: 1470.85,
+ GovDataField.NON_GST_AMOUNT.value: 1258.5,
+ },
+ ]
+ }
+
+ cls.mapped_data = {
+ GSTR1_SubCategory.NIL_EXEMPT.value: {
+ "Inter-State supplies to registered persons": [
+ {
+ GSTR1_DataField.DOC_TYPE.value: "Inter-State supplies to registered persons",
+ GSTR1_DataField.EXEMPTED_AMOUNT.value: 123.45,
+ GSTR1_DataField.NIL_RATED_AMOUNT.value: 1470.85,
+ GSTR1_DataField.NON_GST_AMOUNT.value: 1258.5,
+ GSTR1_DataField.TAXABLE_VALUE.value: 2852.8,
+ }
+ ],
+ "Inter-State supplies to unregistered persons": [
+ {
+ GSTR1_DataField.DOC_TYPE.value: "Inter-State supplies to unregistered persons",
+ GSTR1_DataField.EXEMPTED_AMOUNT.value: 123.45,
+ GSTR1_DataField.NIL_RATED_AMOUNT.value: 1470.85,
+ GSTR1_DataField.NON_GST_AMOUNT.value: 1258.5,
+ GSTR1_DataField.TAXABLE_VALUE.value: 2852.8,
+ }
+ ],
+ }
+ }
+
+ def test_convert_to_internal_data_format(self):
+ output = NilRated().convert_to_internal_data_format(self.json_data)
+ self.assertDictEqual(self.mapped_data, output)
+
+ def test_convert_to_gov_data_format(self):
+ output = NilRated().convert_to_gov_data_format(
+ process_mapped_data(self.mapped_data)
+ )
+ self.assertDictEqual(self.json_data, output)
+
+
+class TestCDNR(FrappeTestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.json_data = [
+ {
+ GovDataField.CUST_GSTIN.value: "24AANFA2641L1ZF",
+ GovDataField.NOTE_DETAILS.value: [
+ {
+ GovDataField.NOTE_TYPE.value: "C",
+ GovDataField.NOTE_NUMBER.value: "533515",
+ GovDataField.NOTE_DATE.value: "23-09-2016",
+ GovDataField.POS.value: "03",
+ GovDataField.REVERSE_CHARGE.value: "Y",
+ GovDataField.INVOICE_TYPE.value: "DE",
+ GovDataField.DOC_VALUE.value: 123123,
+ GovDataField.DIFF_PERCENTAGE.value: 0.65,
+ GovDataField.ITEMS.value: [
+ {
+ GovDataField.INDEX.value: 1,
+ GovDataField.ITEM_DETAILS.value: {
+ GovDataField.TAX_RATE.value: 10,
+ GovDataField.TAXABLE_VALUE.value: 5225.28,
+ GovDataField.SGST.value: 0,
+ GovDataField.CGST.value: 0,
+ GovDataField.IGST.value: 339.64,
+ GovDataField.CESS.value: 789.52,
+ },
+ },
+ {
+ GovDataField.INDEX.value: 2,
+ GovDataField.ITEM_DETAILS.value: {
+ GovDataField.TAX_RATE.value: 10,
+ GovDataField.TAXABLE_VALUE.value: 5225.28,
+ GovDataField.SGST.value: 0,
+ GovDataField.CGST.value: 0,
+ GovDataField.IGST.value: 339.64,
+ GovDataField.CESS.value: 789.52,
+ },
+ },
+ ],
+ },
+ ],
+ }
+ ]
+ cls.mapped_data = {
+ GSTR1_SubCategory.CDNR.value: {
+ "533515": {
+ GSTR1_DataField.CUST_GSTIN.value: "24AANFA2641L1ZF",
+ GSTR1_DataField.CUST_NAME.value: get_party_for_gstin(
+ "24AANFA2641L1ZF"
+ ),
+ GSTR1_DataField.TRANSACTION_TYPE.value: "Credit Note",
+ GSTR1_DataField.DOC_NUMBER.value: "533515",
+ GSTR1_DataField.DOC_DATE.value: "2016-09-23",
+ GSTR1_DataField.POS.value: "03-Punjab",
+ GSTR1_DataField.REVERSE_CHARGE.value: "Y",
+ GSTR1_DataField.DOC_TYPE.value: "Deemed Exports",
+ GSTR1_DataField.DOC_VALUE.value: -123123,
+ GSTR1_DataField.DIFF_PERCENTAGE.value: 0.65,
+ GSTR1_DataField.ITEMS.value: [
+ {
+ GSTR1_ItemField.TAXABLE_VALUE.value: -5225.28,
+ GSTR1_ItemField.IGST.value: -339.64,
+ GSTR1_ItemField.CGST.value: 0,
+ GSTR1_ItemField.SGST.value: 0,
+ GSTR1_ItemField.CESS.value: -789.52,
+ GSTR1_DataField.TAX_RATE.value: 10,
+ },
+ {
+ GSTR1_ItemField.TAXABLE_VALUE.value: -5225.28,
+ GSTR1_ItemField.IGST.value: -339.64,
+ GSTR1_ItemField.CGST.value: 0,
+ GSTR1_ItemField.SGST.value: 0,
+ GSTR1_ItemField.CESS.value: -789.52,
+ GSTR1_DataField.TAX_RATE.value: 10,
+ },
+ ],
+ GSTR1_DataField.TAXABLE_VALUE.value: -10450.56,
+ GSTR1_DataField.IGST.value: -679.28,
+ GSTR1_DataField.CGST.value: 0,
+ GSTR1_DataField.SGST.value: 0,
+ GSTR1_DataField.CESS.value: -1579.04,
+ }
+ }
+ }
+
+ def test_convert_to_internal_data_format(self):
+ output = CDNR().convert_to_internal_data_format(copy.deepcopy(self.json_data))
+ self.assertDictEqual(self.mapped_data, output)
+
+ def test_convert_to_gov_data_format(self):
+ output = CDNR().convert_to_gov_data_format(
+ process_mapped_data(copy.deepcopy(self.mapped_data))
+ )
+ self.assertListEqual(self.json_data, output)
+
+
+class TestCDNUR(FrappeTestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ cls.json_data = [
+ {
+ GovDataField.TYPE.value: "B2CL",
+ GovDataField.NOTE_TYPE.value: "C",
+ GovDataField.NOTE_NUMBER.value: "533515",
+ GovDataField.NOTE_DATE.value: "23-09-2016",
+ GovDataField.POS.value: "03",
+ GovDataField.DOC_VALUE.value: 64646,
+ GovDataField.DIFF_PERCENTAGE.value: 0.65,
+ GovDataField.ITEMS.value: [
+ {
+ GovDataField.INDEX.value: 1,
+ GovDataField.ITEM_DETAILS.value: {
+ GovDataField.TAX_RATE.value: 10,
+ GovDataField.TAXABLE_VALUE.value: 5225.28,
+ GovDataField.IGST.value: 339.64,
+ GovDataField.CESS.value: 789.52,
+ },
+ }
+ ],
+ }
+ ]
+
+ cls.mapped_data = {
+ GSTR1_SubCategory.CDNUR.value: {
+ "533515": {
+ GSTR1_DataField.TRANSACTION_TYPE.value: "Credit Note",
+ GSTR1_DataField.DOC_TYPE.value: "B2CL",
+ GSTR1_DataField.DOC_NUMBER.value: "533515",
+ GSTR1_DataField.DOC_DATE.value: "2016-09-23",
+ GSTR1_DataField.DOC_VALUE.value: -64646,
+ GSTR1_DataField.POS.value: "03-Punjab",
+ GSTR1_DataField.DIFF_PERCENTAGE.value: 0.65,
+ GSTR1_DataField.ITEMS.value: [
+ {
+ GSTR1_ItemField.TAXABLE_VALUE.value: -5225.28,
+ GSTR1_ItemField.IGST.value: -339.64,
+ GSTR1_ItemField.CESS.value: -789.52,
+ GSTR1_DataField.TAX_RATE.value: 10,
+ }
+ ],
+ GSTR1_DataField.TAXABLE_VALUE.value: -5225.28,
+ GSTR1_DataField.IGST.value: -339.64,
+ GSTR1_DataField.CESS.value: -789.52,
+ }
+ }
+ }
+
+ def test_convert_to_internal_data_format(self):
+ output = CDNUR().convert_to_internal_data_format(copy.deepcopy(self.json_data))
+ self.assertDictEqual(self.mapped_data, output)
+
+ def test_convert_to_gov_data_format(self):
+ output = CDNUR().convert_to_gov_data_format(
+ process_mapped_data(copy.deepcopy(self.mapped_data))
+ )
+ self.assertListEqual(self.json_data, output)
+
+
+class TestHSNSUM(FrappeTestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ cls.json_data = {
+ GovDataField.HSN_DATA.value: [
+ {
+ GovDataField.INDEX.value: 1,
+ GovDataField.HSN_CODE.value: "1010",
+ GovDataField.DESCRIPTION.value: "Goods Description",
+ GovDataField.UOM.value: "KGS",
+ GovDataField.QUANTITY.value: 2.05,
+ GovDataField.TAXABLE_VALUE.value: 10.23,
+ GovDataField.IGST.value: 14.52,
+ GovDataField.CESS.value: 500,
+ GovDataField.TAX_RATE.value: 0.1,
+ },
+ {
+ GovDataField.INDEX.value: 2,
+ GovDataField.HSN_CODE.value: "1011",
+ GovDataField.DESCRIPTION.value: "Goods Description",
+ GovDataField.UOM.value: "NOS",
+ GovDataField.QUANTITY.value: 2.05,
+ GovDataField.TAXABLE_VALUE.value: 10.23,
+ GovDataField.IGST.value: 14.52,
+ GovDataField.CESS.value: 500,
+ GovDataField.TAX_RATE.value: 5.0,
+ },
+ ]
+ }
+
+ cls.mapped_data = {
+ GSTR1_SubCategory.HSN.value: {
+ "1010 - KGS-KILOGRAMS - 0.1": {
+ GSTR1_DataField.HSN_CODE.value: "1010",
+ GSTR1_DataField.DESCRIPTION.value: "Goods Description",
+ GSTR1_DataField.UOM.value: "KGS-KILOGRAMS",
+ GSTR1_DataField.QUANTITY.value: 2.05,
+ GSTR1_DataField.TAXABLE_VALUE.value: 10.23,
+ GSTR1_DataField.IGST.value: 14.52,
+ GSTR1_DataField.CESS.value: 500,
+ GSTR1_DataField.TAX_RATE.value: 0.1,
+ GSTR1_DataField.DOC_VALUE.value: 524.75,
+ },
+ "1011 - NOS-NUMBERS - 5.0": {
+ GSTR1_DataField.HSN_CODE.value: "1011",
+ GSTR1_DataField.DESCRIPTION.value: "Goods Description",
+ GSTR1_DataField.UOM.value: "NOS-NUMBERS",
+ GSTR1_DataField.QUANTITY.value: 2.05,
+ GSTR1_DataField.TAXABLE_VALUE.value: 10.23,
+ GSTR1_DataField.IGST.value: 14.52,
+ GSTR1_DataField.CESS.value: 500,
+ GSTR1_DataField.TAX_RATE.value: 5,
+ GSTR1_DataField.DOC_VALUE.value: 524.75,
+ },
+ }
+ }
+
+ def test_convert_to_internal_data_format(self):
+ output = HSNSUM().convert_to_internal_data_format(self.json_data)
+ self.assertDictEqual(self.mapped_data, output)
+
+ def test_convert_to_gov_data_format(self):
+ output = HSNSUM().convert_to_gov_data_format(
+ process_mapped_data(self.mapped_data)
+ )
+ self.assertDictEqual(self.json_data, output)
+
+
+class TestAT(FrappeTestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ cls.json_data = [
+ {
+ GovDataField.POS.value: "05",
+ GovDataField.SUPPLY_TYPE.value: "INTER",
+ GovDataField.DIFF_PERCENTAGE.value: 0.65,
+ GovDataField.ITEMS.value: [
+ {
+ GovDataField.TAX_RATE.value: 5,
+ GovDataField.ADVANCE_AMOUNT.value: 100,
+ GovDataField.IGST.value: 9400,
+ GovDataField.CGST.value: 0,
+ GovDataField.SGST.value: 0,
+ GovDataField.CESS.value: 500,
+ },
+ {
+ GovDataField.TAX_RATE.value: 6,
+ GovDataField.ADVANCE_AMOUNT.value: 100,
+ GovDataField.IGST.value: 9400,
+ GovDataField.CGST.value: 0,
+ GovDataField.SGST.value: 0,
+ GovDataField.CESS.value: 500,
+ },
+ ],
+ },
+ {
+ GovDataField.POS.value: "24",
+ GovDataField.SUPPLY_TYPE.value: "INTER",
+ GovDataField.DIFF_PERCENTAGE.value: 0.65,
+ GovDataField.ITEMS.value: [
+ {
+ GovDataField.TAX_RATE.value: 5,
+ GovDataField.ADVANCE_AMOUNT.value: 100,
+ GovDataField.IGST.value: 9400,
+ GovDataField.CGST.value: 0,
+ GovDataField.SGST.value: 0,
+ GovDataField.CESS.value: 500,
+ },
+ {
+ GovDataField.TAX_RATE.value: 6,
+ GovDataField.ADVANCE_AMOUNT.value: 100,
+ GovDataField.IGST.value: 9400,
+ GovDataField.CGST.value: 0,
+ GovDataField.SGST.value: 0,
+ GovDataField.CESS.value: 500,
+ },
+ ],
+ },
+ ]
+
+ cls.mapped_data = {
+ GSTR1_SubCategory.AT.value: {
+ "05-Uttarakhand - 5.0": [
+ {
+ GSTR1_DataField.POS.value: "05-Uttarakhand",
+ GSTR1_DataField.DIFF_PERCENTAGE.value: 0.65,
+ GSTR1_DataField.IGST.value: 9400,
+ GSTR1_DataField.CESS.value: 500,
+ GSTR1_DataField.CGST.value: 0,
+ GSTR1_DataField.SGST.value: 0,
+ GSTR1_DataField.TAXABLE_VALUE.value: 100,
+ GSTR1_DataField.TAX_RATE.value: 5,
+ },
+ ],
+ "05-Uttarakhand - 6.0": [
+ {
+ GSTR1_DataField.POS.value: "05-Uttarakhand",
+ GSTR1_DataField.DIFF_PERCENTAGE.value: 0.65,
+ GSTR1_DataField.IGST.value: 9400,
+ GSTR1_DataField.CESS.value: 500,
+ GSTR1_DataField.CGST.value: 0,
+ GSTR1_DataField.SGST.value: 0,
+ GSTR1_DataField.TAXABLE_VALUE.value: 100,
+ GSTR1_DataField.TAX_RATE.value: 6,
+ }
+ ],
+ "24-Gujarat - 5.0": [
+ {
+ GSTR1_DataField.POS.value: "24-Gujarat",
+ GSTR1_DataField.DIFF_PERCENTAGE.value: 0.65,
+ GSTR1_DataField.IGST.value: 9400,
+ GSTR1_DataField.CESS.value: 500,
+ GSTR1_DataField.CGST.value: 0,
+ GSTR1_DataField.SGST.value: 0,
+ GSTR1_DataField.TAXABLE_VALUE.value: 100,
+ GSTR1_DataField.TAX_RATE.value: 5,
+ }
+ ],
+ "24-Gujarat - 6.0": [
+ {
+ GSTR1_DataField.POS.value: "24-Gujarat",
+ GSTR1_DataField.DIFF_PERCENTAGE.value: 0.65,
+ GSTR1_DataField.IGST.value: 9400,
+ GSTR1_DataField.CESS.value: 500,
+ GSTR1_DataField.CGST.value: 0,
+ GSTR1_DataField.SGST.value: 0,
+ GSTR1_DataField.TAXABLE_VALUE.value: 100,
+ GSTR1_DataField.TAX_RATE.value: 6,
+ }
+ ],
+ }
+ }
+
+ def test_convert_to_internal_data_format(self):
+ output = AT().convert_to_internal_data_format(self.json_data)
+ self.assertDictEqual(self.mapped_data, output)
+
+ def test_convert_to_gov_data_format(self):
+ output = AT().convert_to_gov_data_format(process_mapped_data(self.mapped_data))
+ self.assertListEqual(self.json_data, output)
+
+
+class TestTXPD(FrappeTestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ cls.json_data = [
+ {
+ GovDataField.POS.value: "05",
+ GovDataField.SUPPLY_TYPE.value: "INTER",
+ GovDataField.DIFF_PERCENTAGE.value: 0.65,
+ GovDataField.ITEMS.value: [
+ {
+ GovDataField.TAX_RATE.value: 5,
+ GovDataField.ADVANCE_AMOUNT.value: 100,
+ GovDataField.IGST.value: 9400,
+ GovDataField.CGST.value: 0,
+ GovDataField.SGST.value: 0,
+ GovDataField.CESS.value: 500,
+ },
+ {
+ GovDataField.TAX_RATE.value: 6,
+ GovDataField.ADVANCE_AMOUNT.value: 100,
+ GovDataField.IGST.value: 9400,
+ GovDataField.CGST.value: 0,
+ GovDataField.SGST.value: 0,
+ GovDataField.CESS.value: 500,
+ },
+ ],
+ },
+ {
+ GovDataField.POS.value: "24",
+ GovDataField.SUPPLY_TYPE.value: "INTER",
+ GovDataField.DIFF_PERCENTAGE.value: 0.65,
+ GovDataField.ITEMS.value: [
+ {
+ GovDataField.TAX_RATE.value: 5,
+ GovDataField.ADVANCE_AMOUNT.value: 100,
+ GovDataField.IGST.value: 9400,
+ GovDataField.CGST.value: 0,
+ GovDataField.SGST.value: 0,
+ GovDataField.CESS.value: 500,
+ },
+ {
+ GovDataField.TAX_RATE.value: 6,
+ GovDataField.ADVANCE_AMOUNT.value: 100,
+ GovDataField.IGST.value: 9400,
+ GovDataField.CGST.value: 0,
+ GovDataField.SGST.value: 0,
+ GovDataField.CESS.value: 500,
+ },
+ ],
+ },
+ ]
+
+ cls.mapped_data = {
+ GSTR1_SubCategory.TXP.value: {
+ "05-Uttarakhand - 5.0": [
+ {
+ GSTR1_DataField.POS.value: "05-Uttarakhand",
+ GSTR1_DataField.DIFF_PERCENTAGE.value: 0.65,
+ GSTR1_DataField.IGST.value: -9400,
+ GSTR1_DataField.CESS.value: -500,
+ GSTR1_DataField.CGST.value: 0,
+ GSTR1_DataField.SGST.value: 0,
+ GSTR1_DataField.TAXABLE_VALUE.value: -100,
+ GSTR1_DataField.TAX_RATE.value: 5,
+ },
+ ],
+ "05-Uttarakhand - 6.0": [
+ {
+ GSTR1_DataField.POS.value: "05-Uttarakhand",
+ GSTR1_DataField.DIFF_PERCENTAGE.value: 0.65,
+ GSTR1_DataField.IGST.value: -9400,
+ GSTR1_DataField.CESS.value: -500,
+ GSTR1_DataField.CGST.value: 0,
+ GSTR1_DataField.SGST.value: 0,
+ GSTR1_DataField.TAXABLE_VALUE.value: -100,
+ GSTR1_DataField.TAX_RATE.value: 6,
+ }
+ ],
+ "24-Gujarat - 5.0": [
+ {
+ GSTR1_DataField.POS.value: "24-Gujarat",
+ GSTR1_DataField.DIFF_PERCENTAGE.value: 0.65,
+ GSTR1_DataField.IGST.value: -9400,
+ GSTR1_DataField.CESS.value: -500,
+ GSTR1_DataField.CGST.value: 0,
+ GSTR1_DataField.SGST.value: 0,
+ GSTR1_DataField.TAXABLE_VALUE.value: -100,
+ GSTR1_DataField.TAX_RATE.value: 5,
+ }
+ ],
+ "24-Gujarat - 6.0": [
+ {
+ GSTR1_DataField.POS.value: "24-Gujarat",
+ GSTR1_DataField.DIFF_PERCENTAGE.value: 0.65,
+ GSTR1_DataField.IGST.value: -9400,
+ GSTR1_DataField.CESS.value: -500,
+ GSTR1_DataField.CGST.value: 0,
+ GSTR1_DataField.SGST.value: 0,
+ GSTR1_DataField.TAXABLE_VALUE.value: -100,
+ GSTR1_DataField.TAX_RATE.value: 6,
+ }
+ ],
+ }
+ }
+
+ def test_convert_to_internal_data_format(self):
+ output = TXPD().convert_to_internal_data_format(self.json_data)
+ self.assertDictEqual(self.mapped_data, output)
+
+ def test_convert_to_gov_data_format(self):
+ output = TXPD().convert_to_gov_data_format(
+ process_mapped_data(self.mapped_data)
+ )
+ self.assertListEqual(self.json_data, output)
+
+
+class TestDOC_ISSUE(FrappeTestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.json_data = {
+ GovDataField.DOC_ISSUE_DETAILS.value: [
+ {
+ GovDataField.DOC_ISSUE_NUMBER.value: 1,
+ GovDataField.DOC_ISSUE_LIST.value: [
+ {
+ GovDataField.INDEX.value: 1,
+ GovDataField.FROM_SR.value: "1",
+ GovDataField.TO_SR.value: "10",
+ GovDataField.TOTAL_COUNT.value: 10,
+ GovDataField.CANCELLED_COUNT.value: 0,
+ GovDataField.NET_ISSUE.value: 10,
+ },
+ {
+ GovDataField.INDEX.value: 2,
+ GovDataField.FROM_SR.value: "11",
+ GovDataField.TO_SR.value: "20",
+ GovDataField.TOTAL_COUNT.value: 10,
+ GovDataField.CANCELLED_COUNT.value: 0,
+ GovDataField.NET_ISSUE.value: 10,
+ },
+ ],
+ },
+ {
+ GovDataField.DOC_ISSUE_NUMBER.value: 2,
+ GovDataField.DOC_ISSUE_LIST.value: [
+ {
+ GovDataField.INDEX.value: 1,
+ GovDataField.FROM_SR.value: "1",
+ GovDataField.TO_SR.value: "10",
+ GovDataField.TOTAL_COUNT.value: 10,
+ GovDataField.CANCELLED_COUNT.value: 0,
+ GovDataField.NET_ISSUE.value: 10,
+ },
+ {
+ GovDataField.INDEX.value: 2,
+ GovDataField.FROM_SR.value: "11",
+ GovDataField.TO_SR.value: "20",
+ GovDataField.TOTAL_COUNT.value: 10,
+ GovDataField.CANCELLED_COUNT.value: 0,
+ GovDataField.NET_ISSUE.value: 10,
+ },
+ ],
+ },
+ ]
+ }
+ cls.mapped_data = {
+ GSTR1_SubCategory.DOC_ISSUE.value: {
+ "Invoices for outward supply - 1": {
+ GSTR1_DataField.DOC_TYPE.value: "Invoices for outward supply",
+ GSTR1_DataField.FROM_SR.value: "1",
+ GSTR1_DataField.TO_SR.value: "10",
+ GSTR1_DataField.TOTAL_COUNT.value: 10,
+ GSTR1_DataField.CANCELLED_COUNT.value: 0,
+ "net_issue": 10,
+ },
+ "Invoices for outward supply - 11": {
+ GSTR1_DataField.DOC_TYPE.value: "Invoices for outward supply",
+ GSTR1_DataField.FROM_SR.value: "11",
+ GSTR1_DataField.TO_SR.value: "20",
+ GSTR1_DataField.TOTAL_COUNT.value: 10,
+ GSTR1_DataField.CANCELLED_COUNT.value: 0,
+ "net_issue": 10,
+ },
+ "Invoices for inward supply from unregistered person - 1": {
+ GSTR1_DataField.DOC_TYPE.value: "Invoices for inward supply from unregistered person",
+ GSTR1_DataField.FROM_SR.value: "1",
+ GSTR1_DataField.TO_SR.value: "10",
+ GSTR1_DataField.TOTAL_COUNT.value: 10,
+ GSTR1_DataField.CANCELLED_COUNT.value: 0,
+ "net_issue": 10,
+ },
+ "Invoices for inward supply from unregistered person - 11": {
+ GSTR1_DataField.DOC_TYPE.value: "Invoices for inward supply from unregistered person",
+ GSTR1_DataField.FROM_SR.value: "11",
+ GSTR1_DataField.TO_SR.value: "20",
+ GSTR1_DataField.TOTAL_COUNT.value: 10,
+ GSTR1_DataField.CANCELLED_COUNT.value: 0,
+ "net_issue": 10,
+ },
+ }
+ }
+
+ def test_convert_to_internal_data_format(self):
+ output = DOC_ISSUE().convert_to_internal_data_format(self.json_data)
+ self.assertDictEqual(self.mapped_data, output)
+
+ def test_convert_to_gov_data_format(self):
+ output = DOC_ISSUE().convert_to_gov_data_format(
+ process_mapped_data(self.mapped_data)
+ )
+ self.assertDictEqual(self.json_data, output)
+
+
+class TestSUPECOM(FrappeTestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.json_data = {
+ GovDataField.SUPECOM_52.value: [
+ {
+ GovDataField.ECOMMERCE_GSTIN.value: "20ALYPD6528PQC5",
+ GovDataField.NET_TAXABLE_VALUE.value: 10000,
+ "igst": 1000,
+ "cgst": 0,
+ "sgst": 0,
+ "cess": 0,
+ }
+ ],
+ GovDataField.SUPECOM_9_5.value: [
+ {
+ GovDataField.ECOMMERCE_GSTIN.value: "20ALYPD6528PQC5",
+ GovDataField.NET_TAXABLE_VALUE.value: 10000,
+ "igst": 1000,
+ "cgst": 0,
+ "sgst": 0,
+ "cess": 0,
+ }
+ ],
+ }
+
+ cls.mapped_data = {
+ GSTR1_SubCategory.SUPECOM_52.value: {
+ "20ALYPD6528PQC5": {
+ GSTR1_DataField.DOC_TYPE.value: GSTR1_SubCategory.SUPECOM_52.value,
+ GSTR1_DataField.ECOMMERCE_GSTIN.value: "20ALYPD6528PQC5",
+ GSTR1_DataField.TAXABLE_VALUE.value: 10000,
+ GSTR1_ItemField.IGST.value: 1000,
+ GSTR1_ItemField.CGST.value: 0,
+ GSTR1_ItemField.SGST.value: 0,
+ GSTR1_ItemField.CESS.value: 0,
+ }
+ },
+ GSTR1_SubCategory.SUPECOM_9_5.value: {
+ "20ALYPD6528PQC5": {
+ GSTR1_DataField.DOC_TYPE.value: GSTR1_SubCategory.SUPECOM_9_5.value,
+ GSTR1_DataField.ECOMMERCE_GSTIN.value: "20ALYPD6528PQC5",
+ GSTR1_DataField.TAXABLE_VALUE.value: 10000,
+ GSTR1_ItemField.IGST.value: 1000,
+ GSTR1_ItemField.CGST.value: 0,
+ GSTR1_ItemField.SGST.value: 0,
+ GSTR1_ItemField.CESS.value: 0,
+ }
+ },
+ }
+
+ def test_convert_to_internal_data_format(self):
+ output = SUPECOM().convert_to_internal_data_format(self.json_data)
+ self.assertDictEqual(self.mapped_data, output)
+
+ def test_convert_to_gov_data_format(self):
+ output = SUPECOM().convert_to_gov_data_format(
+ process_mapped_data(self.mapped_data)
+ )
+ self.assertDictEqual(self.json_data, output)
diff --git a/india_compliance/gst_india/utils/gstr/__init__.py b/india_compliance/gst_india/utils/gstr_2/__init__.py
similarity index 84%
rename from india_compliance/gst_india/utils/gstr/__init__.py
rename to india_compliance/gst_india/utils/gstr_2/__init__.py
index ee3ccc1a0..9c258d9b7 100644
--- a/india_compliance/gst_india/utils/gstr/__init__.py
+++ b/india_compliance/gst_india/utils/gstr_2/__init__.py
@@ -5,22 +5,13 @@
from frappe.query_builder.terms import Criterion
from frappe.utils import cint
-from india_compliance.gst_india.api_classes.returns import (
- GSTR2aAPI,
- GSTR2bAPI,
- ReturnsAPI,
-)
+from india_compliance.gst_india.api_classes.returns import GSTR2aAPI, GSTR2bAPI
from india_compliance.gst_india.doctype.gstr_import_log.gstr_import_log import (
create_import_log,
- toggle_scheduled_jobs,
)
from india_compliance.gst_india.utils import get_party_for_gstin
-from india_compliance.gst_india.utils.gstr import gstr_2a, gstr_2b
-
-
-class ReturnType(Enum):
- GSTR2A = "GSTR2a"
- GSTR2B = "GSTR2b"
+from india_compliance.gst_india.utils.gstr_2 import gstr_2a, gstr_2b
+from india_compliance.gst_india.utils.gstr_utils import ReturnType
class GSTRCategory(Enum):
@@ -334,68 +325,6 @@ def _download_gstr_2a(gstin, return_period, json_data):
save_gstr_2a(gstin, return_period, json_data)
-GSTR_FUNCTIONS = {
- ReturnType.GSTR2A.value: _download_gstr_2a,
- ReturnType.GSTR2B.value: save_gstr_2b,
-}
-
-
-def download_queued_request():
- queued_requests = frappe.get_all(
- "GSTR Import Log",
- filters={"request_id": ["is", "set"]},
- fields=[
- "name",
- "gstin",
- "return_type",
- "classification",
- "return_period",
- "request_id",
- "request_time",
- ],
- )
-
- if not queued_requests:
- return toggle_scheduled_jobs(stopped=True)
-
- for doc in queued_requests:
- frappe.enqueue(_download_queued_request, queue="long", doc=doc)
-
-
-def _download_queued_request(doc):
- try:
- api = ReturnsAPI(doc.gstin)
- response = api.download_files(
- doc.return_period,
- doc.request_id,
- )
-
- except Exception as e:
- frappe.db.delete("GSTR Import Log", {"name": doc.name})
- raise e
-
- if response.error_type in ["otp_requested", "invalid_otp"]:
- return toggle_scheduled_jobs(stopped=True)
-
- if response.error_type == "no_docs_found":
- return create_import_log(
- doc.gstin,
- doc.return_type,
- doc.return_period,
- doc.classification,
- data_not_found=True,
- )
-
- if response.error_type == "queued":
- return
-
- if response.error_type:
- return frappe.db.delete("GSTR Import Log", {"name": doc.name})
-
- frappe.db.set_value("GSTR Import Log", doc.name, "request_id", None)
- GSTR_FUNCTIONS[doc.return_type](doc.gstin, doc.return_period, response)
-
-
def show_queued_message():
frappe.msgprint(
_(
diff --git a/india_compliance/gst_india/utils/gstr/gstr.py b/india_compliance/gst_india/utils/gstr_2/gstr.py
similarity index 65%
rename from india_compliance/gst_india/utils/gstr/gstr.py
rename to india_compliance/gst_india/utils/gstr_2/gstr.py
index 06305f840..f9979cc59 100644
--- a/india_compliance/gst_india/utils/gstr/gstr.py
+++ b/india_compliance/gst_india/utils/gstr_2/gstr.py
@@ -1,13 +1,9 @@
import frappe
-from frappe import _
-from frappe.utils import add_to_date, now_datetime
from india_compliance.gst_india.constants import STATE_NUMBERS
from india_compliance.gst_india.doctype.gst_inward_supply.gst_inward_supply import (
create_inward_supply,
)
-from india_compliance.gst_india.utils import get_gstin_list
-from india_compliance.gst_india.utils.gstr import ReturnsAPI
def get_mapped_value(value, mapping):
@@ -146,85 +142,3 @@ def set_key(self, key, value):
def update_gstins(self):
pass
-
-
-@frappe.whitelist()
-def validate_company_gstins(company=None, company_gstin=None):
- """
- Checks the validity of the company's GSTIN authentication.
-
- Args:
- company_gstin (str): The GSTIN of the company to validate.
-
- Returns:
- dict: A dictionary where the keys are the GSTINs and the values are booleans indicating whether the authentication is valid.
- """
- frappe.has_permission("GST Settings", throw=True)
-
- credentials = get_company_gstin_credentials(company, company_gstin)
-
- if company_gstin and not credentials:
- frappe.throw(
- _("Missing GSTIN credentials for GSTIN: {gstin}.").format(
- gstin=company_gstin
- )
- )
-
- if not credentials:
- frappe.throw(_("Missing credentials in GST Settings"))
-
- if company and not company_gstin:
- missing_credentials = set(get_gstin_list(company)) - set(
- credential.gstin for credential in credentials
- )
-
- if missing_credentials:
- frappe.throw(
- _("Missing GSTIN credentials for GSTIN(s): {gstins}.").format(
- gstins=", ".join(missing_credentials),
- )
- )
-
- gstin_authentication_status = {
- credential.gstin: (
- credential.session_expiry
- and credential.auth_token
- and credential.session_expiry > add_to_date(now_datetime(), minutes=30)
- )
- for credential in credentials
- }
-
- return gstin_authentication_status
-
-
-def get_company_gstin_credentials(company=None, company_gstin=None):
- filters = {"service": "Returns"}
-
- if company:
- filters["company"] = company
-
- if company_gstin:
- filters["gstin"] = company_gstin
-
- return frappe.get_all(
- "GST Credential",
- filters=filters,
- fields=["gstin", "session_expiry", "auth_token"],
- )
-
-
-@frappe.whitelist()
-def request_otp(company_gstin):
- frappe.has_permission("GST Settings", throw=True)
-
- return ReturnsAPI(company_gstin).request_otp()
-
-
-@frappe.whitelist()
-def authenticate_otp(company_gstin, otp):
- frappe.has_permission("GST Settings", throw=True)
-
- api = ReturnsAPI(company_gstin)
- response = api.autheticate_with_otp(otp)
-
- return api.process_response(response)
diff --git a/india_compliance/gst_india/utils/gstr/gstr_2a.py b/india_compliance/gst_india/utils/gstr_2/gstr_2a.py
similarity index 99%
rename from india_compliance/gst_india/utils/gstr/gstr_2a.py
rename to india_compliance/gst_india/utils/gstr_2/gstr_2a.py
index 770a0c7ee..f9636ff9e 100644
--- a/india_compliance/gst_india/utils/gstr/gstr_2a.py
+++ b/india_compliance/gst_india/utils/gstr_2/gstr_2a.py
@@ -3,7 +3,7 @@
import frappe
from india_compliance.gst_india.utils import get_datetime, parse_datetime
-from india_compliance.gst_india.utils.gstr.gstr import GSTR, get_mapped_value
+from india_compliance.gst_india.utils.gstr_2.gstr import GSTR, get_mapped_value
def map_date_format(date_str, source_format, target_format):
diff --git a/india_compliance/gst_india/utils/gstr/gstr_2b.py b/india_compliance/gst_india/utils/gstr_2/gstr_2b.py
similarity index 98%
rename from india_compliance/gst_india/utils/gstr/gstr_2b.py
rename to india_compliance/gst_india/utils/gstr_2/gstr_2b.py
index a1165623e..1e9b348b1 100644
--- a/india_compliance/gst_india/utils/gstr/gstr_2b.py
+++ b/india_compliance/gst_india/utils/gstr_2/gstr_2b.py
@@ -1,7 +1,7 @@
import frappe
from india_compliance.gst_india.utils import parse_datetime
-from india_compliance.gst_india.utils.gstr.gstr import GSTR, get_mapped_value
+from india_compliance.gst_india.utils.gstr_2.gstr import GSTR, get_mapped_value
class GSTR2b(GSTR):
diff --git a/india_compliance/gst_india/utils/gstr/test_gstr_2a.py b/india_compliance/gst_india/utils/gstr_2/test_gstr_2a.py
similarity index 98%
rename from india_compliance/gst_india/utils/gstr/test_gstr_2a.py
rename to india_compliance/gst_india/utils/gstr_2/test_gstr_2a.py
index a58cafc54..4993aef91 100644
--- a/india_compliance/gst_india/utils/gstr/test_gstr_2a.py
+++ b/india_compliance/gst_india/utils/gstr_2/test_gstr_2a.py
@@ -7,7 +7,7 @@
from frappe.utils import get_datetime
from india_compliance.gst_india.utils import get_data_file_path
-from india_compliance.gst_india.utils.gstr import (
+from india_compliance.gst_india.utils.gstr_2 import (
GSTRCategory,
ReturnType,
download_gstr_2a,
@@ -64,8 +64,8 @@ def tearDownClass(cls):
frappe.db.delete(cls.doctype, {"company_gstin": cls.gstin})
frappe.db.delete(cls.log_doctype, {"gstin": cls.gstin})
- @patch("india_compliance.gst_india.utils.gstr.save_gstr")
- @patch("india_compliance.gst_india.utils.gstr.GSTR2aAPI")
+ @patch("india_compliance.gst_india.utils.gstr_2.save_gstr")
+ @patch("india_compliance.gst_india.utils.gstr_2.GSTR2aAPI")
def test_download_gstr_2a(self, mock_gstr_2a_api, mock_save_gstr):
def mock_get_data(action, return_period, otp):
if action in ["B2B", "B2BA", "CDN", "CDNA"]:
diff --git a/india_compliance/gst_india/utils/gstr/test_gstr_2b.py b/india_compliance/gst_india/utils/gstr_2/test_gstr_2b.py
similarity index 98%
rename from india_compliance/gst_india/utils/gstr/test_gstr_2b.py
rename to india_compliance/gst_india/utils/gstr_2/test_gstr_2b.py
index 3d7002b6b..ec940318a 100644
--- a/india_compliance/gst_india/utils/gstr/test_gstr_2b.py
+++ b/india_compliance/gst_india/utils/gstr_2/test_gstr_2b.py
@@ -5,8 +5,8 @@
from frappe.tests.utils import FrappeTestCase
from india_compliance.gst_india.utils import get_data_file_path
-from india_compliance.gst_india.utils.gstr import GSTRCategory, save_gstr_2b
-from india_compliance.gst_india.utils.gstr.test_gstr_2a import TestGSTRMixin
+from india_compliance.gst_india.utils.gstr_2 import GSTRCategory, save_gstr_2b
+from india_compliance.gst_india.utils.gstr_2.test_gstr_2a import TestGSTRMixin
class TestGSTR2b(FrappeTestCase, TestGSTRMixin):
diff --git a/india_compliance/gst_india/utils/gstr_utils.py b/india_compliance/gst_india/utils/gstr_utils.py
new file mode 100644
index 000000000..2e9c0a2c1
--- /dev/null
+++ b/india_compliance/gst_india/utils/gstr_utils.py
@@ -0,0 +1,170 @@
+from enum import Enum
+
+import frappe
+from frappe import _
+from frappe.utils import add_to_date, now_datetime
+
+from india_compliance.gst_india.api_classes.returns import ReturnsAPI
+from india_compliance.gst_india.doctype.gstr_import_log.gstr_import_log import (
+ create_import_log,
+ toggle_scheduled_jobs,
+)
+from india_compliance.gst_india.utils import get_gstin_list
+from india_compliance.gst_india.utils.gstr_1.gstr_1_download import (
+ save_gstr_1_filed_data,
+ save_gstr_1_unfiled_data,
+)
+
+
+class ReturnType(Enum):
+ GSTR2A = "GSTR2a"
+ GSTR2B = "GSTR2b"
+ GSTR1 = "GSTR1"
+ UnfiledGSTR1 = "Unfiled GSTR1"
+
+
+@frappe.whitelist()
+def validate_company_gstins(company=None, company_gstin=None):
+ """
+ Checks the validity of the company's GSTIN authentication.
+
+ Args:
+ company_gstin (str): The GSTIN of the company to validate.
+
+ Returns:
+ dict: A dictionary where the keys are the GSTINs and the values are booleans indicating whether the authentication is valid.
+ """
+ frappe.has_permission("GST Settings", throw=True)
+
+ credentials = get_company_gstin_credentials(company, company_gstin)
+
+ if company_gstin and not credentials:
+ frappe.throw(
+ _("Missing GSTIN credentials for GSTIN: {gstin}.").format(
+ gstin=company_gstin
+ )
+ )
+
+ if not credentials:
+ frappe.throw(_("Missing credentials in GST Settings"))
+
+ if company and not company_gstin:
+ missing_credentials = set(get_gstin_list(company)) - set(
+ credential.gstin for credential in credentials
+ )
+
+ if missing_credentials:
+ frappe.throw(
+ _("Missing GSTIN credentials for GSTIN(s): {gstins}.").format(
+ gstins=", ".join(missing_credentials),
+ )
+ )
+
+ gstin_authentication_status = {
+ credential.gstin: (
+ credential.session_expiry
+ and credential.auth_token
+ and credential.session_expiry > add_to_date(now_datetime(), minutes=30)
+ )
+ for credential in credentials
+ }
+
+ return gstin_authentication_status
+
+
+def get_company_gstin_credentials(company=None, company_gstin=None):
+ filters = {"service": "Returns"}
+
+ if company:
+ filters["company"] = company
+
+ if company_gstin:
+ filters["gstin"] = company_gstin
+
+ return frappe.get_all(
+ "GST Credential",
+ filters=filters,
+ fields=["gstin", "session_expiry", "auth_token"],
+ )
+
+
+@frappe.whitelist()
+def request_otp(company_gstin):
+ frappe.has_permission("GST Settings", throw=True)
+
+ return ReturnsAPI(company_gstin).request_otp()
+
+
+@frappe.whitelist()
+def authenticate_otp(company_gstin, otp):
+ frappe.has_permission("GST Settings", throw=True)
+
+ api = ReturnsAPI(company_gstin)
+ response = api.autheticate_with_otp(otp)
+
+ return api.process_response(response)
+
+
+def download_queued_request():
+ queued_requests = frappe.get_all(
+ "GSTR Import Log",
+ filters={"request_id": ["is", "set"]},
+ fields=[
+ "name",
+ "gstin",
+ "return_type",
+ "classification",
+ "return_period",
+ "request_id",
+ "request_time",
+ ],
+ )
+
+ if not queued_requests:
+ return toggle_scheduled_jobs(stopped=True)
+
+ for doc in queued_requests:
+ frappe.enqueue(_download_queued_request, queue="long", doc=doc)
+
+
+def _download_queued_request(doc):
+ from india_compliance.gst_india.utils.gstr_2 import _download_gstr_2a, save_gstr_2b
+
+ GSTR_FUNCTIONS = {
+ ReturnType.GSTR2A.value: _download_gstr_2a,
+ ReturnType.GSTR2B.value: save_gstr_2b,
+ ReturnType.GSTR1.value: save_gstr_1_filed_data,
+ ReturnType.UnfiledGSTR1.value: save_gstr_1_unfiled_data,
+ }
+
+ try:
+ api = ReturnsAPI(doc.gstin)
+ response = api.download_files(
+ doc.return_period,
+ doc.request_id,
+ )
+
+ except Exception as e:
+ frappe.db.delete("GSTR Import Log", doc.name)
+ raise e
+
+ if response.error_type in ["otp_requested", "invalid_otp"]:
+ return toggle_scheduled_jobs(stopped=True)
+
+ if response.error_type == "no_docs_found":
+ return create_import_log(
+ doc.gstin,
+ doc.return_type,
+ doc.return_period,
+ doc.classification,
+ data_not_found=True,
+ )
+
+ if response.error_type == "queued":
+ return
+
+ if response.error_type:
+ return frappe.db.delete("GSTR Import Log", {"name": doc.name})
+
+ frappe.db.set_value("GSTR Import Log", doc.name, "request_id", None)
+ GSTR_FUNCTIONS[doc.return_type](doc.gstin, doc.return_period, response)
diff --git a/india_compliance/gst_india/workspace/gst_india/gst_india.json b/india_compliance/gst_india/workspace/gst_india/gst_india.json
index 35379c901..0bf634fe1 100644
--- a/india_compliance/gst_india/workspace/gst_india/gst_india.json
+++ b/india_compliance/gst_india/workspace/gst_india/gst_india.json
@@ -1,6 +1,6 @@
{
"charts": [],
- "content": "[{\"id\":\"Xz6h3FH8sZ\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Pending e-Waybills\",\"col\":4}},{\"id\":\"5wHVnb2VB-\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Pending e-Invoices\",\"col\":4}},{\"id\":\"FT76_s4_M1\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Invoice Cancelled, e-Invoice Active\",\"col\":4}},{\"id\":\"ROouz1137a\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"pSkSY7vg5b\",\"type\":\"header\",\"data\":{\"text\":\"Shortcuts\",\"col\":12}},{\"id\":\"kJscjOHSLN\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"GST Settings\",\"col\":3}},{\"id\":\"BT3ZIyt2_1\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"GSTR-1\",\"col\":3}},{\"id\":\"fVgN1kK1r6\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"GSTR-3B\",\"col\":3}},{\"id\":\"kxE1gBYYUW\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Reconciliation Tool\",\"col\":3}},{\"id\":\"-G2gVutaOP\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"HSN Code\",\"col\":3}},{\"id\":\"sEPHpNM3bl\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"India Compliance Account\",\"col\":3}},{\"id\":\"bVd6Hbw3Yp\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"oN0FbtHAdc\",\"type\":\"header\",\"data\":{\"text\":\"Reports and Masters\",\"col\":12}},{\"id\":\"doCaNmtmY8\",\"type\":\"card\",\"data\":{\"card_name\":\"Sales and Purchase Reports\",\"col\":4}},{\"id\":\"Emy7VbsSYq\",\"type\":\"card\",\"data\":{\"card_name\":\"Logs\",\"col\":4}},{\"id\":\"2FUZmTJKVp\",\"type\":\"card\",\"data\":{\"card_name\":\"Other GST Reports\",\"col\":4}},{\"id\":\"PhR9LKvBdc\",\"type\":\"card\",\"data\":{\"card_name\":\"New Reports\",\"col\":3}}]",
+ "content": "[{\"id\":\"Xz6h3FH8sZ\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Pending e-Waybills\",\"col\":4}},{\"id\":\"5wHVnb2VB-\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Pending e-Invoices\",\"col\":4}},{\"id\":\"FT76_s4_M1\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Invoice Cancelled, e-Invoice Active\",\"col\":4}},{\"id\":\"ROouz1137a\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"pSkSY7vg5b\",\"type\":\"header\",\"data\":{\"text\":\"Shortcuts\",\"col\":12}},{\"id\":\"kJscjOHSLN\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"GST Settings\",\"col\":3}},{\"id\":\"BT3ZIyt2_1\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"GSTR-1 Beta\",\"col\":3}},{\"id\":\"fVgN1kK1r6\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"GSTR-3B\",\"col\":3}},{\"id\":\"kxE1gBYYUW\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Reconciliation Tool\",\"col\":3}},{\"id\":\"-G2gVutaOP\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"HSN Code\",\"col\":3}},{\"id\":\"sEPHpNM3bl\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"India Compliance Account\",\"col\":3}},{\"id\":\"bVd6Hbw3Yp\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"oN0FbtHAdc\",\"type\":\"header\",\"data\":{\"text\":\"Reports and Masters\",\"col\":12}},{\"id\":\"doCaNmtmY8\",\"type\":\"card\",\"data\":{\"card_name\":\"Sales and Purchase Reports\",\"col\":4}},{\"id\":\"Emy7VbsSYq\",\"type\":\"card\",\"data\":{\"card_name\":\"Logs\",\"col\":4}},{\"id\":\"2FUZmTJKVp\",\"type\":\"card\",\"data\":{\"card_name\":\"Other GST Reports\",\"col\":4}},{\"id\":\"PhR9LKvBdc\",\"type\":\"card\",\"data\":{\"card_name\":\"New Reports\",\"col\":3}}]",
"creation": "2022-02-28 19:04:58.655348",
"custom_blocks": [],
"docstatus": 0,
@@ -12,14 +12,44 @@
"is_hidden": 0,
"label": "GST India",
"links": [
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "New Reports",
+ "link_count": 1,
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "GST Sales Register Beta",
+ "link_count": 0,
+ "link_to": "GST Sales Register Beta",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
{
"hidden": 0,
"is_query_report": 0,
"label": "Sales and Purchase Reports",
- "link_count": 5,
+ "link_count": 6,
+ "link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "GSTR-1",
+ "link_count": 0,
+ "link_to": "GSTR-1",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
{
"hidden": 0,
"is_query_report": 1,
@@ -73,130 +103,133 @@
{
"hidden": 0,
"is_query_report": 0,
- "label": "Other GST Reports",
+ "label": "Logs",
"link_count": 5,
+ "link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
- "is_query_report": 1,
- "label": "e-Invoice Summary",
+ "is_query_report": 0,
+ "label": "e-Waybill Log",
"link_count": 0,
- "link_to": "e-Invoice Summary",
- "link_type": "Report",
+ "link_to": "e-Waybill Log",
+ "link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
- "label": "GSTR-3B Details",
+ "label": "e-Invoice Log",
"link_count": 0,
- "link_to": "GSTR-3B Details",
- "link_type": "Report",
+ "link_to": "e-Invoice Log",
+ "link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
- "label": "GST Balance",
+ "label": "GSTR-1 Log",
"link_count": 0,
- "link_to": "GST Balance",
- "link_type": "Report",
+ "link_to": "GSTR-1 Log",
+ "link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
- "is_query_report": 1,
- "label": "HSN-wise Summary of Outward Supplies",
+ "is_query_report": 0,
+ "label": "GST Inward Supply",
"link_count": 0,
- "link_to": "HSN-wise-summary of outward supplies",
- "link_type": "Report",
+ "link_to": "GST Inward Supply",
+ "link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
- "label": "Audit Trail",
+ "label": "Integration Request",
"link_count": 0,
- "link_to": "Audit Trail",
- "link_type": "Report",
+ "link_to": "Integration Request",
+ "link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
- "label": "Logs",
- "link_count": 4,
+ "label": "Other GST Reports",
+ "link_count": 6,
+ "link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
- "is_query_report": 0,
- "label": "e-Waybill Log",
+ "is_query_report": 1,
+ "label": "e-Invoice Summary",
"link_count": 0,
- "link_to": "e-Waybill Log",
- "link_type": "DocType",
+ "link_to": "e-Invoice Summary",
+ "link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
- "label": "e-Invoice Log",
+ "label": "GSTR-3B Details",
"link_count": 0,
- "link_to": "e-Invoice Log",
- "link_type": "DocType",
+ "link_to": "GSTR-3B Details",
+ "link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
- "label": "GST Inward Supply",
+ "label": "GST Balance",
"link_count": 0,
- "link_to": "GST Inward Supply",
- "link_type": "DocType",
+ "link_to": "GST Balance",
+ "link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
- "is_query_report": 0,
- "label": "Integration Request",
+ "is_query_report": 1,
+ "label": "HSN-wise Summary of Outward Supplies",
"link_count": 0,
- "link_to": "Integration Request",
- "link_type": "DocType",
+ "link_to": "HSN-wise-summary of outward supplies",
+ "link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
- "label": "New Reports",
- "link_count": 1,
- "link_type": "DocType",
+ "label": "Audit Trail",
+ "link_count": 0,
+ "link_to": "Audit Trail",
+ "link_type": "Report",
"onboard": 0,
- "type": "Card Break"
+ "type": "Link"
},
{
"hidden": 0,
- "is_query_report": 1,
- "label": "GST Sales Register Beta",
+ "is_query_report": 0,
+ "label": "GST Advance Detail",
"link_count": 0,
- "link_to": "GST Sales Register Beta",
+ "link_to": "GST Advance Detail",
"link_type": "Report",
"onboard": 0,
"type": "Link"
}
],
- "modified": "2024-03-26 16:43:35.237283",
+ "modified": "2024-06-08 16:54:55.015726",
"modified_by": "Administrator",
"module": "GST India",
"name": "GST India",
@@ -229,9 +262,9 @@
{
"color": "Grey",
"doc_view": "List",
- "label": "GSTR-1",
- "link_to": "GSTR-1",
- "type": "Report"
+ "label": "GSTR-1 Beta",
+ "link_to": "GSTR-1 Beta",
+ "type": "DocType"
},
{
"color": "Grey",
diff --git a/india_compliance/hooks.py b/india_compliance/hooks.py
index bab5f0e59..0924ffb11 100644
--- a/india_compliance/hooks.py
+++ b/india_compliance/hooks.py
@@ -126,6 +126,7 @@
"validate": "india_compliance.gst_india.overrides.payment_entry.validate",
"on_submit": "india_compliance.gst_india.overrides.payment_entry.on_submit",
"on_update_after_submit": "india_compliance.gst_india.overrides.payment_entry.on_update_after_submit",
+ "before_cancel": "india_compliance.gst_india.overrides.payment_entry.before_cancel",
},
"Purchase Invoice": {
"onload": [
@@ -384,7 +385,7 @@
"cron": {
"*/5 * * * *": [
"india_compliance.gst_india.utils.e_invoice.retry_e_invoice_e_waybill_generation",
- "india_compliance.gst_india.utils.gstr.download_queued_request",
+ "india_compliance.gst_india.utils.gstr_utils.download_queued_request",
"india_compliance.gst_india.doctype.purchase_reconciliation_tool.purchase_reconciliation_tool.auto_refresh_authtoken",
],
"0 2 * * *": [
diff --git a/india_compliance/patches.txt b/india_compliance/patches.txt
index c93c1a889..5f104a13a 100644
--- a/india_compliance/patches.txt
+++ b/india_compliance/patches.txt
@@ -47,6 +47,7 @@ execute:from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation i
india_compliance.patches.v14.set_item_details_from_purchase_invoice_to_bill_of_entry
india_compliance.patches.v14.update_item_gst_details_and_gst_trearment_in_bill_of_entry
india_compliance.patches.v14.update_default_auto_reconciliation_settings
+india_compliance.patches.v14.update_default_gstr1_settings
india_compliance.patches.v14.add_match_found_in_purchase_reconciliation_status
india_compliance.patches.v14.unset_inward_supply_link_for_cancelled_purchase
india_compliance.patches.v14.delete_not_generated_gstr_import_log
diff --git a/india_compliance/patches/post_install/improve_item_tax_template.py b/india_compliance/patches/post_install/improve_item_tax_template.py
index 36baa6ca2..bf3dcd141 100644
--- a/india_compliance/patches/post_install/improve_item_tax_template.py
+++ b/india_compliance/patches/post_install/improve_item_tax_template.py
@@ -275,38 +275,48 @@ def update_gst_treatment_for_transactions():
table = frappe.qb.DocType(item_doctype)
query = frappe.qb.update(table)
+ doctype = item_doctype.replace(" Item", "")
- (
- query.set(
- table.gst_treatment,
- Case()
- .when(table.is_nil_exempt == 1, "Nil-Rated")
- .when(table.is_non_gst == 1, "Non-GST")
- .else_("Taxable"),
- )
- .where(IfNull(table.gst_treatment, "") == "")
- .run()
+ update_gst_treatment_for_nil_exempt_and_non_gst(table, query, item_doctype)
+ update_gst_treatment_for_zero_rated(table, query, doctype)
+
+
+def update_gst_treatment_for_nil_exempt_and_non_gst(table, query, item_doctype):
+ if not frappe.db.has_column(item_doctype, "is_nil_exempt"):
+ return
+
+ (
+ query.set(
+ table.gst_treatment,
+ Case()
+ .when(table.is_nil_exempt == 1, "Nil-Rated")
+ .when(table.is_non_gst == 1, "Non-GST")
+ .else_("Taxable"),
)
+ .where(IfNull(table.gst_treatment, "") == "")
+ .run()
+ )
- doctype = item_doctype.replace(" Item", "")
- if doctype not in SALES_DOCTYPES:
- continue
- doc = frappe.qb.DocType(doctype)
-
- (
- query.join(doc)
- .on(doc.name == table.parent)
- .set(table.gst_treatment, "Zero-Rated")
- .where(
- (doc.gst_category == "SEZ")
- | (
- (doc.gst_category == "Overseas")
- & (doc.place_of_supply == "96-Other Countries")
- )
+def update_gst_treatment_for_zero_rated(table, query, doctype):
+ if doctype not in SALES_DOCTYPES:
+ return
+
+ doc = frappe.qb.DocType(doctype)
+
+ (
+ query.join(doc)
+ .on(doc.name == table.parent)
+ .set(table.gst_treatment, "Zero-Rated")
+ .where(
+ (doc.gst_category == "SEZ")
+ | (
+ (doc.gst_category == "Overseas")
+ & (doc.place_of_supply == "96-Other Countries")
)
- .run()
)
+ .run()
+ )
def update_gst_details_for_transactions(companies):
diff --git a/india_compliance/patches/v14/update_default_gstr1_settings.py b/india_compliance/patches/v14/update_default_gstr1_settings.py
new file mode 100644
index 000000000..e8f4ef986
--- /dev/null
+++ b/india_compliance/patches/v14/update_default_gstr1_settings.py
@@ -0,0 +1,12 @@
+import frappe
+
+
+def execute():
+ frappe.db.set_single_value(
+ "GST Settings",
+ {
+ "compare_gstr_1_data": 1,
+ "freeze_transactions": 1,
+ "filing_frequency": "Monthly",
+ },
+ )
diff --git a/india_compliance/public/js/purchase_reconciliation_tool/data_table_manager.js b/india_compliance/public/js/components/data_table_manager.js
similarity index 93%
rename from india_compliance/public/js/purchase_reconciliation_tool/data_table_manager.js
rename to india_compliance/public/js/components/data_table_manager.js
index a26900945..bcd375e13 100644
--- a/india_compliance/public/js/purchase_reconciliation_tool/data_table_manager.js
+++ b/india_compliance/public/js/components/data_table_manager.js
@@ -78,12 +78,7 @@ india_compliance.DataTableManager = class DataTableManager {
value = column._value(value, column, data);
}
- return frappe.form.get_formatter(column.docfield.fieldtype)(
- value,
- column.docfield,
- { always_show_decimals: true },
- data
- );
+ return frappe.format(value, column, { always_show_decimals: true }, data);
};
return {
@@ -125,7 +120,7 @@ india_compliance.DataTableManager = class DataTableManager {
dynamicRowHeight: true,
checkboxColumn: true,
inlineFilters: true,
- noDataMessage: "No Matching Data Found!",
+ noDataMessage: __("No Matching Data Found!"),
// clusterize: false,
events: {
onCheckRow: () => {
diff --git a/india_compliance/public/js/purchase_reconciliation_tool/filter_group.js b/india_compliance/public/js/components/filter_group.js
similarity index 100%
rename from india_compliance/public/js/purchase_reconciliation_tool/filter_group.js
rename to india_compliance/public/js/components/filter_group.js
diff --git a/india_compliance/public/js/purchase_reconciliation_tool/number_card.js b/india_compliance/public/js/components/number_card.js
similarity index 100%
rename from india_compliance/public/js/purchase_reconciliation_tool/number_card.js
rename to india_compliance/public/js/components/number_card.js
diff --git a/india_compliance/public/js/components/set_gstin_options.js b/india_compliance/public/js/components/set_gstin_options.js
new file mode 100644
index 000000000..e2307ebda
--- /dev/null
+++ b/india_compliance/public/js/components/set_gstin_options.js
@@ -0,0 +1,14 @@
+frappe.provide("india_compliance");
+
+india_compliance.set_gstin_options = async function (frm) {
+ const { query, params } = india_compliance.get_gstin_query(frm.doc.company);
+ const { message } = await frappe.call({
+ method: query,
+ args: params,
+ });
+
+ if (!message) return [];
+ const gstin_field = frm.get_field("company_gstin");
+ gstin_field.set_data(message);
+ return message;
+}
diff --git a/india_compliance/public/js/components/view_group.js b/india_compliance/public/js/components/view_group.js
new file mode 100644
index 000000000..328499083
--- /dev/null
+++ b/india_compliance/public/js/components/view_group.js
@@ -0,0 +1,81 @@
+frappe.provide("india_compliance");
+
+india_compliance.ViewGroup = class ViewGroup {
+ constructor(options) {
+ Object.assign(this, options);
+ this.views = {};
+ this.render();
+ }
+
+ render() {
+ $(this.$wrapper).append(
+ `
+
+ `
+ );
+
+ this.view_group_container = $(`
+
+ `).appendTo(this.$wrapper.find(`.view-switch`));
+
+ this.make_views();
+ this.setup_events();
+ }
+
+ set_active_view(view) {
+ this.active_view = view;
+ this.views[`${view}_view`].children().tab("show");
+ }
+
+ make_views() {
+ this.view_names.forEach(view => {
+ this.views[`${view}_view`] = $(
+ `
+
+
+ ${frappe.unscrub(view)}
+
+
+ `
+ ).appendTo(this.view_group_container);
+ });
+ }
+
+ setup_events() {
+ this.view_group_container.off("click").on("click", ".nav-link", e => {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+
+ this.target = $(e.currentTarget);
+ const target_view = this.target.attr("data-fieldname");
+
+ this.set_active_view(target_view);
+ this.callback && this.callback(target_view);
+ });
+ }
+
+ disable_view(view, title) {
+ this.views[`${view}_view`].attr("title", title);
+ this.views[`${view}_view`].find(".nav-link").addClass("disabled");
+ }
+
+ enable_view(view) {
+ this.views[`${view}_view`].removeAttr("title");
+ this.views[`${view}_view`].find(".nav-link").removeClass("disabled");
+ }
+}
\ No newline at end of file
diff --git a/india_compliance/public/js/gstr1.bundle.js b/india_compliance/public/js/gstr1.bundle.js
new file mode 100644
index 000000000..759400355
--- /dev/null
+++ b/india_compliance/public/js/gstr1.bundle.js
@@ -0,0 +1,4 @@
+import "./components/filter_group";
+import "./components/data_table_manager";
+import "./components/set_gstin_options";
+import "./components/view_group";
\ No newline at end of file
diff --git a/india_compliance/public/js/purchase_reconciliation_tool.bundle.js b/india_compliance/public/js/purchase_reconciliation_tool.bundle.js
new file mode 100644
index 000000000..e236f7422
--- /dev/null
+++ b/india_compliance/public/js/purchase_reconciliation_tool.bundle.js
@@ -0,0 +1,4 @@
+import "./components/data_table_manager";
+import "./components/filter_group";
+import "./components/number_card";
+import "./components/set_gstin_options";
diff --git a/india_compliance/public/js/purchase_reconciliation_tool/purchase_reconciliation_tool.bundle.js b/india_compliance/public/js/purchase_reconciliation_tool/purchase_reconciliation_tool.bundle.js
deleted file mode 100644
index 88785c2ba..000000000
--- a/india_compliance/public/js/purchase_reconciliation_tool/purchase_reconciliation_tool.bundle.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import "./data_table_manager";
-import "./filter_group";
-import "./number_card";
diff --git a/india_compliance/public/js/utils.js b/india_compliance/public/js/utils.js
index f207172d8..7f1dc236a 100644
--- a/india_compliance/public/js/utils.js
+++ b/india_compliance/public/js/utils.js
@@ -13,6 +13,40 @@ frappe.provide("india_compliance");
window.gst_settings = frappe.boot.gst_settings;
Object.assign(india_compliance, {
+ MONTH: [
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December",
+ ],
+
+ QUARTER: ["Jan-Mar", "Apr-Jun", "Jul-Sep", "Oct-Dec"],
+
+ get_month_year_from_period(period) {
+ /**
+ * Returns month or quarter and year from the period
+ * Month or quarter depends on the filing frequency set in GST Settings
+ *
+ * @param {String} period - period in format MMYYYY
+ * @returns {Array} - [month_or_quarter, year]
+ */
+
+ const { filing_frequency } = gst_settings;
+ const month_number = period.slice(0, 2);
+ const year = period.slice(2);
+
+ if (filing_frequency === "Monthly") return [this.MONTH[month_number - 1], year];
+ else return [this.QUARTER[Math.floor(month_number / 3)], year];
+ },
+
get_gstin_query(party, party_type = "Company") {
if (!party) {
frappe.show_alert({
@@ -148,7 +182,7 @@ Object.assign(india_compliance, {
validate_gstin(gstin) {
if (!gstin || gstin.length !== 15) {
- frappe.msgprint(__("GSTIN must be 15 characters long"))
+ frappe.msgprint(__("GSTIN must be 15 characters long"));
return;
}
@@ -162,8 +196,7 @@ Object.assign(india_compliance, {
},
get_gstin_otp(error_type, company_gstin) {
- let description =
- `An OTP has been sent to the registered mobile/email for GSTIN ${company_gstin} for further authentication. Please provide OTP.`;
+ let description = `An OTP has been sent to the registered mobile/email for GSTIN ${company_gstin} for further authentication. Please provide OTP.`;
if (error_type === "invalid_otp")
description = `Invalid OTP was provided for GSTIN ${company_gstin}. Please try again.`;
@@ -187,7 +220,7 @@ Object.assign(india_compliance, {
secondary_action_label: __("Resend OTP"),
secondary_action() {
frappe.call({
- method: "india_compliance.gst_india.utils.gstr.gstr.request_otp",
+ method: "india_compliance.gst_india.utils.gstr_utils.request_otp",
args: { company_gstin },
callback: function () {
frappe.show_alert({
@@ -331,25 +364,30 @@ Object.assign(india_compliance, {
async authenticate_company_gstins(company, company_gstin) {
const { message: gstin_authentication_status } = await frappe.call({
- method: "india_compliance.gst_india.utils.gstr.gstr.validate_company_gstins",
+ method: "india_compliance.gst_india.utils.gstr_utils.validate_company_gstins",
args: { company: company, company_gstin: company_gstin },
});
for (let gstin of Object.keys(gstin_authentication_status)) {
if (gstin_authentication_status[gstin]) continue;
- gstin_authentication_status[gstin] = await this.authenticate_otp(gstin);
+ gstin_authentication_status[gstin] =
+ await this.request_and_authenticate_otp(gstin);
}
return Object.keys(gstin_authentication_status);
},
- async authenticate_otp(gstin) {
+ async request_and_authenticate_otp(gstin) {
await frappe.call({
- method: "india_compliance.gst_india.utils.gstr.gstr.request_otp",
+ method: "india_compliance.gst_india.utils.gstr_utils.request_otp",
args: { company_gstin: gstin },
});
+ this.authenticate_otp(gstin);
+ },
+
+ async authenticate_otp(gstin) {
let error_type = "otp_requested";
let is_authenticated = false;
@@ -357,11 +395,14 @@ Object.assign(india_compliance, {
const otp = await this.get_gstin_otp(error_type, gstin);
const { message } = await frappe.call({
- method: "india_compliance.gst_india.utils.gstr.gstr.authenticate_otp",
+ method: "india_compliance.gst_india.utils.gstr_utils.authenticate_otp",
args: { company_gstin: gstin, otp: otp },
});
- if (message && ["otp_requested", "invalid_otp"].includes(message.error_type)) {
+ if (
+ message &&
+ ["otp_requested", "invalid_otp"].includes(message.error_type)
+ ) {
error_type = message.error_type;
continue;
}
@@ -369,7 +410,35 @@ Object.assign(india_compliance, {
is_authenticated = true;
return true;
}
- }
+ },
+
+ show_dismissable_alert(wrapper, message, alert_type = "primary", on_close = null) {
+ const alert = $(`
+
+ `).prependTo(wrapper);
+
+ alert.on("closed.bs.alert", () => {
+ if (on_close) on_close();
+ });
+
+ return alert;
+ },
});
function is_gstin_check_digit_valid(gstin) {
diff --git a/india_compliance/tests/__init__.py b/india_compliance/tests/__init__.py
index f3e444ab8..4760ca59e 100644
--- a/india_compliance/tests/__init__.py
+++ b/india_compliance/tests/__init__.py
@@ -62,7 +62,7 @@ def create_test_records():
)
for doctype, data in test_records.items():
- make_test_objects(doctype, data, reset=True)
+ make_test_objects(doctype, data)
if doctype == "Company":
add_companies_to_fiscal_year(data)