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": "\"No\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": "\"No\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 %} + + + + + + + + + + {% 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] || "-"); + + %} + + + + + + + {% endfor %} + +
BooksGSTR-1
{{ column_title }}{{ book_value }}{{ gov_value }}
+
\ 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`] = $( + ` + + ` + ).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)