diff --git a/india_compliance/gst_india/api_classes/taxpayer_e_invoice.py b/india_compliance/gst_india/api_classes/taxpayer_e_invoice.py index fcdf8399c..5a89a4a36 100644 --- a/india_compliance/gst_india/api_classes/taxpayer_e_invoice.py +++ b/india_compliance/gst_india/api_classes/taxpayer_e_invoice.py @@ -1,3 +1,6 @@ +import frappe +from frappe import _ + from india_compliance.gst_india.api_classes.taxpayer_base import TaxpayerBaseAPI @@ -12,6 +15,22 @@ class EInvoiceAPI(TaxpayerBaseAPI): "EINV30109": "queued", } + def setup(self, doc=None, *, company_gstin=None): + if doc: + company_gstin = doc.company_gstin + self.default_log_values.update( + reference_doctype=doc.doctype, + reference_name=doc.name, + ) + + if self.sandbox_mode: + frappe.throw(_("Sandbox mode is not supported for Taxpayer e-Invoice API")) + + if not company_gstin: + frappe.throw(_("Company GSTIN is required to use the e-Invoice API")) + + super().setup(company_gstin=company_gstin) + def get_irn_list( self, return_period, diff --git a/india_compliance/gst_india/client_scripts/e_invoice_actions.js b/india_compliance/gst_india/client_scripts/e_invoice_actions.js index 48e727ea7..8409d9114 100644 --- a/india_compliance/gst_india/client_scripts/e_invoice_actions.js +++ b/india_compliance/gst_india/client_scripts/e_invoice_actions.js @@ -49,8 +49,15 @@ frappe.ui.form.on("Sales Invoice", { frappe.call({ method: "india_compliance.gst_india.utils.e_invoice.generate_e_invoice", args: { docname: frm.doc.name, force: true }, - callback: () => { - return frm.refresh(); + callback: async (r) => { + if (r.message?.error_type == "otp_requested") { + await india_compliance.authenticate_otp(frm.doc.company_gstin); + await frappe.call({ + method: "india_compliance.gst_india.utils.e_invoice.handle_duplicate_irn_error", + args: r.message + }); + } + frm.refresh(); }, }); }, diff --git a/india_compliance/gst_india/client_scripts/sales_invoice.js b/india_compliance/gst_india/client_scripts/sales_invoice.js index 434d4e283..4e6bee8bf 100644 --- a/india_compliance/gst_india/client_scripts/sales_invoice.js +++ b/india_compliance/gst_india/client_scripts/sales_invoice.js @@ -57,7 +57,9 @@ frappe.ui.form.on(DOCTYPE, { }); async function gst_invoice_warning(frm) { - if (is_gst_invoice(frm) && !(await contains_gst_account(frm))) { + const contains_gst_account = frm.doc.taxes.some(row => row.gst_tax_type); + + if (is_gst_invoice(frm) && !contains_gst_account) { frm.dashboard.add_comment( __( `GST is applicable for this invoice but no tax accounts specified in @@ -94,23 +96,3 @@ function is_gst_invoice(frm) { else return gst_invoice_conditions; } -async function contains_gst_account(frm) { - const gst_accounts = await _get_account_options(frm.doc.company); - const accounts = frm.doc.taxes.map(taxes => taxes.account_head); - - return accounts.some(account => gst_accounts.includes(account)); -} - -async function _get_account_options(company) { - if (!frappe.flags.gst_accounts) { - frappe.flags.gst_accounts = {}; - } - - if (!frappe.flags.gst_accounts[company]) { - frappe.flags.gst_accounts[company] = await india_compliance.get_account_options( - company - ); - } - - return frappe.flags.gst_accounts[company]; -} diff --git a/india_compliance/gst_india/client_scripts/stock_entry.js b/india_compliance/gst_india/client_scripts/stock_entry.js index 89db94ea5..77cb49b8e 100644 --- a/india_compliance/gst_india/client_scripts/stock_entry.js +++ b/india_compliance/gst_india/client_scripts/stock_entry.js @@ -30,6 +30,13 @@ frappe.ui.form.on(DOCTYPE, { }); set_address_display_events(); + }, + + onload(frm) { + frm.taxes_controller = new india_compliance.taxes_controller(frm, { + total_taxable_value: "total_taxable_value", + }); + on_change_set_address( frm, "supplier_address", @@ -39,12 +46,6 @@ frappe.ui.form.on(DOCTYPE, { ); }, - onload(frm) { - frm.taxes_controller = new india_compliance.taxes_controller(frm, { - total_taxable_value: "total_taxable_value", - }); - }, - refresh(frm) { if (!gst_settings.enable_e_waybill || !gst_settings.enable_e_waybill_for_sc) return; diff --git a/india_compliance/gst_india/constants/custom_fields.py b/india_compliance/gst_india/constants/custom_fields.py index c221bb7c0..65ed9e177 100644 --- a/india_compliance/gst_india/constants/custom_fields.py +++ b/india_compliance/gst_india/constants/custom_fields.py @@ -207,7 +207,7 @@ }, { "fieldname": "bill_from_gst_category", - "label": "GST Category", + "label": "Bill From GST Category", "fieldtype": "Data", "insert_after": "bill_from_gstin", "read_only": 1, @@ -249,7 +249,7 @@ }, { "fieldname": "bill_to_gst_category", - "label": "GST Category", + "label": "Bill To GST Category", "fieldtype": "Data", "insert_after": "bill_to_gstin", "read_only": 1, diff --git a/india_compliance/gst_india/doctype/gstr_1_log/__init__.py b/india_compliance/gst_india/doctype/gst_return_log/__init__.py similarity index 100% rename from india_compliance/gst_india/doctype/gstr_1_log/__init__.py rename to india_compliance/gst_india/doctype/gst_return_log/__init__.py diff --git a/india_compliance/gst_india/doctype/gstr_1_log/gstr_1_log.py b/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py similarity index 72% rename from india_compliance/gst_india/doctype/gstr_1_log/gstr_1_log.py rename to india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py index f29d6271e..48c43a625 100644 --- a/india_compliance/gst_india/doctype/gstr_1_log/gstr_1_log.py +++ b/india_compliance/gst_india/doctype/gst_return_log/generate_gstr_1.py @@ -1,15 +1,11 @@ # 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 frappe import unscrub +from frappe.utils import flt -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, @@ -673,263 +669,3 @@ def normalize_data(data): 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()) - - 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()) - 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, warn_for_missing_credentials=False): - if not settings: - settings = frappe.get_cached_doc("GST Settings") - - if not is_production_api_enabled(settings): - return False - - if not settings.compare_gstr_1_data: - return False - - if not settings.has_valid_credentials(self.gstin, "Returns"): - if warn_for_missing_credentials: - frappe.publish_realtime( - "show_message", - dict( - message=_( - "Credentials are missing for GSTIN {0} for service" - " Returns in GST Settings" - ).format(self.gstin), - title=_("Missing Credentials"), - ), - user=frappe.session.user, - ) - - return False - - return True - - 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 - if frappe.db.exists("GSTIN", gstin): - gstin_doc = frappe.get_doc("GSTIN", gstin) - else: - 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/gstr_1_log.js b/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.js similarity index 65% rename from india_compliance/gst_india/doctype/gstr_1_log/gstr_1_log.js rename to india_compliance/gst_india/doctype/gst_return_log/gst_return_log.js index 30af20e14..0c7e31968 100644 --- a/india_compliance/gst_india/doctype/gstr_1_log/gstr_1_log.js +++ b/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.js @@ -1,32 +1,31 @@ // Copyright (c) 2024, Resilient Tech and contributors // For license information, please see license.txt - -frappe.ui.form.on("GSTR-1 Log", { +frappe.ui.form.on("GST Return Log", { refresh(frm) { - const [month_or_quarter, year] = india_compliance.get_month_year_from_period(frm.doc.return_period); + 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") + frappe.set_route("Form", "GSTR-1 Beta"); // after form loads - new Promise((resolve) => { + 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, + 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/gst_return_log/gst_return_log.json similarity index 88% rename from india_compliance/gst_india/doctype/gstr_1_log/gstr_1_log.json rename to india_compliance/gst_india/doctype/gst_return_log/gst_return_log.json index 3955a768e..2a179baa4 100644 --- a/india_compliance/gst_india/doctype/gstr_1_log/gstr_1_log.json +++ b/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.json @@ -1,8 +1,9 @@ { "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", + "autoname": "format:{return_type}-{return_period}-{gstin}", + "creation": "2024-07-06 08:47:55.801429", + "default_view": "List", + "description": "Keeps the log of GST Returns filed by GSTIN and Period", "doctype": "DocType", "engine": "InnoDB", "field_order": [ @@ -10,6 +11,7 @@ "filing_status", "return_period", "company", + "return_type", "column_break_sqwh", "filing_date", "acknowledgement_number", @@ -47,7 +49,16 @@ { "fieldname": "return_period", "fieldtype": "Data", + "in_list_view": 1, "label": "Return Period", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", "read_only": 1 }, { @@ -66,55 +77,47 @@ "label": "Acknowledgement Number", "read_only": 1 }, - { - "fieldname": "section_break_oisv", - "fieldtype": "Section Break", - "label": "Government Data" - }, { "fieldname": "gstin", "fieldtype": "Data", + "in_list_view": 1, "label": "GSTIN", - "read_only": 1 - }, - { - "fieldname": "column_break_hxfu", - "fieldtype": "Column Break" + "read_only": 1, + "reqd": 1 }, { - "fieldname": "computed_data_section", - "fieldtype": "Section Break", - "hide_border": 1, - "label": "Computed Data" + "fieldname": "generation_status", + "fieldtype": "Select", + "hidden": 1, + "label": "Generation Status", + "options": "\nIn Progress\nGenerated\nFailed", + "read_only": 1 }, { - "fieldname": "reconciled_data_section", + "fieldname": "section_break_oisv", "fieldtype": "Section Break", - "label": "Reconciled Data" + "label": "Government Data" }, { - "fieldname": "column_break_ndup", - "fieldtype": "Column Break" + "fieldname": "unfiled", + "fieldtype": "Attach", + "label": "Unfiled Invoices", + "read_only": 1 }, { - "default": "0", - "fieldname": "is_latest_data", - "fieldtype": "Check", - "label": "Is Latest Data", + "fieldname": "filed", + "fieldtype": "Attach", + "label": "Filed Data", "read_only": 1 }, { - "fieldname": "generation_status", - "fieldtype": "Select", - "hidden": 1, - "label": "Generation Status", - "options": "\nIn Progress\nGenerated\nFailed", - "read_only": 1 + "fieldname": "column_break_hxfu", + "fieldtype": "Column Break" }, { - "fieldname": "filed", + "fieldname": "unfiled_summary", "fieldtype": "Attach", - "label": "Filed Data", + "label": "Unfiled Summary", "read_only": 1 }, { @@ -123,12 +126,33 @@ "label": "Filed Summary", "read_only": 1 }, + { + "fieldname": "computed_data_section", + "fieldtype": "Section Break", + "hide_border": 1, + "label": "Computed Data" + }, + { + "default": "0", + "fieldname": "is_latest_data", + "fieldtype": "Check", + "label": "Is Latest Data", + "read_only": 1 + }, + { + "fieldname": "section_break_emlz", + "fieldtype": "Section Break" + }, { "fieldname": "books", "fieldtype": "Attach", "label": "Books Data", "read_only": 1 }, + { + "fieldname": "column_break_ehcm", + "fieldtype": "Column Break" + }, { "fieldname": "books_summary", "fieldtype": "Attach", @@ -136,52 +160,42 @@ "read_only": 1 }, { - "fieldname": "reconcile", - "fieldtype": "Attach", - "label": "Reconcile GSTR-1", - "read_only": 1 + "fieldname": "reconciled_data_section", + "fieldtype": "Section Break", + "label": "Reconciled Data" }, { - "fieldname": "reconcile_summary", + "fieldname": "reconcile", "fieldtype": "Attach", - "label": "Reconcile Summary", + "label": "Reconcile GSTR-1", "read_only": 1 }, { - "fieldname": "unfiled", - "fieldtype": "Attach", - "label": "Unfiled Invoices", - "read_only": 1 + "fieldname": "column_break_ndup", + "fieldtype": "Column Break" }, { - "fieldname": "unfiled_summary", + "fieldname": "reconcile_summary", "fieldtype": "Attach", - "label": "Unfiled Summary", + "label": "Reconcile 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 + "fieldname": "return_type", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Return Type", + "read_only": 1, + "reqd": 1 } ], "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2024-05-28 19:49:55.513493", + "modified": "2024-07-14 18:35:34.743195", "modified_by": "Administrator", "module": "GST India", - "name": "GSTR-1 Log", + "name": "GST Return Log", "naming_rule": "Expression", "owner": "Administrator", "permissions": [ diff --git a/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py b/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py new file mode 100644 index 000000000..e9b5126b0 --- /dev/null +++ b/india_compliance/gst_india/doctype/gst_return_log/gst_return_log.py @@ -0,0 +1,275 @@ +# Copyright (c) 2024, Resilient Tech and contributors +# For license information, please see license.txt + +import gzip +from datetime import datetime + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import get_datetime, get_datetime_str, get_last_day, getdate + +from india_compliance.gst_india.doctype.gst_return_log.generate_gstr_1 import ( + GenerateGSTR1, +) +from india_compliance.gst_india.utils import is_production_api_enabled + + +class GSTReturnLog(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()) + + 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()) + 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, warn_for_missing_credentials=False): + if not settings: + settings = frappe.get_cached_doc("GST Settings") + + if not is_production_api_enabled(settings): + return False + + if not settings.compare_gstr_1_data: + return False + + if not settings.has_valid_credentials(self.gstin, "Returns"): + if warn_for_missing_credentials: + frappe.publish_realtime( + "show_message", + dict( + message=_( + "Credentials are missing for GSTIN {0} for service" + " Returns in GST Settings" + ).format(self.gstin), + title=_("Missing Credentials"), + ), + user=frappe.session.user, + ) + + return False + + return True + + 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"GSTR1-{info['ret_prd']}-{gstin}"] = info + + # existing logs + gstr1_logs = frappe._dict( + frappe.get_all( + "GST Return Log", + filters={"name": ("in", list(return_info.keys()))}, + fields=["name", "acknowledgement_number"], + as_list=1, + ) + ) + + # update gstr-1 filed upto + if frappe.db.exists("GSTIN", gstin): + gstin_doc = frappe.get_doc("GSTIN", gstin) + else: + 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 = { + "return_type": "GSTR1", + "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("GST Return Log", key, filing_details) + _update_gstr_1_filed_upto(filed_upto) + + # No updates if status is same + continue + + frappe.get_doc( + { + "doctype": "GST Return 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/gst_return_log/test_gst_return_log.py similarity index 78% rename from india_compliance/gst_india/doctype/gstr_1_log/test_gstr_1_log.py rename to india_compliance/gst_india/doctype/gst_return_log/test_gst_return_log.py index e8978437c..e7c6d222c 100644 --- a/india_compliance/gst_india/doctype/gstr_1_log/test_gstr_1_log.py +++ b/india_compliance/gst_india/doctype/gst_return_log/test_gst_return_log.py @@ -5,5 +5,5 @@ from frappe.tests.utils import FrappeTestCase -class TestGSTR1FiledLog(FrappeTestCase): +class TestGSTReturnLog(FrappeTestCase): pass 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 c7a1a4480..49b8f8a9d 100644 --- a/india_compliance/gst_india/doctype/gst_settings/gst_settings.json +++ b/india_compliance/gst_india/doctype/gst_settings/gst_settings.json @@ -38,8 +38,9 @@ "enable_e_invoice", "auto_generate_e_invoice", "generate_e_waybill_with_e_invoice", - "apply_e_invoice_only_for_selected_companies", + "fetch_e_invoice_details_from_gst_portal", "column_break_17", + "apply_e_invoice_only_for_selected_companies", "e_invoice_applicable_from", "e_invoice_applicable_companies", "gstr_1_section_break", @@ -314,7 +315,7 @@ { "default": "0", "depends_on": "eval:doc.enable_e_invoice", - "description": "e-Invoice will be generated for only selected companies.", + "description": "e-Invoice will be generated for only selected companies", "fieldname": "apply_e_invoice_only_for_selected_companies", "fieldtype": "Check", "label": "Apply e-Invoice for Selected Companies" @@ -368,14 +369,14 @@ }, { "default": "0", - "description": "Reverse Charge will automatically be applied if the Supplier is Unregistered and Reverse Charge value threshold is met. ", + "description": "Reverse Charge will automatically be applied if the Supplier is Unregistered and Reverse Charge value threshold is met", "fieldname": "enable_rcm_for_unregistered_supplier", "fieldtype": "Check", "label": "Enable Reverse Charge for Purchase from Unregistered Supplier" }, { "default": "1", - "description": "If checked, Vendor Reference No will be mandatory for transactions (except from unregistered suppliers) to facilitate purchase reconciliation and e-Waybill generation.", + "description": "If checked, Vendor Reference No will be mandatory for transactions (except from unregistered suppliers) to facilitate purchase reconciliation and e-Waybill generation", "fieldname": "require_supplier_invoice_no", "fieldtype": "Check", "label": "Require Vendor Document Reference for GST Purchases or Receipts" @@ -619,12 +620,20 @@ "fieldname": "enable_e_waybill_for_sc", "fieldtype": "Check", "label": "Enable e-Waybill Generation for Subcontracting" + }, + { + "default": "1", + "depends_on": "eval:doc.enable_e_invoice", + "description": "The system will attempt to get e-Invoice details from the GST Portal if it can't find them on the e-Invoice Portal", + "fieldname": "fetch_e_invoice_details_from_gst_portal", + "fieldtype": "Check", + "label": "Fetch e-Invoice details from GST Portal" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-07-12 10:25:49.243652", + "modified": "2024-07-25 19:16:25.036677", "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 9e25708fd..4f98e1235 100644 --- a/india_compliance/gst_india/doctype/gst_settings/gst_settings.py +++ b/india_compliance/gst_india/doctype/gst_settings/gst_settings.py @@ -499,7 +499,7 @@ def update_not_applicable_status(e_invoice_applicability_date=None, company=None 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 + Additionally, update the `is_not_latest_gstr1_data` field in the GST Return Log """ posting_date = getdate(posting_date) @@ -536,7 +536,9 @@ def restrict_gstr_1_transaction_for(posting_date, company_gstin, gst_settings=No 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.db.set_value( + "GST Return Log", f"GSTR1-{period}-{company_gstin}", "is_latest_data", 0 + ) frappe.publish_realtime( "is_not_latest_data", 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 index 0858bcb36..f19222983 100644 --- 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 @@ -38,8 +38,8 @@ const GSTR1_SubCategory = { 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)", + SUPECOM_52: "Liable to collect tax u/s 52(TCS)", + SUPECOM_9_5: "Liable to pay tax u/s 9(5)" }; const INVOICE_TYPE = { @@ -103,7 +103,6 @@ const GSTR1_DataField = { 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"); @@ -166,8 +165,8 @@ frappe.ui.form.on(DOCTYPE, { return; frappe.after_ajax(() => { - frm.doc.__onload = { data }; - frm.trigger("after_save"); + frm.doc.__gst_data = data ; + frm.trigger("load_gstr1_data"); }); }); }, @@ -195,27 +194,34 @@ frappe.ui.form.on(DOCTYPE, { refresh(frm) { // Primary Action frm.disable_save(); - frm.page.set_primary_action(__("Generate"), () => frm.save()); - }, + frm.page.set_primary_action(__("Generate"), () => + frm.call("generate_gstr1") + ); - before_save(frm) { - frm.doc.__unsaved = true; + // After indicator set in frappe refresh + if (frm.doc.__gst_data) frm.gstr1.render_indicator(); + else frm.page.clear_indicator(); }, - async after_save(frm) { - const data = frm.doc.__onload?.data; + load_gstr1_data(frm) { + const data = frm.doc.__gst_data; if (!frm._otp_requested && data == "otp_requested") { frm._otp_requested = true; india_compliance .authenticate_otp(frm.doc.company_gstin) - .then(() => frm.save()); + .then(() => frm.call("generate_gstr1")); + return; } frm._otp_requested = false; if (!data?.status) return; + + // Toggle HTML fields + frm.refresh(); + frm.gstr1.status = data.status; frm.gstr1.refresh_data(data); }, @@ -260,8 +266,6 @@ class GSTR1 { } 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(); @@ -430,7 +434,6 @@ class GSTR1 { this.$wrapper.find(`[data-fieldname="filed_tab"]`).html(tab_name); this.frm.page.set_indicator(this.status, color); - this.frm.refresh(); } // SETUP @@ -986,10 +989,10 @@ class TabManager { args[2]?.indent == 0 ? `${value}` : isDescriptionCell - ? ` + ? `

${value}

` - : value; + : value; return value; } @@ -1739,9 +1742,9 @@ class FiledTab extends GSTR1_TabManager { const { include_uploaded, delete_missing } = dialog ? dialog.get_values() : { - include_uploaded: true, - delete_missing: false, - }; + include_uploaded: true, + delete_missing: false, + }; const doc = me.instance.frm.doc; @@ -1807,7 +1810,7 @@ class FiledTab extends GSTR1_TabManager { render_empty_state(this.instance.frm); this.instance.frm .call("mark_as_filed") - .then(() => this.instance.frm.trigger("after_save")); + .then(() => this.instance.frm.trigger("load_gstr1_data")); } // COLUMNS @@ -1981,7 +1984,7 @@ class ReconcileTab extends FiledTab { }); } - get_creation_time_string() { } // pass + get_creation_time_string() {} // pass get_detail_view_column() { return [ @@ -2095,7 +2098,7 @@ function is_gstr1_api_enabled() { } function patch_set_indicator(frm) { - frm.toolbar.set_indicator = function () { }; + frm.toolbar.set_indicator = function () {}; } async function set_default_company_gstin(frm) { @@ -2178,7 +2181,7 @@ function render_empty_state(frm) { if ($(".gst-ledger-difference").length) { $(".gst-ledger-difference").remove(); } - frm.doc.__onload = null; + frm.doc.__gst_data = null; frm.refresh(); } 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 index 87f2d16b4..ee26f0955 100644 --- 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 @@ -56,18 +56,18 @@ "reqd": 1 }, { - "depends_on": "eval: doc.__onload?.data && Object.keys(doc.__onload.data).length", + "depends_on": "eval: doc.__gst_data && Object.keys(doc.__gst_data).length", "fieldname": "tabs_html", "fieldtype": "HTML" }, { - "depends_on": "eval: !doc.__onload?.data", + "depends_on": "eval: !doc.__gst_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", + "depends_on": "eval: doc.__gst_data && Object.keys(doc.__gst_data).length === 0", "fieldname": "tabs_no_data", "fieldtype": "HTML", "options": "\"No\n\t

{{ __(\"No data available for selected filters\") }}

" 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 index a6ce1b3fe..88253740f 100644 --- 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 @@ -5,7 +5,6 @@ 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 @@ -17,18 +16,13 @@ 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) + self.generate_gstr1(recompute_books=True) @frappe.whitelist() def sync_with_gstn(self, sync_for): - self.validate(sync_for=sync_for, recompute_books=True) + self.generate_gstr1(sync_for=sync_for, recompute_books=True) @frappe.whitelist() def mark_as_filed(self): @@ -44,22 +38,24 @@ def mark_as_filed(self): else: frappe.db.set_value( - "GSTR-1 Log", - f"{period}-{self.company_gstin}", + "GST Return Log", + f"GSTR1-{period}-{self.company_gstin}", "filing_status", return_status, ) - self.validate() - run_onload(self) + self.generate_gstr1() - def validate(self, sync_for=None, recompute_books=False): + @frappe.whitelist() + def generate_gstr1(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}"): + if log_name := frappe.db.exists( + "GST Return Log", f"GSTR1-{period}-{self.company_gstin}" + ): - gstr1_log = frappe.get_doc("GSTR-1 Log", log_name) + gstr1_log = frappe.get_doc("GST Return Log", log_name) message = None if gstr1_log.status == "In Progress": @@ -78,10 +74,11 @@ def validate(self, sync_for=None, recompute_books=False): return else: - gstr1_log = frappe.new_doc("GSTR-1 Log") + gstr1_log = frappe.new_doc("GST Return Log") gstr1_log.company = self.company gstr1_log.gstin = self.company_gstin gstr1_log.return_period = period + gstr1_log.return_type = "GSTR1" gstr1_log.insert() settings = frappe.get_cached_doc("GST Settings") @@ -97,9 +94,10 @@ def validate(self, sync_for=None, recompute_books=False): data = gstr1_log.load_data() if data: - self.data = data - self.data["status"] = gstr1_log.filing_status or "Not Filed" + data = data + data["status"] = gstr1_log.filing_status or "Not Filed" gstr1_log.update_status("Generated") + self.on_generate(data) return # request OTP @@ -107,17 +105,18 @@ def validate(self, sync_for=None, recompute_books=False): self.company_gstin ): request_otp(self.company_gstin) - self.data = "otp_requested" + data = "otp_requested" + self.on_generate(data) return self.gstr1_log = gstr1_log # generate gstr1 gstr1_log.update_status("In Progress") - frappe.enqueue(self.generate_gstr1, queue="short") + frappe.enqueue(self._generate_gstr1, queue="short") frappe.msgprint(_("GSTR-1 is being prepared"), alert=True) - def generate_gstr1(self): + def _generate_gstr1(self): """ Try to generate GSTR-1 data. Wrapper for generating GSTR-1 data """ @@ -144,11 +143,17 @@ def generate_gstr1(self): raise e - def on_generate(self, data, filters): + def on_generate(self, data, filters=None): """ Once data is generated, update the status and publish the data """ - self.gstr1_log.db_set({"generation_status": "Generated", "is_latest_data": 1}) + if not filters: + filters = self + + if getattr(self, "gstr1_log", None): + self.gstr1_log.db_set( + {"generation_status": "Generated", "is_latest_data": 1} + ) frappe.publish_realtime( "gstr1_data_prepared", 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 index 5e0a408aa..dfb245fbe 100644 --- 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 @@ -140,7 +140,7 @@ def generate(self, gstin, period): """ self.gstin = gstin self.period = period - gstr_1_log = frappe.get_doc("GSTR-1 Log", f"{period}-{gstin}") + gstr_1_log = frappe.get_doc("GST Return Log", f"GSTR1-{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] @@ -735,7 +735,9 @@ def __init__(self, company_gstin, month_or_quarter, year): self.year = year self.period = get_period(month_or_quarter, year) - gstr1_log = frappe.get_doc("GSTR-1 Log", f"{self.period}-{company_gstin}") + gstr1_log = frappe.get_doc( + "GST Return Log", f"GSTR1-{self.period}-{company_gstin}" + ) self.data = self.process_data(gstr1_log.load_data("books")["books"]) @@ -1149,7 +1151,9 @@ def __init__(self, company_gstin, month_or_quarter, year): self.year = year self.period = get_period(month_or_quarter, year) - gstr1_log = frappe.get_doc("GSTR-1 Log", f"{self.period}-{company_gstin}") + gstr1_log = frappe.get_doc( + "GST Return Log", f"GSTR1-{self.period}-{company_gstin}" + ) self.summary = gstr1_log.load_data("reconcile_summary")["reconcile_summary"] data = gstr1_log.load_data("reconcile")["reconcile"] @@ -2049,7 +2053,7 @@ def download_gstr_1_json( 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}") + gstr1_log = frappe.get_doc("GST Return Log", f"GSTR1-{period}-{company_gstin}") data = gstr1_log.get_json_for("books") data = data.update(data.pop("aggregate_data", {})) diff --git a/india_compliance/gst_india/overrides/subcontracting_transaction.py b/india_compliance/gst_india/overrides/subcontracting_transaction.py index 5ed0bc95c..ce83ebc8c 100644 --- a/india_compliance/gst_india/overrides/subcontracting_transaction.py +++ b/india_compliance/gst_india/overrides/subcontracting_transaction.py @@ -1,6 +1,11 @@ +from pypika import Order + import frappe from frappe import _, bold from frappe.contacts.doctype.address.address import get_address_display +from frappe.utils import flt +from erpnext.accounts.party import get_address_tax_category +from erpnext.stock.get_item_details import get_item_tax_template from india_compliance.gst_india.overrides.sales_invoice import ( update_dashboard_with_gst_logs, @@ -9,6 +14,7 @@ GSTAccounts, get_place_of_supply, ignore_gst_validations, + is_inter_state_supply, set_gst_tax_type, validate_gst_category, validate_gst_transporter_id, @@ -16,7 +22,7 @@ validate_mandatory_fields, validate_place_of_supply, ) -from india_compliance.gst_india.utils import is_api_enabled +from india_compliance.gst_india.utils import get_gst_accounts_by_type, is_api_enabled from india_compliance.gst_india.utils.e_waybill import get_e_waybill_info from india_compliance.gst_india.utils.taxes_controller import ( CustomTaxController, @@ -28,6 +34,87 @@ SUBCONTRACTING_ORDER_RECEIPT_FIELD_MAP = {"total_taxable_value": "total"} +def after_mapping_subcontracting_order(doc, method, source_doc): + if source_doc.doctype != "Purchase Order": + return + + doc.taxes_and_charges = "" + doc.taxes = [] + + if ignore_gst_validations(doc): + return + + set_taxes(doc) + + if not doc.items: + return + + tax_category = source_doc.tax_category + + if not tax_category: + tax_category = get_address_tax_category( + frappe.db.get_value("Supplier", source_doc.supplier, "tax_category"), + source_doc.supplier_address, + ) + + args = {"company": doc.company, "tax_category": tax_category} + + for item in doc.items: + out = {} + item_doc = frappe.get_cached_doc("Item", item.item_code) + get_item_tax_template(args, item_doc, out) + item.item_tax_template = out.get("item_tax_template") + + +def after_mapping_stock_entry(doc, method, source_doc): + if source_doc.doctype == "Subcontracting Order": + return + + doc.taxes_and_charges = "" + doc.taxes = [] + + +def set_taxes(doc): + accounts = get_gst_accounts_by_type(doc.company, "Output", throw=False) + if not accounts: + return + + sales_tax_template = frappe.qb.DocType("Sales Taxes and Charges Template") + sales_tax_template_row = frappe.qb.DocType("Sales Taxes and Charges") + + rate = ( + frappe.qb.from_(sales_tax_template_row) + .left_join(sales_tax_template) + .on(sales_tax_template.name == sales_tax_template_row.parent) + .select(sales_tax_template_row.rate) + .where(sales_tax_template_row.parenttype == "Sales Taxes and Charges Template") + .where(sales_tax_template_row.account_head == accounts.get("igst_account")) + .where(sales_tax_template.disabled == 0) + .orderby(sales_tax_template.is_default, order=Order.desc) + .orderby(sales_tax_template.modified, order=Order.desc) + .limit(1) + .run(pluck=True) + )[0] or 0 + + tax_types = ("igst",) + if not is_inter_state_supply(doc): + tax_types = ("cgst", "sgst") + rate = flt(rate / 2) + + for tax_type in tax_types: + account = accounts.get(tax_type + "_account") + doc.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": account, + "rate": rate, + "gst_tax_type": tax_type, + "description": account, + }, + ) + + def get_dashboard_data(data): doctype = ( "Subcontracting Receipt" diff --git a/india_compliance/gst_india/overrides/transaction.py b/india_compliance/gst_india/overrides/transaction.py index 2dff79c2f..7e920e2f1 100644 --- a/india_compliance/gst_india/overrides/transaction.py +++ b/india_compliance/gst_india/overrides/transaction.py @@ -1349,8 +1349,7 @@ def get_item_tax_detail(self, item): return response def set_tax_amount_precisions(self, doctype): - doc_meta = self.doc.meta if self.doc.meta else frappe.get_meta(doctype) - item_doctype = doc_meta.get_field("items").options + item_doctype = frappe.get_meta(doctype).get_field("items").options meta = frappe.get_meta(item_doctype) diff --git a/india_compliance/gst_india/report/gst_sales_register_beta/gst_sales_register_beta.js b/india_compliance/gst_india/report/gst_sales_register_beta/gst_sales_register_beta.js index 7ea7c6c1b..ca1e7582c 100644 --- a/india_compliance/gst_india/report/gst_sales_register_beta/gst_sales_register_beta.js +++ b/india_compliance/gst_india/report/gst_sales_register_beta/gst_sales_register_beta.js @@ -16,6 +16,10 @@ const INVOICE_TYPE = { "Credit/Debit Notes (Unregistered)": ["CDNUR"], }; +if (gst_settings.enable_sales_through_ecommerce_operators) { + INVOICE_TYPE["Supplies made through E-commerce Operators"] = ["Liable to pay tax u/s 9(5)", "Liable to collect tax u/s 52(TCS)"] +} + frappe.query_reports["GST Sales Register Beta"] = { onload: set_sub_category_options, @@ -30,6 +34,7 @@ frappe.query_reports["GST Sales Register Beta"] = { report.set_filter_value({ company_gstin: "", }); + report.refresh(); }, get_query: function () { return { @@ -70,11 +75,11 @@ frappe.query_reports["GST Sales Register Beta"] = { fieldtype: "Autocomplete", fieldname: "invoice_category", label: __("Invoice Category"), - options: - "B2B, SEZ, DE\nB2C (Large)\nExports\nB2C (Others)\nNil-Rated, Exempted, Non-GST\nCredit/Debit Notes (Registered)\nCredit/Debit Notes (Unregistered)", - on_change(report) { + options: Object.keys(INVOICE_TYPE), + on_change: report => { report.set_filter_value("invoice_sub_category", ""); set_sub_category_options(report); + report.refresh(); }, depends_on: 'eval:doc.summary_by=="Summary by HSN" || doc.summary_by=="Summary by Item"', @@ -131,7 +136,7 @@ custom_report_column_total = function (...args) { if (column_field === "description") return; const total = this.datamanager.data.reduce((acc, row) => { - if (row.indent !== 1) acc += row[column_field] || 0; + if (row.indent !== 1 && row.description !== "Supplies made through E-commerce Operators") acc += row[column_field] || 0; return acc; }, 0); 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 dc61f78cc..c424dca2b 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,6 +5,7 @@ from frappe import _ from frappe.utils import getdate +from india_compliance.gst_india.utils.gstr_1 import GSTR1_Category from india_compliance.gst_india.utils.gstr_1.gstr_1_data import GSTR1Invoices @@ -199,6 +200,16 @@ def get_columns(filters): ) if filters.summary_by == "Summary by Item": + if gst_settings.enable_sales_through_ecommerce_operators: + columns.append( + { + "label": _("E-Commerce GSTIN"), + "fieldname": "ecommerce_gstin", + "fieldtype": "Data", + "width": 180, + } + ) + columns.append( { "label": _("Item Code"), @@ -300,7 +311,11 @@ def get_columns(filters): }, ] ) - if not filters.invoice_category: + + if ( + not filters.invoice_category + or filters.invoice_category == GSTR1_Category.SUPECOM.value + ): columns.append( { "label": _("Invoice Category"), @@ -309,7 +324,11 @@ def get_columns(filters): "fieldtype": "Data", } ) - if not filters.invoice_sub_category: + + if ( + not filters.invoice_sub_category + or filters.invoice_category == GSTR1_Category.SUPECOM.value + ): columns.append( { "label": _("Invoice Sub Category"), @@ -319,4 +338,17 @@ def get_columns(filters): } ) + if ( + filters.summary_by == "Summary by Item" + and gst_settings.enable_sales_through_ecommerce_operators + ): + columns.append( + { + "label": _("E-Commerce Supply Type"), + "fieldname": "ecommerce_supply_type", + "fieldtype": "Data", + "width": 250, + } + ) + return columns 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 b01d9d881..836bb9856 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 @@ -493,7 +493,7 @@ "total_cess_amount": 0.0, }, { - "description": "Overlaping Invoices in Nil-Rated/Exempt/Non-GST", + "description": "Overlaping Invoices in Nil-Rated/Exempt/Non-GST and E-commerce Sales", "no_of_records": -5, }, ] diff --git a/india_compliance/gst_india/setup/__init__.py b/india_compliance/gst_india/setup/__init__.py index 4ba25ba33..7c941bb49 100644 --- a/india_compliance/gst_india/setup/__init__.py +++ b/india_compliance/gst_india/setup/__init__.py @@ -204,6 +204,7 @@ def set_default_gst_settings(): "auto_generate_e_invoice": 1, "generate_e_waybill_with_e_invoice": 1, "e_invoice_applicable_from": nowdate(), + "fetch_e_invoice_details_from_gst_portal": 1, "autofill_party_info": 1, "archive_party_info_days": 7, "validate_gstin_status": 1, diff --git a/india_compliance/gst_india/utils/e_invoice.py b/india_compliance/gst_india/utils/e_invoice.py index c390f7ab9..d66eff3b3 100644 --- a/india_compliance/gst_india/utils/e_invoice.py +++ b/india_compliance/gst_india/utils/e_invoice.py @@ -7,6 +7,7 @@ from frappe.utils import ( add_to_date, cstr, + flt, format_date, get_datetime, getdate, @@ -15,6 +16,9 @@ from india_compliance.exceptions import GSPServerError from india_compliance.gst_india.api_classes.e_invoice import EInvoiceAPI +from india_compliance.gst_india.api_classes.taxpayer_e_invoice import ( + EInvoiceAPI as TaxpayerEInvoiceAPI, +) from india_compliance.gst_india.constants import ( CURRENCY_CODES, EXPORT_TYPES, @@ -132,30 +136,15 @@ def generate_e_invoice(docname, throw=True, force=False): # Handle Duplicate IRN if result.InfCd == "DUPIRN": - response = api.get_e_invoice_by_irn(result.Desc.Irn) - - if signed_data := response.SignedInvoice: - invoice_data = json.loads( - jwt.decode(signed_data, options={"verify_signature": False})["data"] - ) - - previous_invoice_amount = invoice_data.get("ValDtls").get("TotInvVal") - current_invoice_amount = data.get("ValDtls").get("TotInvVal") - - if previous_invoice_amount != current_invoice_amount: - frappe.throw( - _( - "e-Invoice is already available against Invoice {0} with a Grand Total of Rs.{1}" - " Duplicate IRN requests are not considered by e-Invoice Portal." - ).format( - frappe.bold(invoice_data.get("DocDtls").get("No")), - frappe.bold(previous_invoice_amount), - ) - ) - - # Handle error 2283: - # IRN details cannot be provided as it is generated more than 2 days ago - result = result.Desc if response.error_code == "2283" else response + current_gstin = data.get("BuyerDtls").get("Gstin") + current_invoice_amount = data.get("ValDtls").get("TotInvVal") + + return handle_duplicate_irn_error( + irn_data=result.Desc, + current_gstin=current_gstin, + current_invoice_amount=current_invoice_amount, + doc=doc, + ) # Handle Invalid GSTIN Error if result.error_code in ("3028", "3029"): @@ -193,6 +182,108 @@ def generate_e_invoice(docname, throw=True, force=False): doc.db_set({"einvoice_status": "Failed"}) raise e + return log_and_process_e_invoice_generation(doc, result, api.sandbox_mode) + + +@frappe.whitelist() +def handle_duplicate_irn_error( + irn_data, + current_gstin, + current_invoice_amount, + doc=None, + docname=None, +): + """ + Handle Duplicate IRN errors by fetching the IRN details and comparing with the current invoice. + + Steps: + 1. Fetch IRN details using the IRN number using e-Invoice API. + 2. If the IRN details cannot be fetched, fetch the IRN details from the GST Portal. + 3. Compare the buyer GSTIN and invoice amount with the current invoice and throw an error if they don't match. + """ + + if isinstance(irn_data, str): + irn_data = json.loads(irn_data, object_hook=frappe._dict) + current_invoice_amount = flt(current_invoice_amount) + + doc = doc or load_doc("Sales Invoice", docname, "submit") + api = EInvoiceAPI(doc) + response = api.get_e_invoice_by_irn(irn_data.Irn) + + # Handle error 2283: + # IRN details cannot be provided as it is generated more than 2 days ago + if ( + response.error_code == "2283" + and api.settings.fetch_e_invoice_details_from_gst_portal + ): + response = TaxpayerEInvoiceAPI(doc).get_irn_details(irn_data.Irn) + + if response.error_type == "otp_requested": + response.update( + { + "irn_data": irn_data, + "current_gstin": current_gstin, + "current_invoice_amount": current_invoice_amount, + "docname": doc.name, + } + ) + + return response + + response = frappe._dict(response.data or response.error) + + if signed_data := response.SignedInvoice: + verify_e_invoice_details(current_gstin, current_invoice_amount, signed_data) + + if response.error_code: + response = irn_data + + return log_and_process_e_invoice_generation(doc, response, api.sandbox_mode) + + +def verify_e_invoice_details(current_gstin, current_invoice_amount, signed_data): + invoice_data = json.loads( + jwt.decode(signed_data, options={"verify_signature": False})["data"] + ) + + previous_gstin = invoice_data.get("BuyerDtls").get("Gstin") + previous_invoice_amount = invoice_data.get("ValDtls").get("TotInvVal") + + error_message = "" + if previous_gstin != current_gstin: + error_message += _("
  • Customer GSTIN (Previous: {0}).
  • ").format( + frappe.bold(previous_gstin) + ) + + if previous_invoice_amount != current_invoice_amount: + previous_invoice_amount_formatted = frappe.format_value( + previous_invoice_amount, currency=frappe.db.get_default("currency") + ) + + error_message += _("
  • Invoice amount (Previous: {0}).
  • ").format( + frappe.bold(previous_invoice_amount_formatted) + ) + + if error_message: + frappe.throw( + _( + "An e-Invoice already exists for Invoice No {0}, but with different details compared to the current Invoice:
    {1}" + "Hence, the IRN number is not updated against current Invoice." + "

    Corrective Steps:

    " + "
  • Generate a new Invoice for the same transaction.
  • " + "
  • Try cancelling e-Invoice from e-Invoice portal if possible. Alternatively, clear/update e-Invoice as posted automatically in GSTR-1.
  • " + ).format( + frappe.bold(invoice_data.get("DocDtls").get("No")), + error_message, + ), + ) + + +def log_and_process_e_invoice_generation(doc, result, sandbox_mode=False): + """ + Load and process the e-Invoice generation result. + """ + doc.db_set( { "irn": result.Irn, @@ -213,13 +304,13 @@ def generate_e_invoice(docname, throw=True, force=False): doc, { "irn": doc.irn, - "sales_invoice": docname, + "sales_invoice": doc.name, "acknowledgement_number": result.AckNo, "acknowledged_on": parse_datetime(result.AckDt), "signed_invoice": result.SignedInvoice, "signed_qr_code": result.SignedQRCode, "invoice_data": invoice_data, - "is_generated_in_sandbox_mode": api.sandbox_mode, + "is_generated_in_sandbox_mode": sandbox_mode, }, ) diff --git a/india_compliance/gst_india/utils/gstin_info.py b/india_compliance/gst_india/utils/gstin_info.py index d70886887..06c8c2bf5 100644 --- a/india_compliance/gst_india/utils/gstin_info.py +++ b/india_compliance/gst_india/utils/gstin_info.py @@ -11,7 +11,7 @@ from india_compliance.gst_india.api_classes.e_invoice import EInvoiceAPI from india_compliance.gst_india.api_classes.e_waybill import EWaybillAPI from india_compliance.gst_india.api_classes.public import PublicAPI -from india_compliance.gst_india.doctype.gstr_1_log.gstr_1_log import ( +from india_compliance.gst_india.doctype.gst_return_log.gst_return_log import ( process_gstr_1_returns_info, ) from india_compliance.gst_india.utils import parse_datetime, titlecase, validate_gstin diff --git a/india_compliance/gst_india/utils/gstr_1/__init__.py b/india_compliance/gst_india/utils/gstr_1/__init__.py index 0a9cb2f49..66718a0a8 100644 --- a/india_compliance/gst_india/utils/gstr_1/__init__.py +++ b/india_compliance/gst_india/utils/gstr_1/__init__.py @@ -49,8 +49,8 @@ class GSTR1_SubCategory(Enum): 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)" + SUPECOM_52 = "Liable to collect tax u/s 52(TCS)" + SUPECOM_9_5 = "Liable to pay tax u/s 9(5)" class SUPECOM(Enum): diff --git a/india_compliance/gst_india/utils/gstr_1/gstr_1_data.py b/india_compliance/gst_india/utils/gstr_1/gstr_1_data.py index 17f7dbd9c..a421e5234 100644 --- a/india_compliance/gst_india/utils/gstr_1/gstr_1_data.py +++ b/india_compliance/gst_india/utils/gstr_1/gstr_1_data.py @@ -1,5 +1,6 @@ # Copyright (c) 2024, Resilient Tech and contributors # For license information, please see license.txt +from itertools import combinations from pypika import Order @@ -46,6 +47,10 @@ "category": "is_cdnur_invoice", "sub_category": "set_for_cdnur", }, + GSTR1_Category.SUPECOM.value: { + "category": "is_ecommerce_sales_invoice", + "sub_category": "set_for_ecommerce_supply_type", + }, } @@ -80,6 +85,7 @@ def get_base_query(self): self.si.posting_date, IfNull(self.si.place_of_supply, "").as_("place_of_supply"), self.si.is_reverse_charge, + IfNull(self.si.ecommerce_gstin, "").as_("ecommerce_gstin"), self.si.is_export_with_gst, self.si.is_return, self.si.is_debit_note, @@ -289,6 +295,9 @@ def is_cdnur_invoice(self, invoice): and (self.is_export(invoice) or self.is_b2cl_cn_dn(invoice)) ) + def is_ecommerce_sales_invoice(self, invoice): + return bool(invoice.ecommerce_gstin) + class GSTR1Subcategory(GSTR1CategoryConditions): @@ -340,6 +349,13 @@ def set_for_cdnur(self, invoice): invoice.invoice_type = "B2CL" return + def set_for_ecommerce_supply_type(self, invoice): + if invoice.is_reverse_charge: + invoice.ecommerce_supply_type = GSTR1_SubCategory.SUPECOM_9_5.value + return + + invoice.ecommerce_supply_type = GSTR1_SubCategory.SUPECOM_52.value + def _set_invoice_type_for_b2b_and_cdnr(self, invoice): if invoice.gst_category == "Deemed Export": invoice.invoice_type = GSTR1_B2B_InvoiceType.DE.value @@ -396,9 +412,12 @@ def process_invoices(self, invoices): invoice["stock_uom"] = gst_uom def assign_categories(self, invoice): + if not invoice.invoice_sub_category: + self.set_invoice_category(invoice) + self.set_invoice_sub_category_and_type(invoice) - self.set_invoice_category(invoice) - self.set_invoice_sub_category_and_type(invoice) + if invoice.ecommerce_gstin and not invoice.ecommerce_supply_type: + self.set_for_ecommerce_supply_type(invoice) def set_invoice_category(self, invoice): for category, functions in CATEGORY_CONDITIONS.items(): @@ -467,19 +486,29 @@ def get_filtered_invoices( elif invoice_sub_category == invoice.invoice_sub_category: filtered_invoices.append(invoice) + elif invoice_sub_category == invoice.ecommerce_supply_type: + filtered_invoices.append(invoice) + + self.process_invoices(invoices) + return filtered_invoices def get_overview(self): final_summary = [] sub_category_summary = self.get_sub_category_summary() - IGNORED_CATEGORIES = ( + IGNORED_CATEGORIES = { GSTR1_Category.AT, GSTR1_Category.TXP, GSTR1_Category.DOC_ISSUE, GSTR1_Category.HSN, - GSTR1_Category.SUPECOM, + } + + is_ecommerce_sales_enabled = frappe.get_cached_value( + "GST Settings", None, "enable_sales_through_ecommerce_operators" ) + if not is_ecommerce_sales_enabled: + IGNORED_CATEGORIES.add(GSTR1_Category.SUPECOM) for category, sub_categories in CATEGORY_SUB_CATEGORY_MAPPING.items(): if category in IGNORED_CATEGORIES: @@ -522,16 +551,20 @@ def get_sub_category_summary(self): **self.AMOUNT_FIELDS, } - for row in invoices: - summary_row = summary[ - row.get("invoice_sub_category", row["invoice_category"]) - ] + def _update_summary_row(row, sub_category_field="invoice_sub_category"): + summary_row = summary[row.get(sub_category_field, row["invoice_category"])] for key in self.AMOUNT_FIELDS: summary_row[key] += row[key] summary_row["unique_records"].add(row.invoice_no) + for row in invoices: + _update_summary_row(row) + + if row.ecommerce_gstin: + _update_summary_row(row, "ecommerce_supply_type") + for summary_row in summary.values(): summary_row["no_of_records"] = len(summary_row["unique_records"]) @@ -539,24 +572,37 @@ def get_sub_category_summary(self): def update_overlaping_invoice_summary(self, sub_category_summary, final_summary): nil_exempt = GSTR1_SubCategory.NIL_EXEMPT.value + supecom_52 = GSTR1_SubCategory.SUPECOM_52.value + supecom_9_5 = GSTR1_SubCategory.SUPECOM_9_5.value # Get Unique Taxable Invoices unique_invoices = set() for category, row in sub_category_summary.items(): - if category == nil_exempt: + if category in (nil_exempt, supecom_52, supecom_9_5): continue unique_invoices.update(row["unique_records"]) # Get Overlaping Invoices - category_invoices = sub_category_summary[nil_exempt]["unique_records"] - overlaping_invoices = category_invoices.intersection(unique_invoices) + invoice_sets = [ + sub_category_summary[nil_exempt]["unique_records"], + { + *sub_category_summary[supecom_52]["unique_records"], + *sub_category_summary[supecom_9_5]["unique_records"], + }, + unique_invoices, + ] + + overlaping_invoices = [] + + for set1, set2 in combinations(invoice_sets, 2): + overlaping_invoices.extend(set1.intersection(set2)) # Update Summary if overlaping_invoices: final_summary.append( { - "description": "Overlaping Invoices in Nil-Rated/Exempt/Non-GST", + "description": "Overlaping Invoices in Nil-Rated/Exempt/Non-GST and E-commerce Sales", "no_of_records": -len(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 index 1b9d2cc45..eb39ba59f 100644 --- a/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py +++ b/india_compliance/gst_india/utils/gstr_1/gstr_1_download.py @@ -111,7 +111,7 @@ def save_gstr_1(gstin, return_period, json_data, return_type): mapped_data = convert_to_internal_data_format(json_data) - gstr1_log = frappe.get_doc("GSTR-1 Log", f"{return_period}-{gstin}") + gstr1_log = frappe.get_doc("GST Return Log", f"GSTR1-{return_period}-{gstin}") gstr1_log.update_json_for(data_field, mapped_data, overwrite=False) 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 index a602bcbb0..19697220d 100644 --- 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 @@ -2,7 +2,9 @@ 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.doctype.gst_return_log.generate_gstr_1 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, diff --git a/india_compliance/gst_india/utils/test_e_invoice.py b/india_compliance/gst_india/utils/test_e_invoice.py index 09e83d88e..030a1a9ea 100644 --- a/india_compliance/gst_india/utils/test_e_invoice.py +++ b/india_compliance/gst_india/utils/test_e_invoice.py @@ -769,7 +769,7 @@ def test_e_invoice_for_duplicate_irn(self): # Assert if Invoice amount has changed self.assertRaisesRegex( frappe.ValidationError, - re.compile(r"^(e-Invoice is already available against Invoice.*)$"), + re.compile(r"^(An e-Invoice already exists for Invoice.*)$"), generate_e_invoice, si.name, ) 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 0bf634fe1..12e554b44 100644 --- a/india_compliance/gst_india/workspace/gst_india/gst_india.json +++ b/india_compliance/gst_india/workspace/gst_india/gst_india.json @@ -132,9 +132,9 @@ { "hidden": 0, "is_query_report": 0, - "label": "GSTR-1 Log", + "label": "GST Return Log", "link_count": 0, - "link_to": "GSTR-1 Log", + "link_to": "GST Return Log", "link_type": "DocType", "onboard": 0, "type": "Link" @@ -229,7 +229,7 @@ "type": "Link" } ], - "modified": "2024-06-08 16:54:55.015726", + "modified": "2024-07-14 18:01:43.058590", "modified_by": "Administrator", "module": "GST India", "name": "GST India", diff --git a/india_compliance/hooks.py b/india_compliance/hooks.py index e2080ab03..8301a6df5 100644 --- a/india_compliance/hooks.py +++ b/india_compliance/hooks.py @@ -234,9 +234,11 @@ "Stock Entry": { "onload": "india_compliance.gst_india.overrides.subcontracting_transaction.onload", "validate": "india_compliance.gst_india.overrides.subcontracting_transaction.validate", + "after_mapping": "india_compliance.gst_india.overrides.subcontracting_transaction.after_mapping_stock_entry", }, "Subcontracting Order": { "validate": "india_compliance.gst_india.overrides.subcontracting_transaction.validate", + "after_mapping": "india_compliance.gst_india.overrides.subcontracting_transaction.after_mapping_subcontracting_order", }, "Subcontracting Receipt": { "onload": "india_compliance.gst_india.overrides.subcontracting_transaction.onload", diff --git a/india_compliance/patches.txt b/india_compliance/patches.txt index 9663e8f8c..3492215e5 100644 --- a/india_compliance/patches.txt +++ b/india_compliance/patches.txt @@ -4,7 +4,7 @@ india_compliance.patches.v15.remove_duplicate_web_template [post_model_sync] india_compliance.patches.v14.set_default_for_overridden_accounts_setting -execute:from india_compliance.gst_india.setup import create_custom_fields; create_custom_fields() #52 +execute:from india_compliance.gst_india.setup import create_custom_fields; create_custom_fields() #53 execute:from india_compliance.gst_india.setup import create_property_setters; create_property_setters() #8 execute:from india_compliance.income_tax_india.setup import create_custom_fields; create_custom_fields() #1 india_compliance.patches.post_install.remove_old_fields #1 @@ -59,3 +59,4 @@ india_compliance.patches.v14.unset_inward_supply_link_for_cancelled_purchase india_compliance.patches.v14.delete_not_generated_gstr_import_log india_compliance.patches.v14.enable_sales_through_ecommerce_operator execute:from india_compliance.gst_india.setup import set_default_print_settings; set_default_print_settings() +india_compliance.patches.v15.migrate_gstr1_log_to_returns_log diff --git a/india_compliance/patches/v15/migrate_gstr1_log_to_returns_log.py b/india_compliance/patches/v15/migrate_gstr1_log_to_returns_log.py new file mode 100644 index 000000000..3745cafc5 --- /dev/null +++ b/india_compliance/patches/v15/migrate_gstr1_log_to_returns_log.py @@ -0,0 +1,21 @@ +import frappe + + +def execute(): + if not frappe.db.table_exists("GSTR-1 Log"): + return + + old = frappe.qb.DocType("GSTR-1 Log") + old_docs = frappe.qb.from_(old).select("*").run(as_dict=True) + + for doc in old_docs: + new_doc = frappe.get_doc( + {**doc, "doctype": "GST Return Log", "name": None, "return_type": "GSTR1"} + ) + new_doc.insert(ignore_if_duplicate=True) + + # Drop the old table + frappe.db.delete("GSTR-1 Log") + + # Clear all fields saved in GSTR-1 Beta + frappe.db.delete("Singles", {"doctype": "GSTR-1 Beta"})