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) {