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}
{{ __(\"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": "\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 += _("