Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions rohit_common/before_migrate_patches.py
Original file line number Diff line number Diff line change
@@ -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():
Expand Down
119 changes: 119 additions & 0 deletions rohit_common/patches/run_unwanted_patches.py
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -287,7 +301,9 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}
95 changes: 92 additions & 3 deletions rohit_common/rohit_common/india_gst_api/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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])
Expand Down Expand Up @@ -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)}
44 changes: 39 additions & 5 deletions rohit_common/rohit_common/india_gst_api/gst_public_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down