diff --git a/india_compliance/gst_india/api_classes/returns.py b/india_compliance/gst_india/api_classes/returns.py
index 5f64909e23..c575b838aa 100644
--- a/india_compliance/gst_india/api_classes/returns.py
+++ b/india_compliance/gst_india/api_classes/returns.py
@@ -110,7 +110,12 @@ def autheticate_with_otp(self, otp=None):
endpoint="authenticate",
)
- def refresh_auth_token(self, auth_token):
+ def refresh_auth_token(self):
+ auth_token = self.get_auth_token()
+
+ if not auth_token:
+ return
+
return super().post(
json={
"action": "REFRESHTOKEN",
@@ -151,6 +156,9 @@ def decrypt_response(self, response):
values,
)
+ # cache of parent doctype GST Settings is not cleared by default so clear it manually
+ frappe.clear_document_cache("GST Settings")
+
return response
def encrypt_request(self, json):
@@ -158,8 +166,12 @@ def encrypt_request(self, json):
return
if json.get("app_key"):
- json["app_key"] = encrypt_using_public_key(
- self.app_key, self.get_public_certificate()
+ json["app_key"] = (
+ aes_encrypt_data(self.app_key, self.session_key)
+ if json.get("action") == "REFRESHTOKEN"
+ else encrypt_using_public_key(
+ self.app_key, self.get_public_certificate()
+ )
)
if json.get("otp"):
@@ -179,6 +191,18 @@ def get_public_certificate(self):
return certificate.encode()
+ def get_auth_token(self):
+ if not self.auth_token:
+ return None
+
+ if not self.session_expiry:
+ return None
+
+ if self.session_expiry <= now_datetime():
+ return None
+
+ return self.auth_token
+
class ReturnsAPI(ReturnsAuthenticate):
API_NAME = "GST Returns"
@@ -333,18 +357,6 @@ def generate_app_key(self):
return app_key
- def get_auth_token(self):
- if not self.auth_token:
- return None
-
- if not self.session_expiry:
- return None
-
- if self.session_expiry <= now_datetime():
- return None
-
- return self.auth_token
-
def download_files(self, return_period, token, otp=None):
response = self.get(
"FILEDET",
diff --git a/india_compliance/gst_india/data/test_purchase_reconciliation_tool.json b/india_compliance/gst_india/data/test_purchase_reconciliation_tool.json
new file mode 100644
index 0000000000..79bcf93555
--- /dev/null
+++ b/india_compliance/gst_india/data/test_purchase_reconciliation_tool.json
@@ -0,0 +1,193 @@
+{
+ "TEST_CASES": {
+ "TEST_EXACT_MATCH": [
+ {
+ "PURCHASE_INVOICE": {
+ "bill_no": "BILL-23-00001"
+ },
+ "INWARD_SUPPLY": {
+ "bill_no": "BILL-23-00001"
+ },
+ "RECONCILED_DATA": {
+ "action": "No Action",
+ "bill_date": "2023-12-11",
+ "bill_no": "BILL-23-00001",
+ "classification": "B2B",
+ "differences": "",
+ "inward_supply_company_gstin": "24AAQCA8719H1ZC",
+ "inward_supply_name": "",
+ "match_status": "Exact Match",
+ "purchase_company_gstin": "24AAQCA8719H1ZC",
+ "purchase_doctype": "Purchase Invoice",
+ "purchase_invoice_name": "",
+ "supplier_gstin": "24AABCR6898M1ZN",
+ "supplier_name": "_Test Registered Supplier",
+ "tax_difference": 0.0,
+ "taxable_value_difference": 0.0
+ }
+ },
+ {
+ "BILL_OF_ENTRY": {
+ "bill_no": "BILL-23-00011"
+ },
+ "INWARD_SUPPLY": {
+ "supplier_name": "_Test Foreign Supplier",
+ "bill_no": "BILL-23-00011",
+ "bill_date": "2023-12-11",
+ "classification": "IMPG",
+ "doc_type": "Bill of Entry",
+ "supply_type": "",
+ "place_of_supply": "24-Gujarat",
+ "items": [
+ {
+ "taxable_value": 10000,
+ "rate": 18,
+ "igst": 1800
+ }
+ ],
+ "document_value": 11800,
+ "supplier_gstin": null,
+ "itc_availability": "Yes",
+ "return_period_2b": "122023",
+ "gen_date_2b": "2023-12-11"
+ },
+ "RECONCILED_DATA": {
+ "action": "No Action",
+ "bill_date": "2023-12-11",
+ "bill_no": "BILL-23-00011",
+ "classification": "IMPG",
+ "differences": "",
+ "inward_supply_company_gstin": "24AAQCA8719H1ZC",
+ "inward_supply_name": "",
+ "match_status": "Exact Match",
+ "purchase_company_gstin": "24AAQCA8719H1ZC",
+ "purchase_doctype": "Bill of Entry",
+ "purchase_invoice_name": "",
+ "supplier_gstin": "_Test Foreign Supplier",
+ "supplier_name": "_Test Foreign Supplier",
+ "tax_difference": 0.0,
+ "taxable_value_difference": 0.0
+ }
+ },
+ {
+ "PURCHASE_INVOICE": {
+ "bill_no": "BILL-23-00010",
+ "company_gstin": "29AABCR1718E1ZL"
+ },
+ "INWARD_SUPPLY": {
+ "bill_no": "BILL-23-00010",
+ "company_gstin": "29AABCR1718E1ZL"
+ },
+ "RECONCILED_DATA": {
+ "action": "No Action",
+ "bill_date": "2023-12-11",
+ "bill_no": "BILL-23-00010",
+ "classification": "B2B",
+ "differences": "",
+ "inward_supply_company_gstin": "29AABCR1718E1ZL",
+ "inward_supply_name": "",
+ "match_status": "Exact Match",
+ "purchase_company_gstin": "29AABCR1718E1ZL",
+ "purchase_doctype": "Purchase Invoice",
+ "purchase_invoice_name": "",
+ "supplier_gstin": "24AABCR6898M1ZN",
+ "supplier_name": "_Test Registered Supplier",
+ "tax_difference": 0.0,
+ "taxable_value_difference": 0.0
+ }
+ }
+ ],
+ "TEST_DIFFERENT_COMPANY_GSTIN_MISMATCH": [
+ {
+ "PURCHASE_INVOICE": {
+ "bill_no": "BILL-23-00021",
+ "company_gstin": "24AAQCA8719H1ZC"
+ },
+ "INWARD_SUPPLY": {
+ "bill_no": "BILL-23-00021",
+ "company_gstin": "29AABCR1718E1ZL"
+ },
+ "RECONCILED_DATA": {
+ "action": "No Action",
+ "bill_date": "2023-12-11",
+ "bill_no": "BILL-23-00021",
+ "classification": "B2B",
+ "differences": "COMPANY_GSTIN",
+ "inward_supply_company_gstin": "29AABCR1718E1ZL",
+ "inward_supply_name": "",
+ "match_status": "Mismatch",
+ "purchase_company_gstin": "24AAQCA8719H1ZC",
+ "purchase_doctype": "Purchase Invoice",
+ "purchase_invoice_name": "",
+ "supplier_gstin": "24AABCR6898M1ZN",
+ "supplier_name": "_Test Registered Supplier",
+ "tax_difference": 0.0,
+ "taxable_value_difference": 0.0
+ }
+ }
+ ],
+ "TEST_ROUNDING_DIFFERENCE_SUGGESTED_MATCH": [
+ {
+ "PURCHASE_INVOICE": {
+ "bill_no": "BILL-23-00030"
+ },
+ "INWARD_SUPPLY": {
+ "bill_no": "BILL-23-00030",
+ "items": [
+ {
+ "taxable_value": 10000.50,
+ "rate": 18,
+ "sgst": 900.045,
+ "cgst": 900.045
+ }
+ ],
+ "document_value": 11800.09
+ },
+ "RECONCILED_DATA": {
+ "action": "No Action",
+ "bill_date": "2023-12-11",
+ "bill_no": "BILL-23-00030",
+ "classification": "B2B",
+ "differences": "Rounding Difference, TAXABLE_VALUE, CGST, SGST",
+ "inward_supply_company_gstin": "24AAQCA8719H1ZC",
+ "inward_supply_name": "",
+ "match_status": "Suggested Match",
+ "purchase_company_gstin": "24AAQCA8719H1ZC",
+ "purchase_doctype": "Purchase Invoice",
+ "purchase_invoice_name": "",
+ "supplier_gstin": "24AABCR6898M1ZN",
+ "supplier_name": "_Test Registered Supplier",
+ "tax_difference": -0.09,
+ "taxable_value_difference": -0.5
+ }
+ }
+ ],
+ "TEST_BILL_NO_SUGGESTED_MATCH": [
+ {
+ "PURCHASE_INVOICE": {
+ "bill_no": "BILL-23-00040"
+ },
+ "INWARD_SUPPLY": {
+ "bill_no": "BILL-23-00045"
+ },
+ "RECONCILED_DATA": {
+ "action": "No Action",
+ "bill_date": "2023-12-11",
+ "bill_no": "BILL-23-00040",
+ "classification": "B2B",
+ "differences": "",
+ "inward_supply_company_gstin": "24AAQCA8719H1ZC",
+ "inward_supply_name": "",
+ "match_status": "Suggested Match",
+ "purchase_company_gstin": "24AAQCA8719H1ZC",
+ "purchase_doctype": "Purchase Invoice",
+ "purchase_invoice_name": "",
+ "supplier_gstin": "24AABCR6898M1ZN",
+ "supplier_name": "_Test Registered Supplier",
+ "tax_difference": 0.0,
+ "taxable_value_difference": 0.0
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
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 404753faa5..6c89cd6da2 100644
--- a/india_compliance/gst_india/doctype/gst_settings/gst_settings.json
+++ b/india_compliance/gst_india/doctype/gst_settings/gst_settings.json
@@ -50,6 +50,34 @@
"gst_uom_map",
"accounts_tab",
"gst_accounts",
+ "purchase_reconciliation_tab",
+ "purchase_reconciliation_section",
+ "enable_auto_reconciliation",
+ "inward_supply_period",
+ "column_break_zlfe",
+ "gst_categories_section",
+ "reconcile_for_b2b",
+ "reconcile_for_b2ba",
+ "column_break_notc",
+ "reconcile_for_cdnr",
+ "reconcile_for_cdnra",
+ "column_break_jpws",
+ "reconcile_for_isd",
+ "reconcile_for_isda",
+ "column_break_brkt",
+ "reconcile_for_impg",
+ "reconcile_for_impgsez",
+ "auto_reconciliation_days_section",
+ "reconcile_on_monday",
+ "reconcile_on_friday",
+ "column_break_fghn",
+ "reconcile_on_tuesday",
+ "reconcile_on_saturday",
+ "column_break_koqs",
+ "reconcile_on_wednesday",
+ "reconcile_on_sunday",
+ "column_break_sdge",
+ "reconcile_on_thursday",
"credentials_tab",
"credentials",
"api_secret",
@@ -365,6 +393,160 @@
"fieldtype": "Check",
"label": "Enable e-Waybill Generation from Purchase Receipt"
},
+ {
+ "fieldname": "purchase_reconciliation_section",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "enable_auto_reconciliation",
+ "fieldtype": "Check",
+ "label": "Enable Auto Reconciliation"
+ },
+ {
+ "fieldname": "column_break_zlfe",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval: india_compliance.is_api_enabled(doc)",
+ "fieldname": "purchase_reconciliation_tab",
+ "fieldtype": "Tab Break",
+ "label": "Purchase Reconciliation"
+ },
+ {
+ "depends_on": "eval: doc.enable_auto_reconciliation",
+ "fieldname": "auto_reconciliation_days_section",
+ "fieldtype": "Section Break",
+ "label": "Auto Reconciliation Days"
+ },
+ {
+ "fieldname": "column_break_fghn",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "2",
+ "depends_on": "eval: doc.enable_auto_reconciliation",
+ "description": "The Inward Supply data for the specified number of months will be used for reconciliation starting from the provided date.",
+ "fieldname": "inward_supply_period",
+ "fieldtype": "Int",
+ "label": "Inward Supply Period in months"
+ },
+ {
+ "depends_on": "eval: doc.enable_auto_reconciliation",
+ "fieldname": "gst_categories_section",
+ "fieldtype": "Section Break",
+ "label": "GST Categories"
+ },
+ {
+ "fieldname": "column_break_brkt",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_notc",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_jpws",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_koqs",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_sdge",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "reconcile_on_monday",
+ "fieldtype": "Check",
+ "label": "Monday"
+ },
+ {
+ "default": "1",
+ "fieldname": "reconcile_on_friday",
+ "fieldtype": "Check",
+ "label": "Friday"
+ },
+ {
+ "default": "1",
+ "fieldname": "reconcile_on_tuesday",
+ "fieldtype": "Check",
+ "label": "Tuesday"
+ },
+ {
+ "default": "0",
+ "fieldname": "reconcile_on_saturday",
+ "fieldtype": "Check",
+ "label": "Saturday"
+ },
+ {
+ "default": "0",
+ "fieldname": "reconcile_on_wednesday",
+ "fieldtype": "Check",
+ "label": "Wednesday"
+ },
+ {
+ "default": "0",
+ "fieldname": "reconcile_on_sunday",
+ "fieldtype": "Check",
+ "label": "Sunday"
+ },
+ {
+ "default": "0",
+ "fieldname": "reconcile_on_thursday",
+ "fieldtype": "Check",
+ "label": "Thursday"
+ },
+ {
+ "default": "1",
+ "fieldname": "reconcile_for_b2b",
+ "fieldtype": "Check",
+ "label": "B2B"
+ },
+ {
+ "default": "0",
+ "fieldname": "reconcile_for_b2ba",
+ "fieldtype": "Check",
+ "label": "B2BA"
+ },
+ {
+ "default": "1",
+ "fieldname": "reconcile_for_cdnr",
+ "fieldtype": "Check",
+ "label": "CDNR"
+ },
+ {
+ "default": "0",
+ "fieldname": "reconcile_for_cdnra",
+ "fieldtype": "Check",
+ "label": "CDNRA"
+ },
+ {
+ "default": "0",
+ "fieldname": "reconcile_for_isd",
+ "fieldtype": "Check",
+ "label": "ISD"
+ },
+ {
+ "default": "0",
+ "fieldname": "reconcile_for_isda",
+ "fieldtype": "Check",
+ "label": "ISDA"
+ },
+ {
+ "default": "0",
+ "fieldname": "reconcile_for_impg",
+ "fieldtype": "Check",
+ "label": "IMPG"
+ },
+ {
+ "default": "0",
+ "fieldname": "reconcile_for_impgsez",
+ "fieldtype": "Check",
+ "label": "IMPGSEZ"
+ },
{
"default": "0",
"depends_on": "eval: india_compliance.is_api_enabled(doc)",
@@ -384,7 +566,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2024-01-22 18:09:00.011977",
+ "modified": "2024-03-14 11:04:18.651943",
"modified_by": "Administrator",
"module": "GST India",
"name": "GST Settings",
diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py
index c283c081df..8e35860dd0 100644
--- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py
+++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py
@@ -16,6 +16,7 @@
from india_compliance.gst_india.utils import (
get_escaped_name,
get_gst_accounts_by_type,
+ get_gstin_list,
get_party_for_gstin,
)
from india_compliance.gst_india.utils.gstr import IMPORT_CATEGORY, ReturnType
@@ -24,6 +25,7 @@
class Fields(Enum):
FISCAL_YEAR = "fy"
SUPPLIER_GSTIN = "supplier_gstin"
+ COMPANY_GSTIN = "company_gstin"
BILL_NO = "bill_no"
PLACE_OF_SUPPLY = "place_of_supply"
REVERSE_CHARGE = "is_reverse_charge"
@@ -76,6 +78,7 @@ class MatchStatus(Enum):
"rule": {
Fields.FISCAL_YEAR: Rule.EXACT_MATCH,
Fields.SUPPLIER_GSTIN: Rule.EXACT_MATCH,
+ Fields.COMPANY_GSTIN: Rule.EXACT_MATCH,
Fields.BILL_NO: Rule.EXACT_MATCH,
Fields.PLACE_OF_SUPPLY: Rule.EXACT_MATCH,
Fields.REVERSE_CHARGE: Rule.EXACT_MATCH,
@@ -91,6 +94,7 @@ class MatchStatus(Enum):
"rule": {
Fields.FISCAL_YEAR: Rule.EXACT_MATCH,
Fields.SUPPLIER_GSTIN: Rule.EXACT_MATCH,
+ Fields.COMPANY_GSTIN: Rule.EXACT_MATCH,
Fields.BILL_NO: Rule.FUZZY_MATCH,
Fields.PLACE_OF_SUPPLY: Rule.EXACT_MATCH,
Fields.REVERSE_CHARGE: Rule.EXACT_MATCH,
@@ -106,6 +110,7 @@ class MatchStatus(Enum):
"rule": {
Fields.FISCAL_YEAR: Rule.EXACT_MATCH,
Fields.SUPPLIER_GSTIN: Rule.EXACT_MATCH,
+ Fields.COMPANY_GSTIN: Rule.EXACT_MATCH,
Fields.BILL_NO: Rule.EXACT_MATCH,
Fields.PLACE_OF_SUPPLY: Rule.EXACT_MATCH,
Fields.REVERSE_CHARGE: Rule.EXACT_MATCH,
@@ -121,6 +126,7 @@ class MatchStatus(Enum):
"rule": {
Fields.FISCAL_YEAR: Rule.EXACT_MATCH,
Fields.SUPPLIER_GSTIN: Rule.EXACT_MATCH,
+ Fields.COMPANY_GSTIN: Rule.EXACT_MATCH,
Fields.BILL_NO: Rule.FUZZY_MATCH,
Fields.PLACE_OF_SUPPLY: Rule.EXACT_MATCH,
Fields.REVERSE_CHARGE: Rule.EXACT_MATCH,
@@ -136,6 +142,7 @@ class MatchStatus(Enum):
"rule": {
Fields.FISCAL_YEAR: Rule.EXACT_MATCH,
Fields.SUPPLIER_GSTIN: Rule.EXACT_MATCH,
+ # Fields.COMPANY_GSTIN: Rule.MISMATCH,
Fields.BILL_NO: Rule.EXACT_MATCH,
# Fields.PLACE_OF_SUPPLY: Rule.MISMATCH,
# Fields.IS_REVERSE_CHARGE: Rule.MISMATCH,
@@ -151,6 +158,7 @@ class MatchStatus(Enum):
"rule": {
Fields.FISCAL_YEAR: Rule.EXACT_MATCH,
Fields.SUPPLIER_GSTIN: Rule.EXACT_MATCH,
+ # Fields.COMPANY_GSTIN: Rule.MISMATCH,
Fields.BILL_NO: Rule.FUZZY_MATCH,
# Fields.PLACE_OF_SUPPLY: Rule.MISMATCH,
# Fields.IS_REVERSE_CHARGE: Rule.MISMATCH,
@@ -166,6 +174,7 @@ class MatchStatus(Enum):
"rule": {
Fields.FISCAL_YEAR: Rule.EXACT_MATCH,
Fields.SUPPLIER_GSTIN: Rule.EXACT_MATCH,
+ Fields.COMPANY_GSTIN: Rule.EXACT_MATCH,
# Fields.BILL_NO: Rule.MISMATCH,
Fields.PLACE_OF_SUPPLY: Rule.EXACT_MATCH,
Fields.REVERSE_CHARGE: Rule.EXACT_MATCH,
@@ -185,6 +194,7 @@ class MatchStatus(Enum):
"rule": {
Fields.FISCAL_YEAR: Rule.EXACT_MATCH,
# Fields.SUPPLIER_GSTIN: Rule.MISMATCH,
+ Fields.COMPANY_GSTIN: Rule.EXACT_MATCH,
Fields.BILL_NO: Rule.EXACT_MATCH,
Fields.PLACE_OF_SUPPLY: Rule.EXACT_MATCH,
Fields.REVERSE_CHARGE: Rule.EXACT_MATCH,
@@ -198,6 +208,7 @@ class MatchStatus(Enum):
"rule": {
Fields.FISCAL_YEAR: Rule.EXACT_MATCH,
# Fields.SUPPLIER_GSTIN: Rule.MISMATCH,
+ Fields.COMPANY_GSTIN: Rule.EXACT_MATCH,
Fields.BILL_NO: Rule.FUZZY_MATCH,
Fields.PLACE_OF_SUPPLY: Rule.EXACT_MATCH,
Fields.REVERSE_CHARGE: Rule.EXACT_MATCH,
@@ -211,6 +222,7 @@ class MatchStatus(Enum):
"rule": {
Fields.FISCAL_YEAR: Rule.EXACT_MATCH,
# Fields.SUPPLIER_GSTIN: Rule.MISMATCH,
+ Fields.COMPANY_GSTIN: Rule.MISMATCH,
Fields.BILL_NO: Rule.FUZZY_MATCH,
# Fields.PLACE_OF_SUPPLY: Rule.MISMATCH,
# Fields.IS_REVERSE_CHARGE: Rule.MISMATCH,
@@ -226,6 +238,7 @@ class MatchStatus(Enum):
"rule": {
Fields.FISCAL_YEAR: Rule.EXACT_MATCH,
# Fields.SUPPLIER_GSTIN: Rule.MISMATCH,
+ Fields.COMPANY_GSTIN: Rule.EXACT_MATCH,
# Fields.BILL_NO: Rule.MISMATCH,
Fields.PLACE_OF_SUPPLY: Rule.EXACT_MATCH,
Fields.REVERSE_CHARGE: Rule.EXACT_MATCH,
@@ -292,11 +305,16 @@ def get_query(self, additional_fields=None):
frappe.qb.from_(self.GSTR2)
.left_join(self.GSTR2_ITEM)
.on(self.GSTR2_ITEM.parent == self.GSTR2.name)
- .where(self.company_gstin == self.GSTR2.company_gstin)
.where(IfNull(self.GSTR2.match_status, "") != "Amended")
.groupby(self.GSTR2_ITEM.parent)
.select(*fields, ConstantColumn("GST Inward Supply").as_("doctype"))
)
+
+ if self.company_gstin == "All":
+ query = query.where(self.GSTR2.company_gstin.notnull())
+ else:
+ query = query.where(self.company_gstin == self.GSTR2.company_gstin)
+
if self.include_ignored == 0:
query = query.where(IfNull(self.GSTR2.action, "") != "Ignore")
@@ -311,6 +329,7 @@ def get_fields(self, additional_fields=None, table=None):
"bill_date",
"name",
"supplier_gstin",
+ "company_gstin",
"is_reverse_charge",
"place_of_supply",
]
@@ -413,7 +432,6 @@ def get_query(self, additional_fields=None, is_return=False):
.on(self.PI_TAX.parent == self.PI.name)
.left_join(pi_item)
.on(pi_item.parent == self.PI.name)
- .where(self.company_gstin == self.PI.company_gstin)
.where(self.PI.docstatus == 1)
.where(IfNull(self.PI.reconciliation_status, "") != "Not Applicable")
.groupby(self.PI.name)
@@ -424,6 +442,11 @@ def get_query(self, additional_fields=None, is_return=False):
)
)
+ if self.company_gstin == "All":
+ query = query.where(self.PI.company_gstin.notnull())
+ else:
+ query = query.where(self.company_gstin == self.PI.company_gstin)
+
if self.include_ignored == 0:
query = query.where(IfNull(self.PI.reconciliation_status, "") != "Ignored")
@@ -439,6 +462,7 @@ def get_fields(self, additional_fields=None, is_return=False):
fields = [
"name",
"supplier_gstin",
+ "company_gstin",
"bill_no",
"place_of_supply",
"is_reverse_charge",
@@ -579,7 +603,6 @@ def get_fields(self, additional_fields=None):
tax_fields = [
self.query_tax_amount(account).as_(tax[:-8])
for tax, account in gst_accounts.items()
- if account
]
fields = [
@@ -588,6 +611,7 @@ def get_fields(self, additional_fields=None):
self.BOE.total_taxable_value.as_("taxable_value"),
self.BOE.bill_of_entry_date.as_("bill_date"),
self.BOE.posting_date,
+ self.BOE.company_gstin,
self.PI.supplier_name,
self.PI.place_of_supply,
self.PI.is_reverse_charge,
@@ -1019,6 +1043,7 @@ def get_all_inward_supply(
):
inward_supply_fields = [
"supplier_name",
+ "company_gstin",
"classification",
"match_status",
"action",
@@ -1036,6 +1061,7 @@ def get_all_purchase_invoice_and_bill_of_entry(
purchase_fields = [
"supplier",
"supplier_name",
+ "company_gstin",
"is_return",
"gst_category",
"reconciliation_status",
@@ -1096,6 +1122,8 @@ def process_data(self, reconciliation_data: list, retain_doc: bool = False):
default_dict = {
"supplier_name": "",
"supplier_gstin": "",
+ "purchase_company_gstin": "",
+ "inward_supply_company_gstin": "",
"bill_no": "",
"bill_date": "",
"match_status": "",
@@ -1131,6 +1159,8 @@ def update_fields(self, data, purchase, inward_supply):
"supplier_name": data.supplier_name
or self.guess_supplier_name(data.supplier_gstin),
"supplier_gstin": data.supplier_gstin or data.supplier_name,
+ "purchase_company_gstin": purchase.get("company_gstin") or "",
+ "inward_supply_company_gstin": inward_supply.get("company_gstin") or "",
"purchase_doctype": purchase.get("doctype"),
"purchase_invoice_name": purchase.get("name"),
"inward_supply_name": inward_supply.get("name"),
diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_detail_comparision.html b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_detail_comparision.html
index 38e24f78d1..6735501a0d 100644
--- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_detail_comparision.html
+++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_detail_comparision.html
@@ -8,6 +8,11 @@
+
+ Company GSTIN |
+ {{ inward_supply.company_gstin || '-' }} |
+ {{ purchase.company_gstin || '-' }} |
+
Document Links
{% if inward_supply.name %}
diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js
index 0da90d2307..6a5a85673c 100644
--- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js
+++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js
@@ -60,6 +60,7 @@ async function add_gstr2b_alert(frm) {
frm,
[frm.doc.inward_supply_from_date, frm.doc.inward_supply_to_date],
ReturnType.GSTR2B,
+ frm.company_gstin,
true
);
remove_gstr2b_alert(existing_alert);
@@ -1112,6 +1113,7 @@ class ImportDialog {
constructor(frm, for_download = true) {
this.frm = frm;
this.for_download = for_download;
+ this.company_gstin = frm.doc.company_gstin;
this.init_dialog();
this.dialog.show();
}
@@ -1171,18 +1173,18 @@ class ImportDialog {
if (this.return_type === ReturnType.GSTR2A) {
this.dialog.$wrapper.find(".btn-secondary").removeClass("hidden");
this.dialog.set_primary_action(__("Download All"), () => {
- download_gstr(this.frm, this.date_range, this.return_type, false);
+ download_gstr(this.frm, this.date_range, this.return_type, this.company_gstin, false);
this.dialog.hide();
});
this.dialog.set_secondary_action_label(__("Download Missing"));
this.dialog.set_secondary_action(() => {
- download_gstr(this.frm, this.date_range, this.return_type, true);
+ download_gstr(this.frm, this.date_range, this.return_type, this.company_gstin, true);
this.dialog.hide();
});
} else if (this.return_type === ReturnType.GSTR2B) {
this.dialog.$wrapper.find(".btn-secondary").addClass("hidden");
this.dialog.set_primary_action(__("Download"), () => {
- download_gstr(this.frm, this.date_range, this.return_type, true);
+ download_gstr(this.frm, this.date_range, this.return_type, this.company_gstin, true);
this.dialog.hide();
});
}
@@ -1205,12 +1207,14 @@ class ImportDialog {
async fetch_import_history() {
const { message } = await this.frm.call("get_import_history", {
+ company_gstin: this.company_gstin,
return_type: this.return_type,
date_range: this.date_range,
for_download: this.for_download,
});
- if (!message) return;
+ // TODO: modify HTML for case: company_gstin == "All"
+ if (!message || this.company_gstin == "All") return;
this.dialog.fields_dict.history.html(
frappe.render_template("gstr_download_history", message)
);
@@ -1263,6 +1267,25 @@ class ImportDialog {
this.return_type = this.dialog.get_value("return_type");
},
},
+ {
+ label: "Company GSTIN",
+ fieldname: "company_gstin",
+ fieldtype: "Autocomplete",
+ default: this.frm.doc.company_gstin,
+ get_query: async () => {
+ let { message: gstin_list } = await frappe.call({
+ method: "india_compliance.gst_india.utils.get_gstin_list",
+ args: { party: this.frm.doc.company },
+ });
+
+ gstin_list.unshift("All");
+ this.dialog.fields_dict.company_gstin.set_data(gstin_list);
+ },
+ onchange: () => {
+ this.company_gstin = this.dialog.get_value("company_gstin");
+ this.fetch_import_history();
+ }
+ },
{
fieldtype: "Column Break",
},
@@ -1312,29 +1335,25 @@ async function download_gstr(
frm,
date_range,
return_type,
+ company_gstin,
only_missing = true,
otp = null
) {
- let method;
- const args = { date_range, otp };
-
- if (return_type === ReturnType.GSTR2A) {
- method = "download_gstr_2a";
- args.force = !only_missing;
- } else {
- method = "download_gstr_2b";
- }
+ const authenticated_company_gstins =
+ await india_compliance.authenticate_company_gstins(
+ frm.doc.company,
+ company_gstin == "All" ? null : company_gstin
+ );
+ const args = {
+ return_type: return_type,
+ company_gstins: authenticated_company_gstins,
+ date_range: date_range,
+ force: !only_missing,
+ otp,
+ };
frm.events.show_progress(frm, "download");
- const { message } = await frm.call(method, args);
-
- if (message && ["otp_requested", "invalid_otp"].includes(message.error_type)) {
- const otp = await india_compliance.get_gstin_otp(
- message.error_type,
- frm.doc.company_gstin
- );
- if (otp) download_gstr(frm, date_range, return_type, only_missing, otp);
- }
+ await frm.call("download_gstr", args);
}
class EmailDialog {
@@ -1692,6 +1711,8 @@ async function set_gstin_options(frm) {
});
if (!message) return [];
+ message.unshift("All");
+
const gstin_field = frm.get_field("company_gstin");
gstin_field.set_data(message);
return message;
diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py
index 9208b8be0d..74abe18896 100644
--- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py
+++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py
@@ -7,6 +7,7 @@
import frappe
from frappe.model.document import Document
from frappe.query_builder.functions import IfNull
+from frappe.utils import add_to_date, cint, now_datetime
from frappe.utils.response import json_handler
from india_compliance.gst_india.constants import ORIGINAL_VS_AMENDED
@@ -17,9 +18,14 @@
ReconciledData,
Reconciler,
)
-from india_compliance.gst_india.utils import get_json_from_file, get_timespan_date_range
+from india_compliance.gst_india.utils import (
+ get_json_from_file,
+ get_timespan_date_range,
+ is_api_enabled,
+)
from india_compliance.gst_india.utils.exporter import ExcelExporter
from india_compliance.gst_india.utils.gstr import (
+ ACTIONS,
IMPORT_CATEGORY,
GSTRCategory,
ReturnsAPI,
@@ -96,38 +102,23 @@ def upload_gstr(self, return_type, period, file_path):
return save_gstr_2b(self.company_gstin, period, json_data)
@frappe.whitelist()
- def download_gstr_2a(self, date_range, force=False, otp=None):
+ def download_gstr(
+ self, company_gstins, date_range, return_type=None, force=False, otp=None
+ ):
frappe.has_permission("Purchase Reconciliation Tool", "write", throw=True)
- return_type = ReturnType.GSTR2A
- periods = BaseUtil.get_periods(date_range, return_type)
- if not force:
- periods = self.get_periods_to_download(return_type, periods)
-
- return download_gstr_2a(self.company_gstin, periods, otp)
-
- @frappe.whitelist()
- def download_gstr_2b(self, date_range, otp=None):
- frappe.has_permission("Purchase Reconciliation Tool", "write", throw=True)
-
- return_type = ReturnType.GSTR2B
- periods = self.get_periods_to_download(
- return_type, BaseUtil.get_periods(date_range, return_type)
- )
- return download_gstr_2b(self.company_gstin, periods, otp)
-
- def get_periods_to_download(self, return_type, periods):
- existing_periods = get_import_history(
- self.company_gstin,
- return_type,
- periods,
- pluck="return_period",
+ download_gstr(
+ company_gstins=company_gstins,
+ date_range=date_range,
+ return_type=return_type,
+ force=force,
+ otp=otp,
)
- return [period for period in periods if period not in existing_periods]
-
@frappe.whitelist()
- def get_import_history(self, return_type, date_range, for_download=True):
+ def get_import_history(
+ self, company_gstin, return_type, date_range, for_download=True
+ ):
frappe.has_permission("Purchase Reconciliation Tool", "write", throw=True)
if not return_type:
@@ -135,7 +126,7 @@ def get_import_history(self, return_type, date_range, for_download=True):
return_type = ReturnType(return_type)
periods = BaseUtil.get_periods(date_range, return_type, True)
- history = get_import_history(self.company_gstin, return_type, periods)
+ history = get_import_history(company_gstin, return_type, periods)
columns = [
"Period",
@@ -467,6 +458,61 @@ def _get_link_options(self, data):
return data
+def download_gstr(
+ company_gstins,
+ date_range,
+ return_type=None,
+ force=False,
+ otp=None,
+ gst_categories=None,
+):
+ if return_type:
+ return_type = ReturnType(return_type)
+
+ for company_gstin in company_gstins:
+ try:
+ if not return_type or return_type == ReturnType.GSTR2A:
+ _download_gstr_2a(date_range, company_gstin, force, otp, gst_categories)
+
+ if not return_type or return_type == ReturnType.GSTR2B:
+ _download_gstr_2b(date_range, company_gstin, otp)
+ except Exception:
+ frappe.log_error(
+ frappe.get_traceback(),
+ f"Error while downloading {return_type.value if return_type else 'GSTR 2A & 2B'} for {company_gstin} ",
+ )
+
+
+def _download_gstr_2a(
+ date_range, company_gstin, force=False, otp=None, gst_categories=None
+):
+ return_type = ReturnType.GSTR2A
+ periods = BaseUtil.get_periods(date_range, return_type)
+ if not force:
+ periods = get_periods_to_download(company_gstin, return_type, periods)
+
+ return download_gstr_2a(company_gstin, periods, otp, gst_categories)
+
+
+def _download_gstr_2b(date_range, company_gstin, otp=None):
+ return_type = ReturnType.GSTR2B
+ periods = get_periods_to_download(
+ company_gstin, return_type, BaseUtil.get_periods(date_range, return_type)
+ )
+ return download_gstr_2b(company_gstin, periods, otp)
+
+
+def get_periods_to_download(company_gstin, return_type, periods):
+ existing_periods = get_import_history(
+ company_gstin,
+ return_type,
+ periods,
+ pluck="return_period",
+ )
+
+ return [period for period in periods if period not in existing_periods]
+
+
def get_import_history(
company_gstin, return_type: ReturnType, periods: List[str], fields=None, pluck=None
):
@@ -556,11 +602,137 @@ def wrapper(*args, **kwargs):
return wrapper
-@frappe.whitelist()
-def resend_otp(company_gstin):
- frappe.has_permission("Purchase Reconciliation Tool", "write", throw=True)
+def auto_refresh_authtoken():
+ is_auto_refresh_enabled = frappe.db.get_single_value(
+ "GST Settings", "auto_refresh_auth_token"
+ )
+
+ if not is_auto_refresh_enabled:
+ return
+
+ for credential in frappe.get_all(
+ "GST Credential",
+ filters={
+ "service": "Returns",
+ "session_expiry": (">=", now_datetime()),
+ },
+ fields=["session_key", "session_expiry", "gstin", "auth_token"],
+ ):
+ if credential.session_key and credential.session_expiry < add_to_date(
+ now_datetime(), minutes=10
+ ):
+ api = ReturnsAPI(credential.gstin)
+ response = api.refresh_auth_token()
+ api.process_response(response)
+
+
+class AutoReconcile:
+ def __init__(self):
+ self.gst_settings = frappe.get_cached_doc("GST Settings")
+ self.today = frappe.utils.getdate()
+
+ self.inward_supply_from_date = frappe.utils.add_months(
+ frappe.utils.get_first_day(self.today),
+ -cint(self.gst_settings.inward_supply_period - 1),
+ )
+ self.reconciliation_companies = self.get_reconciliation_company_list()
+
+ def download_gstr(self):
+ if not self.is_reconciliation_enabled():
+ return
+
+ # GST Categories for which GSTR 2A is to be downloaded
+ gst_categories = self.get_gst_categories()
+ gstins = self.get_gstins_with_valid_credentials()
+
+ download_gstr(
+ date_range=[
+ self.inward_supply_from_date.strftime("%Y-%m-%d"),
+ self.today.strftime("%Y-%m-%d"),
+ ],
+ company_gstins=gstins,
+ gst_categories=gst_categories,
+ )
+
+ def get_gst_categories(self):
+ return [
+ category.value
+ for category in ACTIONS.values()
+ if getattr(self.gst_settings, "reconcile_for_" + category.value.lower())
+ ]
+
+ def get_gstins_with_valid_credentials(self):
+ valid_gstins = set()
+
+ for row in self.gst_settings.credentials:
+ if not self.is_authenticated_credential(row):
+ continue
+
+ valid_gstins.add(row.gstin)
+
+ return valid_gstins
+
+ def is_authenticated_credential(self, credential_row):
+ """Returns True if reconciliation is enabled for the company and the session is valid"""
+ return (
+ credential_row.company in self.reconciliation_companies
+ and credential_row.session_expiry >= now_datetime()
+ )
+
+ def reconcile_purchases(self):
+ """Reconcile purchases for selected companies and GSTINs with valid credentials"""
+ if not self.is_reconciliation_enabled():
+ return
+
+ for company in self.reconciliation_companies:
+ self.reconcile_purchases_for_company(company)
+
+ def reconcile_purchases_for_company(self, company):
+ purchase_reconciliation_tool = frappe.get_doc("Purchase Reconciliation Tool")
+ purchase_reconciliation_tool.update(
+ {
+ "company": company,
+ "company_gstin": "All",
+ "gst_return": "Both GSTR 2A & 2B",
+ "purchase_from_date": frappe.utils.add_years(self.today, -1),
+ "purchase_to_date": self.today,
+ "inward_supply_from_date": self.inward_supply_from_date,
+ "inward_supply_to_date": self.today,
+ }
+ )
+
+ purchase_reconciliation_tool.save(ignore_permissions=True)
+
+ def get_reconciliation_company_list(self):
+ """Returns list of companies for which auto reconciliation is enabled and credentials are available"""
+ companies = set()
+ for credential in self.gst_settings.credentials:
+ if credential.service == "Returns":
+ companies.add(credential.company)
+
+ return companies
+
+ def is_reconciliation_enabled(self):
+ """Returns True if auto reconciliation is enabled for the current day"""
+ if not is_api_enabled(self.gst_settings):
+ return False
+
+ if self.settings.sandbox_mode:
+ return False
+
+ return self.gst_settings.enable_auto_reconciliation and self.gst_settings.get(
+ "reconcile_on_" + frappe.utils.getdate().strftime("%A").lower()
+ )
+
+
+def auto_download_gstr():
+ """Auto download GSTR 2A and 2B"""
+ AutoReconcile().download_gstr()
+
- return ReturnsAPI(company_gstin).request_otp()
+def auto_reconcile():
+ """Auto reconcile purchases and inward supplies"""
+ AutoReconcile().reconcile_purchases()
class BuildExcel:
diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/test_purchase_reconciliation_tool.py b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/test_purchase_reconciliation_tool.py
index a9a7a7d989..0c9dc5172c 100644
--- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/test_purchase_reconciliation_tool.py
+++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/test_purchase_reconciliation_tool.py
@@ -1,9 +1,164 @@
# Copyright (c) 2022, Resilient Tech and Contributors
# See license.txt
-# import frappe
+import datetime
+
+import frappe
+from frappe.test_runner import make_test_objects
from frappe.tests.utils import FrappeTestCase
+from india_compliance.gst_india.doctype.bill_of_entry.bill_of_entry import (
+ make_bill_of_entry,
+)
+from india_compliance.gst_india.utils.tests import (
+ create_purchase_invoice as _create_purchase_invoice,
+)
+
+PURCHASE_INVOICE_DEFAULT_ARGS = {
+ "bill_no": "BILL-23-00001",
+ "bill_date": "2023-12-11",
+ "qty": 10,
+ "rate": 1000,
+ "is_in_state": 1,
+ "posting_date": "2023-12-11",
+ "set_posting_time": 1,
+}
+INWARD_SUPPLY_DEFAULT_ARGS = {
+ "company": "_Test Indian Registered Company",
+ "company_gstin": "24AAQCA8719H1ZC",
+ "supplier_name": "_Test Registered Supplier",
+ "bill_no": "BILL-23-00001",
+ "bill_date": "2023-12-11",
+ "classification": "B2B",
+ "doc_type": "Invoice",
+ "supply_type": "Regular",
+ "place_of_supply": "24-Gujarat",
+ "supplier_gstin": "24AABCR6898M1ZN",
+ "items": [{"taxable_value": 10000, "rate": 18, "sgst": 900, "cgst": 900}],
+ "document_value": 11800,
+ "itc_availability": "Yes",
+ "return_period_2b": "122023",
+ "gen_date_2b": "2023-12-11",
+}
+BILL_OF_ENTRY_DEFAULT_ARGS = {
+ "supplier": "_Test Foreign Supplier",
+ "supplier_gstin": "",
+ "gst_category": "Overseas",
+ "is_in_state": 0,
+ "posting_date": "2023-12-11",
+ "set_posting_time": 1,
+}
+
class TestPurchaseReconciliationTool(FrappeTestCase):
- pass
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ cls.test_data = frappe.get_file_json(
+ frappe.get_app_path(
+ "india_compliance",
+ "gst_india",
+ "data",
+ "test_purchase_reconciliation_tool.json",
+ )
+ )
+
+ cls.create_test_data()
+
+ def test_purchase_reconciliation_tool(self):
+ purchase_reconciliation_tool = frappe.get_doc("Purchase Reconciliation Tool")
+ purchase_reconciliation_tool.update(
+ {
+ "company": "_Test Indian Registered Company",
+ "company_gstin": "All",
+ "purchase_from_date": "2023-11-01",
+ "purchase_to_date": "2023-12-31",
+ "inward_supply_from_date": "2023-11-01",
+ "inward_supply_to_date": "2023-12-31",
+ "gst_return": "GSTR 2B",
+ }
+ )
+
+ purchase_reconciliation_tool.save(ignore_permissions=True)
+ reconciled_data = purchase_reconciliation_tool.ReconciledData.get()
+
+ for row in reconciled_data:
+ for key, value in row.items():
+ if isinstance(value, datetime.date):
+ row[key] = str(value)
+
+ for row in reconciled_data:
+ self.assertDictEqual(
+ row,
+ self.reconciled_data.get(
+ (row.purchase_invoice_name, row.inward_supply_name)
+ )
+ or {},
+ )
+
+ @classmethod
+ def create_test_data(cls):
+ frappe.db.set_single_value("GST Settings", "enable_overseas_transactions", 1)
+ test_cases = cls.test_data.get("TEST_CASES")
+
+ make_test_objects("Address", cls.test_data.get("ADDRESSES"), reset=True)
+
+ cls.reconciled_data = frappe._dict()
+
+ for test_case in test_cases.values():
+ for value in test_case:
+ if value.get("PURCHASE_INVOICE"):
+ pi = create_purchase_invoice(**value.get("PURCHASE_INVOICE"))
+
+ elif value.get("BILL_OF_ENTRY"):
+ pi = create_boe(**value.get("BILL_OF_ENTRY"))
+
+ if value.get("INWARD_SUPPLY"):
+ gst_is = create_gst_inward_supply(**value.get("INWARD_SUPPLY"))
+
+ _reconciled_data = value.get("RECONCILED_DATA")
+
+ _reconciled_data["purchase_invoice_name"] = pi.get("name")
+ _reconciled_data["inward_supply_name"] = gst_is.get("name")
+
+ cls.reconciled_data[(pi.get("name"), gst_is.get("name"))] = (
+ _reconciled_data
+ )
+
+ frappe.db.set_single_value("GST Settings", "enable_overseas_transactions", 0)
+
+
+def create_purchase_invoice(**kwargs):
+ args = PURCHASE_INVOICE_DEFAULT_ARGS.copy()
+ args.update(kwargs)
+
+ return _create_purchase_invoice(**args).submit()
+
+
+def create_gst_inward_supply(**kwargs):
+ args = INWARD_SUPPLY_DEFAULT_ARGS.copy()
+ args.update(kwargs)
+
+ gst_inward_supply = frappe.new_doc("GST Inward Supply")
+
+ gst_inward_supply.update(args)
+
+ return gst_inward_supply.insert()
+
+
+def create_boe(**kwargs):
+ kwargs.update(BILL_OF_ENTRY_DEFAULT_ARGS)
+
+ pi = create_purchase_invoice(**kwargs)
+ pi.submit()
+ boe = make_bill_of_entry(pi.name)
+ boe.update(
+ {
+ "bill_of_entry_no": pi.bill_no,
+ "bill_of_entry_date": pi.bill_date,
+ "posting_date": pi.posting_date,
+ }
+ )
+
+ return boe.save(ignore_permissions=True).submit()
diff --git a/india_compliance/gst_india/setup/__init__.py b/india_compliance/gst_india/setup/__init__.py
index 551b3c6758..e6280d091b 100644
--- a/india_compliance/gst_india/setup/__init__.py
+++ b/india_compliance/gst_india/setup/__init__.py
@@ -204,6 +204,13 @@ def set_default_gst_settings():
"validate_gstin_status": 1,
"gstin_status_refresh_interval": 30,
"enable_retry_einv_ewb_generation": 1,
+ # Auto - Reconciliation
+ "enable_auto_reconciliation": 1,
+ "inward_supply_period": 2,
+ "reconcile_on_tuesday": 1,
+ "reconcile_on_friday": 1,
+ "reconcile_for_b2b": 1,
+ "reconcile_for_cdnr": 1,
}
if frappe.conf.developer_mode:
diff --git a/india_compliance/gst_india/utils/gstr/__init__.py b/india_compliance/gst_india/utils/gstr/__init__.py
index ff4a356a14..3fd771b972 100644
--- a/india_compliance/gst_india/utils/gstr/__init__.py
+++ b/india_compliance/gst_india/utils/gstr/__init__.py
@@ -52,7 +52,7 @@ class GSTRCategory(Enum):
IMPORT_CATEGORY = ("IMPG", "IMPGSEZ")
-def download_gstr_2a(gstin, return_periods, otp=None):
+def download_gstr_2a(gstin, return_periods, otp=None, gst_categories=None):
total_expected_requests = len(return_periods) * len(ACTIONS)
requests_made = 0
queued_message = False
@@ -73,6 +73,9 @@ def download_gstr_2a(gstin, return_periods, otp=None):
):
continue
+ if gst_categories and category.value not in gst_categories:
+ continue
+
frappe.publish_realtime(
"update_api_progress",
{
diff --git a/india_compliance/gst_india/utils/gstr/gstr.py b/india_compliance/gst_india/utils/gstr/gstr.py
index afa480ac4d..6b7d16eb23 100644
--- a/india_compliance/gst_india/utils/gstr/gstr.py
+++ b/india_compliance/gst_india/utils/gstr/gstr.py
@@ -1,9 +1,13 @@
import frappe
+from frappe import _
+from frappe.utils import add_to_date, now_datetime
from india_compliance.gst_india.constants import STATE_NUMBERS
from india_compliance.gst_india.doctype.gst_inward_supply.gst_inward_supply import (
create_inward_supply,
)
+from india_compliance.gst_india.utils import get_gstin_list
+from india_compliance.gst_india.utils.gstr import ReturnsAPI
def get_mapped_value(value, mapping):
@@ -122,3 +126,85 @@ def set_key(self, key, value):
def update_gstins(self):
pass
+
+
+@frappe.whitelist()
+def validate_company_gstins(company=None, company_gstin=None):
+ """
+ Checks the validity of the company's GSTIN authentication.
+
+ Args:
+ company_gstin (str): The GSTIN of the company to validate.
+
+ Returns:
+ dict: A dictionary where the keys are the GSTINs and the values are booleans indicating whether the authentication is valid.
+ """
+ frappe.has_permission("GST Settings", throw=True)
+
+ credentials = get_company_gstin_credentials(company, company_gstin)
+
+ if company_gstin and not credentials:
+ frappe.throw(
+ _("Missing GSTIN credentials for GSTIN: {gstin}.").format(
+ gstin=company_gstin
+ )
+ )
+
+ if not credentials:
+ frappe.throw(_("Missing credentials in GST Settings"))
+
+ if company and not company_gstin:
+ missing_credentials = set(get_gstin_list(company)) - set(
+ credential.gstin for credential in credentials
+ )
+
+ if missing_credentials:
+ frappe.throw(
+ _("Missing GSTIN credentials for GSTIN(s): {gstins}.").format(
+ gstins=", ".join(missing_credentials),
+ )
+ )
+
+ gstin_authentication_status = {
+ credential.gstin: (
+ credential.session_expiry
+ and credential.auth_token
+ and credential.session_expiry > add_to_date(now_datetime(), minutes=30)
+ )
+ for credential in credentials
+ }
+
+ return gstin_authentication_status
+
+
+def get_company_gstin_credentials(company=None, company_gstin=None):
+ filters = {"service": "Returns"}
+
+ if company:
+ filters["company"] = company
+
+ if company_gstin:
+ filters["gstin"] = company_gstin
+
+ return frappe.get_all(
+ "GST Credential",
+ filters=filters,
+ fields=["gstin", "session_expiry", "auth_token"],
+ )
+
+
+@frappe.whitelist()
+def request_otp(company_gstin):
+ frappe.has_permission("GST Settings", throw=True)
+
+ return ReturnsAPI(company_gstin).request_otp()
+
+
+@frappe.whitelist()
+def authenticate_otp(company_gstin, otp):
+ frappe.has_permission("GST Settings", throw=True)
+
+ api = ReturnsAPI(company_gstin)
+ response = api.autheticate_with_otp(otp)
+
+ return api.process_response(response)
diff --git a/india_compliance/hooks.py b/india_compliance/hooks.py
index cb17d431b5..245d42d640 100644
--- a/india_compliance/hooks.py
+++ b/india_compliance/hooks.py
@@ -365,6 +365,13 @@
"*/5 * * * *": [
"india_compliance.gst_india.utils.e_invoice.retry_e_invoice_e_waybill_generation",
"india_compliance.gst_india.utils.gstr.download_queued_request",
+ "india_compliance.gst_india.doctype.purchase_reconciliation_tool.purchase_reconciliation_tool.auto_refresh_authtoken",
+ ],
+ "0 2 * * *": [
+ "india_compliance.gst_india.doctype.purchase_reconciliation_tool.purchase_reconciliation_tool.auto_download_gstr",
+ ],
+ "0 4 * * *": [
+ "india_compliance.gst_india.doctype.purchase_reconciliation_tool.purchase_reconciliation_tool.auto_reconcile",
],
"0 1 * * *": [
"india_compliance.gst_india.utils.e_waybill.extend_scheduled_e_waybills"
diff --git a/india_compliance/income_tax_india/overrides/company.py b/india_compliance/income_tax_india/overrides/company.py
index 72d512261b..90ca5fbd86 100644
--- a/india_compliance/income_tax_india/overrides/company.py
+++ b/india_compliance/income_tax_india/overrides/company.py
@@ -71,7 +71,7 @@ def update_existing_tax_withholding_category(category_doc, category_name, compan
else:
accounts = category_doc.get("accounts")
if accounts:
- doc.append("accounts", accounts[0])
+ doc.extend("accounts", accounts)
# add rates if not present for the dates
largest_date = None
diff --git a/india_compliance/patches.txt b/india_compliance/patches.txt
index 9869b84700..0418584068 100644
--- a/india_compliance/patches.txt
+++ b/india_compliance/patches.txt
@@ -46,3 +46,4 @@ india_compliance.patches.v14.update_e_invoice_status
execute:from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import execute_repost_item_valuation; execute_repost_item_valuation()
india_compliance.patches.v14.set_hsn_from_purchase_invoice_to_bill_of_entry
india_compliance.patches.v14.update_item_gst_details_and_gst_trearment_in_bill_of_entry
+india_compliance.patches.v14.update_default_auto_reconciliation_settings
diff --git a/india_compliance/patches/v14/update_default_auto_reconciliation_settings.py b/india_compliance/patches/v14/update_default_auto_reconciliation_settings.py
new file mode 100644
index 0000000000..e46b7c0696
--- /dev/null
+++ b/india_compliance/patches/v14/update_default_auto_reconciliation_settings.py
@@ -0,0 +1,14 @@
+import frappe
+
+
+def execute():
+ frappe.db.set_single_value(
+ "GST Settings",
+ {
+ "inward_supply_period": 2,
+ "reconcile_on_tuesday": 1,
+ "reconcile_on_friday": 1,
+ "reconcile_for_b2b": 1,
+ "reconcile_for_cdnr": 1,
+ },
+ )
diff --git a/india_compliance/public/js/utils.js b/india_compliance/public/js/utils.js
index c263b37bd4..5c6391a9c9 100644
--- a/india_compliance/public/js/utils.js
+++ b/india_compliance/public/js/utils.js
@@ -162,9 +162,9 @@ Object.assign(india_compliance, {
get_gstin_otp(error_type, company_gstin) {
let description =
- "An OTP has been sent to your registered mobile/email for further authentication. Please provide OTP.";
+ `An OTP has been sent to the registered mobile/email for GSTIN ${company_gstin} for further authentication. Please provide OTP.`;
if (error_type === "invalid_otp")
- description = "Invalid OTP was provided. Please try again.";
+ description = `Invalid OTP was provided for GSTIN ${company_gstin}. Please try again.`;
return new Promise(resolve => {
const prompt = new frappe.ui.Dialog({
@@ -186,7 +186,7 @@ Object.assign(india_compliance, {
secondary_action_label: __("Resend OTP"),
secondary_action() {
frappe.call({
- method: "india_compliance.gst_india.doctype.purchase_reconciliation_tool.purchase_reconciliation_tool.resend_otp",
+ method: "india_compliance.gst_india.utils.gstr.gstr.request_otp",
args: { company_gstin },
callback: function () {
frappe.show_alert({
@@ -325,6 +325,48 @@ Object.assign(india_compliance, {
.find(`.dropdown-item[data-label="${encodeURIComponent(btn_name)}"]`)
.addClass("text-danger");
},
+
+ async authenticate_company_gstins(company, company_gstin) {
+ const { message: gstin_authentication_status } = await frappe.call({
+ method: "india_compliance.gst_india.utils.gstr.gstr.validate_company_gstins",
+ args: { company: company, company_gstin: company_gstin },
+ });
+
+ for (let gstin of Object.keys(gstin_authentication_status)) {
+ if (gstin_authentication_status[gstin]) continue;
+
+ gstin_authentication_status[gstin] = await this.authenticate_otp(gstin);
+ }
+
+ return Object.keys(gstin_authentication_status);
+ },
+
+ async authenticate_otp(gstin) {
+ await frappe.call({
+ method: "india_compliance.gst_india.utils.gstr.gstr.request_otp",
+ args: { company_gstin: gstin },
+ });
+
+ let error_type = "otp_requested";
+ let is_authenticated = false;
+
+ while (!is_authenticated) {
+ const otp = await this.get_gstin_otp(error_type, gstin);
+
+ const { message } = await frappe.call({
+ method: "india_compliance.gst_india.utils.gstr.gstr.authenticate_otp",
+ args: { company_gstin: gstin, otp: otp },
+ });
+
+ if (message && ["otp_requested", "invalid_otp"].includes(message.error_type)) {
+ error_type = message.error_type;
+ continue;
+ }
+
+ is_authenticated = true;
+ return true;
+ }
+ }
});
function is_gstin_check_digit_valid(gstin) {
|