Add lightweight GST operator agent for portal automation
This commit is contained in:
@@ -13,7 +13,7 @@ from urllib.parse import urlencode
|
||||
|
||||
import jwt
|
||||
from fastapi import APIRouter, File, Form, Request, UploadFile
|
||||
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
|
||||
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse, StreamingResponse
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.db.common import CommonSessionLocal
|
||||
@@ -144,9 +144,12 @@ def extract_gstr3b_itc(data) -> dict[str, float]:
|
||||
return totals
|
||||
|
||||
router = APIRouter(prefix="/tools/accounting/gst-reconciliation", tags=["accounting-gst-reconciliation-ui"])
|
||||
_TOKEN_PURPOSE = "gst_operator_browser_v1"
|
||||
_TOKEN_MINUTES = 30
|
||||
_TOKEN_PURPOSE = "gst_lightweight_operator_v2"
|
||||
_TOKEN_MINUTES = 15
|
||||
_UPLOAD_ROOT = Path(tempfile.gettempdir()) / "audit_firm_gst_operator_uploads"
|
||||
_OPERATOR_AGENT_VERSION = "1.0.0"
|
||||
_OPERATOR_AGENT_PORT = 8791
|
||||
_OPERATOR_RUNTIME_ROOT = Path(__file__).resolve().parent / "gst_operator_agent_runtime"
|
||||
|
||||
|
||||
def _fy_bounds(fy: str) -> tuple[date, date]:
|
||||
@@ -410,12 +413,35 @@ def page(request: Request, client_id: int | None = None, registration_id: int |
|
||||
return templates.TemplateResponse("modules/accounting/templates/accounting/gst_reconciliation.html",{
|
||||
"request":request,"current_user":user,"current_user_roles":get_user_roles(db,user.id),"current_user_permissions":get_user_permissions(db,user.id),"csrf_token":get_or_create_csrf_token(request),
|
||||
"clients":clients,"selected_client":selected,"registrations":registrations,"selected_registration":selected_reg,"credentials":credentials,"node":node,"node_online":_node_online(node) if node else False,
|
||||
"period":period,"financial_year":financial_year,"download_mode":download_mode,"operator_job":operator_job,"gst_login_url":GST_LOGIN_URL,"message":message,"error":error,"title":"GST Return Reconciliation",
|
||||
"period":period,"financial_year":financial_year,"download_mode":download_mode,"operator_job":operator_job,"gst_login_url":GST_LOGIN_URL,
|
||||
"operator_agent_version":_OPERATOR_AGENT_VERSION,"operator_agent_port":_OPERATOR_AGENT_PORT,
|
||||
"message":message,"error":error,"title":"GST Return Reconciliation",
|
||||
})
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/operator-agent/download")
|
||||
def download_operator_agent(request: Request):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_partner(request, db, "accounting.learning.view")
|
||||
if response:
|
||||
return response
|
||||
if not _OPERATOR_RUNTIME_ROOT.is_dir():
|
||||
return JSONResponse({"ok": False, "error": "GST Operator Agent runtime is missing from this ERP build."}, status_code=404)
|
||||
memory = io.BytesIO()
|
||||
with zipfile.ZipFile(memory, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as archive:
|
||||
for path in sorted(_OPERATOR_RUNTIME_ROOT.rglob("*")):
|
||||
if path.is_file() and "__pycache__" not in path.parts:
|
||||
archive.write(path, Path("ARRR_GST_Operator_Agent") / path.relative_to(_OPERATOR_RUNTIME_ROOT))
|
||||
memory.seek(0)
|
||||
headers = {"Content-Disposition": f'attachment; filename="ARRR_GST_Operator_Agent_{_OPERATOR_AGENT_VERSION}.zip"'}
|
||||
return StreamingResponse(memory, media_type="application/zip", headers=headers)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/download/start")
|
||||
def start_download(
|
||||
request: Request,
|
||||
@@ -459,7 +485,7 @@ def start_download(
|
||||
log_access(db,request,user,cred,"use_for_gst_download",reason=f"GST returns {financial_year}: {','.join(return_types)}",fields="username,secret",success=True)
|
||||
db.commit()
|
||||
request.session["gst_operator_job"]={"token":token,"job_id":jti,"periods":periods,"return_types":return_types}
|
||||
return _redirect(client_id,registration_id=registration_id,period=period,financial_year=financial_year,download_mode=download_mode,message="GST download prepared. The ERP will now ask the Local Agent on this computer to open the visible GST browser; CAPTCHA/OTP will appear here. Completed files will be transferred to the configured client local storage.")
|
||||
return _redirect(client_id,registration_id=registration_id,period=period,financial_year=financial_year,download_mode=download_mode,message="GST download prepared. The lightweight GST Operator Agent on this computer will open the visible GST browser and autofill the selected Credential Vault login. Complete CAPTCHA/OTP there; downloaded return data will then be transferred to the configured client storage.")
|
||||
except Exception as exc:
|
||||
db.rollback(); return _redirect(client_id,registration_id=registration_id,period=period,financial_year=financial_year,download_mode=download_mode,error=str(exc))
|
||||
finally:
|
||||
@@ -482,7 +508,7 @@ async def operator_redeem(request: Request):
|
||||
raise ValueError("GST username/password is missing in Credential Vault.")
|
||||
finally:
|
||||
db.close()
|
||||
return JSONResponse({"ok":True,"payload":{**{k:v for k,v in data.items() if k not in {"iat","exp","purpose"}},"username":username,"password":password,"upload_url":str(get_settings().ERP_PUBLIC_BASE_URL).rstrip("/")+"/tools/accounting/gst-reconciliation/operator/upload","login_timeout_seconds":900}})
|
||||
return JSONResponse({"ok":True,"payload":{**{k:v for k,v in data.items() if k not in {"iat","exp","purpose"}},"username":username,"password":password,"upload_url":str(get_settings().ERP_PUBLIC_BASE_URL).rstrip("/")+"/tools/accounting/gst-reconciliation/operator/upload","login_timeout_seconds":900,"operator_agent_min_version":_OPERATOR_AGENT_VERSION}})
|
||||
except Exception as exc:
|
||||
return JSONResponse({"ok":False,"error":str(exc)},status_code=400)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user