diff --git a/rohit_common/before_migrate_patches.py b/rohit_common/before_migrate_patches.py index b001204..ba30312 100644 --- a/rohit_common/before_migrate_patches.py +++ b/rohit_common/before_migrate_patches.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- +from rohit_common.patches.run_unwanted_patches import run_unwanted_patches import frappe import erpnext def execute (): + run_unwanted_patches() add_default_company_fy() def add_default_company_fy(): diff --git a/rohit_common/patches/run_unwanted_patches.py b/rohit_common/patches/run_unwanted_patches.py new file mode 100644 index 0000000..55fe417 --- /dev/null +++ b/rohit_common/patches/run_unwanted_patches.py @@ -0,0 +1,119 @@ +import frappe + +def run_unwanted_patches(): + if not frappe.db.exists("Patch Log", {"patch": "frappe.patches.v16.running_unwanted_patches"}): + #frappe + set_route_for_blog_category() + set_read_times() + update_icons_in_customized_desk_pages() + rename_desk_page_to_workspace() + setup_likes_from_feedback() + + #erpnext + update_is_cancelled_field() + change_is_subcontracted_fieldtype() + rename_account_type_doctype() + execute_rename_desk_page() + replace_pos_page_with_point_of_sale_page() + print_uom_after_quantity_patch() + update_member_email_address() + clear_reconciliation_values_from_singles() + execute_rename_tds_report() + + running_unwanted_patches() +def running_unwanted_patches(): + frappe.get_doc({ + 'doctype': 'Patch Log', + 'patch': 'frappe.patches.v16.running_unwanted_patches', + }).insert(ignore_permissions=True) + frappe.db.commit() +def setup_likes_from_feedback(): + frappe.get_doc({ + 'doctype': 'Patch Log', + 'patch': 'frappe.patches.v14_0.setup_likes_from_feedback', + }).insert(ignore_permissions=True) + frappe.db.commit() + +def rename_desk_page_to_workspace(): + frappe.get_doc({ + 'doctype': 'Patch Log', + 'patch': 'frappe.patches.v13_0.rename_desk_page_to_workspace # 02.02.2021', + }).insert(ignore_permissions=True) + frappe.db.commit() +def update_icons_in_customized_desk_pages(): + frappe.get_doc({ + 'doctype': 'Patch Log', + 'patch': 'frappe.patches.v13_0.update_icons_in_customized_desk_pages', + }).insert(ignore_permissions=True) + frappe.db.commit() + +def set_route_for_blog_category(): + frappe.get_doc({ + 'doctype': 'Patch Log', + 'patch': 'frappe.patches.v13_0.set_route_for_blog_category', + }).insert(ignore_permissions=True) + frappe.db.commit() + +def set_read_times(): + frappe.get_doc({ + 'doctype': 'Patch Log', + 'patch': 'frappe.patches.v13_0.set_read_times', + }).insert(ignore_permissions=True) + frappe.db.commit() +def update_is_cancelled_field(): + frappe.get_doc({ + 'doctype': 'Patch Log', + 'patch': 'erpnext.patches.v12_0.update_is_cancelled_field', + }).insert(ignore_permissions=True) + frappe.db.commit() +def change_is_subcontracted_fieldtype(): + frappe.get_doc({ + 'doctype': 'Patch Log', + 'patch': 'erpnext.patches.v14_0.change_is_subcontracted_fieldtype', + }).insert(ignore_permissions=True) + frappe.db.commit() +def rename_account_type_doctype(): + frappe.get_doc({ + 'doctype': 'Patch Log', + 'patch': 'erpnext.patches.v12_0.rename_account_type_doctype', + }).insert(ignore_permissions=True) + frappe.db.commit() + +def execute_rename_desk_page(): + frappe.get_doc({ + 'doctype': 'Patch Log', + 'patch': 'execute:frappe.rename_doc("Desk Page", "Getting Started", "Home", force=True)', + }).insert(ignore_permissions=True) + frappe.db.commit() + +def replace_pos_page_with_point_of_sale_page(): + frappe.get_doc({ + 'doctype': 'Patch Log', + 'patch': 'erpnext.patches.v13_0.replace_pos_page_with_point_of_sale_page', + }).insert(ignore_permissions=True) + frappe.db.commit() + +def print_uom_after_quantity_patch(): + frappe.get_doc({ + 'doctype': 'Patch Log', + 'patch': 'erpnext.patches.v13_0.print_uom_after_quantity_patch', + }).insert(ignore_permissions=True) + frappe.db.commit() +def update_member_email_address(): + frappe.get_doc({ + 'doctype': 'Patch Log', + 'patch': 'erpnext.patches.v13_0.update_member_email_address', + }).insert(ignore_permissions=True) + frappe.db.commit() +def clear_reconciliation_values_from_singles(): + frappe.get_doc({ + 'doctype': 'Patch Log', + 'patch': 'erpnext.patches.v14_0.clear_reconciliation_values_from_singles', + }).insert(ignore_permissions=True) + frappe.db.commit() +def execute_rename_tds_report(): + frappe.get_doc({ + 'doctype': 'Patch Log', + 'patch': 'execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Details", force=True)', + }).insert(ignore_permissions=True) + frappe.db.commit() diff --git a/rohit_common/rohit_common/doctype/gst_return_status/gst_return_status.py b/rohit_common/rohit_common/doctype/gst_return_status/gst_return_status.py index 12beee4..f4da4cd 100644 --- a/rohit_common/rohit_common/doctype/gst_return_status/gst_return_status.py +++ b/rohit_common/rohit_common/doctype/gst_return_status/gst_return_status.py @@ -23,17 +23,16 @@ def get_return_status(self): frappe.throw('Selected FY {} is before the GST Era'.format(tup[0])) elif tup[1] > today.date(): frappe.throw('Selected FY {} has not Even Started'.format(tup[0])) + # try: response = track_return(self.gstin, self.fiscal_year) + # except Exception as e: + frappe.throw(f"Error in GST API session or DSC: {str(response)}") efiled_list = response.get('EFiledlist') - # frappe.throw(str(efiled_list)) if efiled_list: self.json_reply = str(efiled_list) for d in efiled_list: temp_dict = frappe._dict({}) - if d.get('valid') == 'Y': - temp_dict['valid_gst_return'] = 'Yes' - else: - temp_dict['valid_gst_return'] = 'No' + temp_dict['valid_gst_return'] = 'Yes' if d.get('valid') == 'Y' else 'No' temp_dict['mode_of_filing'] = d.get('mof') temp_dict['date_of_filing'] = (datetime.strptime(d.get('dof'), '%d-%m-%Y')).date() temp_dict['return_period'] = d.get('ret_prd') diff --git a/rohit_common/rohit_common/doctype/rohit_settings/rohit_settings.json b/rohit_common/rohit_common/doctype/rohit_settings/rohit_settings.json index 8d8a240..998602f 100644 --- a/rohit_common/rohit_common/doctype/rohit_settings/rohit_settings.json +++ b/rohit_common/rohit_common/doctype/rohit_settings/rohit_settings.json @@ -1,10 +1,13 @@ { + "actions": [], "creation": "2020-09-23 15:55:42.979487", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ "sb0", + "dsc_pfx_path", + "dsc_password", "tax_pro_asp_id", "tax_pro_password", "gstin", @@ -51,6 +54,16 @@ "fieldtype": "Section Break", "label": "Tax Pro GSP" }, + { + "fieldname": "dsc_pfx_path", + "fieldtype": "Attach", + "label": "DSC PFX File Path" + }, + { + "fieldname": "dsc_password", + "fieldtype": "Data", + "label": "DSC Password" + }, { "fieldname": "tax_pro_asp_id", "fieldtype": "Data", @@ -269,7 +282,8 @@ } ], "issingle": 1, - "modified": "2022-08-08 09:37:52.222305", + "links": [], + "modified": "2025-09-16 12:33:40.529343", "modified_by": "Administrator", "module": "rohit_common", "name": "Rohit Settings", @@ -287,7 +301,9 @@ } ], "quick_entry": 1, + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/rohit_common/rohit_common/india_gst_api/common.py b/rohit_common/rohit_common/india_gst_api/common.py index 09f9e4d..3776419 100644 --- a/rohit_common/rohit_common/india_gst_api/common.py +++ b/rohit_common/rohit_common/india_gst_api/common.py @@ -4,6 +4,11 @@ import frappe from datetime import datetime from frappe.utils import flt, get_last_day, getdate +import base64 +import requests +from cryptography.hazmat.primitives.serialization import pkcs12 +from cryptography.hazmat.primitives.asymmetric import padding as asym_padding +from cryptography.hazmat.primitives import hashes def get_place_of_supply(dtype, dname): @@ -154,14 +159,33 @@ def get_gsp_details(api, action, gstin=None, api_type=None): sandbox = rset.sandbox_mode gsp_link = rset.api_link asp_id = rset.tax_pro_asp_id - asp_pass = rset.tax_pro_password + asp_pass = rset.tax_pro_asp_secret + dsc_pfx_path = get_file_full_path(getattr(rset, 'dsc_pfx_path', None)) + dsc_password = getattr(rset, 'dsc_password', None) + if not dsc_pfx_path or not dsc_password: + frappe.throw("DSC PFX path or password not set in Rohit Settings") + txn_id = str(datetime.now().timestamp()).replace('.', '') + ip_usr = "127.0.0.1" # or fetch from settings if needed + result = call_getkey_api( + aspid=asp_id, + asp_password=asp_pass, + txn=txn_id, + pfx_file=dsc_pfx_path, + dsc_password=dsc_password, + ip_usr=ip_usr, + sandbox=sandbox + ) + session_id = result.get("asp_session_id") + asp_ek = result.get("enc_key") + # frappe.throw(f"Debug Info: {result}") + if not session_id: + frappe.throw(f"Session ID could not be generated: {result.get('error', 'Unknown error')}") if api == 'eway': gsp_link = gsp_link[:8] + "einvapi." + gsp_link[8:] else: gsp_link = gsp_link[:8] + "gstapi." + gsp_link[8:] - if not gstin: if api_type == 'common': gstin = rset.gstin @@ -180,9 +204,17 @@ def get_gsp_details(api, action, gstin=None, api_type=None): gsp_link = gsp_sandbox_link if api_type == 'common': gsp_link = gsp_link + api_url + 'aspid=' + asp_id + '&password=' + asp_pass + '&Action=' + action + print(gsp_link, asp_id, asp_pass, gstin, sandbox, session_id, asp_ek) + # frappe.throw(f"Debug Info: {gsp_link}, {asp_id}, {asp_pass}, {gstin}, {sandbox}, {session_id}, {asp_ek}") + # gsp_link, asp_id, asp_pass, caller_gstin, sandbox, session_id, asp_ek + return gsp_link, asp_id, asp_pass, gstin, sandbox, session_id, asp_ek - return gsp_link, asp_id, asp_pass, gstin, sandbox +def get_file_full_path(file): + if "private" not in file: + return frappe.get_site_path() + "/public" + file # noqa: 501 + else: + return frappe.get_site_path() + file def gst_return_period_validation(return_period): month = flt(return_period[:2]) @@ -215,3 +247,60 @@ def get_dates_from_return_period(monthly_ret_pd): def validate_gstin(gstin): if len(gstin) != 15: frappe.throw(f"GST Number: {gstin} Should be of 15 Characters") + +def call_getkey_api(aspid, asp_password, txn, pfx_file, dsc_password, ip_usr, sandbox): + """ + Calls the GST GSP GetKey API to generate session_id using DSC. + Returns a dict with asp_session_id and enc_key (and error if any). + """ + # Load DSC and sign content + try: + with open(pfx_file, 'rb') as f: + pfx_data = f.read() + private_key, certificate, _ = pkcs12.load_key_and_certificates( + pfx_data, + password=dsc_password.encode() if dsc_password else None + ) + if certificate is None or private_key is None: + return {"error": "Certificate not found"} + subject = certificate.subject.rfc4514_string() + if not any(attr.startswith("OU=GST") for attr in subject.split(',')): + return {"error": "Certificate not found"} + timestamp = datetime.now().strftime("%d%m%Y%H%M%S%f")[:20] + content_to_sign = aspid + timestamp + signature = private_key.sign( + content_to_sign.encode("utf-8"), + asym_padding.PKCS1v15(), + hashes.SHA256() + ) + signed_content = base64.b64encode(signature).decode('utf-8') + except Exception as e: + return {"error": str(e)} + + url = "https://gstsandbox.charteredinfo.com/aspapi/v1.0/getKey" if sandbox else "https://gstapi.charteredinfo.com/aspapi/v1.0/getKey" + headers = { + "aspid": aspid, + "txn": txn, + "Content-Type": "application/json; charset=utf-8", + "ip-usr": ip_usr + } + payload = { + "timestamp": timestamp, + "signed_content": signed_content + } + try: + response = requests.post(url, json=payload, headers=headers, timeout=15) + if response.status_code != 200: + return {"error": f"HTTP {response.status_code}", "details": response.text} + data = response.json() + if data.get("status_cd") != "1": + return {"error": "API call failed", "message": data.get("message")} + return { + "asp_session_id": data.get("session_id"), + "enc_key": data.get("enc_key"), + "validity_min": data.get("validity_min"), + "txn": data.get("txn"), + "raw_response": data + } + except Exception as e: + return {"error": str(e)} \ No newline at end of file diff --git a/rohit_common/rohit_common/india_gst_api/gst_public_api.py b/rohit_common/rohit_common/india_gst_api/gst_public_api.py index 07480e1..ce2ef26 100644 --- a/rohit_common/rohit_common/india_gst_api/gst_public_api.py +++ b/rohit_common/rohit_common/india_gst_api/gst_public_api.py @@ -23,18 +23,52 @@ def search_gstin(gstin=None): def track_return(gstin, fiscal_year, type_of_return=None): + # Debug logging for asp_secret value + # Debug logging for encryption diagnostics (mask sensitive info) # (fiscal_year, start_date, end_date) = get_fiscal_year(for_date) fy_format = fiscal_year[:5] + fiscal_year[7:] - gsp_link, asp_id, asp_pass, caller_gstin, sandbox = get_gsp_details(api_type="common", action='RETTRACK', - api="returns") + gsp_link, asp_id, asp_pass, caller_gstin, sandbox, session_id, asp_ek = get_gsp_details(api_type="common", action='RETTRACK', api="returns") + print(f"[GSTAPI DEBUG] tax_pro_asp_secret (len={len(asp_pass)}): {asp_pass[:8]}...{asp_pass[-8:]}") + if not session_id: + frappe.throw("Session ID could not be generated. Please check DSC and API credentials.") + # Encrypt asp_pass using AspEK (asp_ek) as per TaxPro GSP requirements + from Crypto.Cipher import AES + from Crypto.Util.Padding import pad + import base64 + + # Decode asp_ek and ensure it's 32 bytes for AES-256 + key_bytes = base64.b64decode(asp_ek) + if len(key_bytes) > 32: + key_bytes = key_bytes[:32] + elif len(key_bytes) < 32: + # Pad key to 32 bytes if needed (shouldn't happen, but for safety) + key_bytes = key_bytes.ljust(32, b'\0') + cipher = AES.new(key_bytes, AES.MODE_ECB) + # Base64 decode asp_pass (Asp Secret Key) before encryption, as per TaxPro GSP sample + payload = base64.b64decode(asp_pass) + padded = pad(payload, AES.block_size) + encrypted = cipher.encrypt(padded) + asp_secret_encrypted = base64.b64encode(encrypted).decode('utf-8') + print(f"[GSTAPI DEBUG] asp_ek (len={len(asp_ek)}): {asp_ek[:8]}...{asp_ek[-8:]}") + print(f"[GSTAPI DEBUG] asp_pass (len={len(asp_pass)}): {asp_pass[:2]}...{asp_pass[-2:]}") + print(f"[GSTAPI DEBUG] asp_secret_encrypted (len={len(asp_secret_encrypted)}): {asp_secret_encrypted[:8]}...{asp_secret_encrypted[-8:]}") + if type_of_return: full_url = gsp_link + '&Gstin=' + gstin + '&FY=' + fy_format + '&type=' + type_of_return else: full_url = gsp_link + '&Gstin=' + gstin + '&FY=' + fy_format - response = requests.get(url=full_url, timeout=timeout) - # frappe.throw(str(response.text)) + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "asp-secret": asp_secret_encrypted, + "session-id": session_id, + "aspid": asp_id, + "txn": str(datetime.datetime.now().timestamp()).replace('.', ''), + "GSTIN": gstin , + "ip-usr": "127.0.0.1" + } + response = requests.get(url=full_url, headers=headers, timeout=timeout) json_response = response.json() - # frappe.throw(str(json_response)) return json_response