diff --git a/app/modules/accounting/gst_operator_agent_runtime/README.txt b/app/modules/accounting/gst_operator_agent_runtime/README.txt index f894485..8a5fc23 100644 --- a/app/modules/accounting/gst_operator_agent_runtime/README.txt +++ b/app/modules/accounting/gst_operator_agent_runtime/README.txt @@ -1,45 +1,57 @@ -ARRR GST Operator Agent 1.5.1 +ARRR GST Operator Agent 1.6.0 +================================ -Purpose -------- -Runs only on the GST operator workstation. It opens visible Chrome/Edge, redeems a short-lived GST job token from ARRR ERP, fills the selected Credential Vault username/password, waits for manual CAPTCHA/OTP, downloads selected GST return data, and uploads the package back to ERP for transfer to the configured Storage Agent. +Primary deployment: self-installing Windows EXE. -Architecture +The Windows setup EXE is built from gst_operator_agent.py using: + + powershell -ExecutionPolicy Bypass -File .\build_gst_operator_agent_exe.ps1 + +Generated file: + + dist\ARRR_GST_Operator_Agent_Setup_1.6.0.exe + +Operator workstation installation +--------------------------------- +1. Download ARRR_GST_Operator_Agent_Setup_1.6.0.exe from ARRR ERP. +2. Double-click the EXE. +3. The EXE copies itself to: + + %LOCALAPPDATA%\ARRR\GSTOperatorAgent\ARRR_GST_Operator_Agent.exe + +4. It writes config.json for https://office.arrrassociates.com. +5. It registers the current-user Windows protocol: + + arrrgst:// + +6. It creates a normal Windows Installed Apps uninstall registration. +7. Return to GST Return Reconciliation and start the GST download. + +No administrator rights are required. +Python is not required on operator workstations. +No localhost web server or trusted local certificate is used. + +Runtime flow ------------ -ARRR ERP starts the agent through the Windows custom URL protocol arrrgst://. No browser-to-localhost HTTP/HTTPS request is used. No localhost TLS certificate, local web server, Tally access, or client storage access is required on the operator workstation. +ARRR ERP issues a short-lived signed GST operator token. +Windows opens arrrgst://start?token=... +The installed EXE redeems the token over HTTPS. +GST Portal username/password are retrieved from ERP Credential Vault only after token redemption. +The visible Chrome/Edge GST browser is opened by the operator agent. +The operator completes CAPTCHA/OTP. +The agent downloads the selected GST return JSON/ZIP files and uploads the verified package back to ARRR ERP for client storage. -Installation ------------- -Run install_gst_operator_agent.ps1 as the Windows user who will operate GST downloads. Administrator rights are not required. Python 3.11+ and installed Chrome or Edge are required. +Source/repair fallback +---------------------- +The ERP still exposes a source ZIP for development/repair. The legacy PowerShell source installer remains supported and preserves the previous behavior, but the EXE is the normal workstation installation method from version 1.6.0 onward. -Browser prompt --------------- -The first time ARRR ERP opens arrrgst://, Chrome/Edge may ask whether to open ARRR GST Operator Agent. Choose Open/Allow. - -Security +Commands -------- -The custom URL contains only a short-lived signed job token. GST username/password are redeemed by the local agent directly from ARRR ERP and are never inserted in ERP page HTML or in the custom URL. CAPTCHA/OTP remains manual. +Self-test: + ARRR_GST_Operator_Agent.exe --self-test -Uninstall ---------- -Run uninstall_gst_operator_agent.ps1. It removes the protocol registration and also cleans legacy v1.0.x localhost listener/startup/certificate state. +Repair/install explicitly: + ARRR_GST_Operator_Agent.exe --install --erp-base-url https://office.arrrassociates.com - -V1.5.1 OCTAGST-STYLE RETURN-DASHBOARD EXTRACTION -- Preserves the v1.4.0 Credential Vault login, manual CAPTCHA/OTP, natural Welcome -> Return Dashboard flow, ERP upload and failure diagnostics. -- GSTR-2B no longer transfers the browser to gstr2b.gst.gov.in. -- GSTR-2B is requested from the authenticated return.gst.gov.in Return Dashboard using the GST offline-download API pattern used by the supplied OctaGST extension. -- The agent follows GST-provided download URLs, saves returned ZIP/JSON files, and safely extracts JSON members locally. -- Same-origin GSTR-1 and GSTR-3B API calls remain unchanged. -- No automatic GST logout is performed. -- A job is completed only after every selected period/return has a saved result and ERP/client-storage upload is confirmed. -- Manual JSON/ZIP import remains available in ERP as fallback. - - -V1.5.1 NATURAL MONTHLY RETURN FLOW + GSTR-2B PENDING POLL --------------------------------------------------------- -- Restores robust Aadhaar/E-KYC Remind me later / Later dismissal. -- GSTR-2B generation-accepted responses are PENDING, not failures; polls up to 25 minutes. -- GSTR-1: selects FY/Quarter/Month on Return Dashboard, clicks Search, opens the actual GSTR-1 card, then performs offline generation/download from the initialized page context. -- GSTR-3B: selects FY/Quarter/Month, clicks Search, opens the actual GSTR-3B card, then reads summary/tax-payable from the initialized return page context. -- Existing Credential Vault, custom protocol, login/CAPTCHA, manual fallback, ERP upload and Storage Agent flow are unchanged. +Uninstall: + ARRR_GST_Operator_Agent.exe --uninstall diff --git a/app/modules/accounting/gst_operator_agent_runtime/build_gst_operator_agent_exe.ps1 b/app/modules/accounting/gst_operator_agent_runtime/build_gst_operator_agent_exe.ps1 new file mode 100644 index 0000000..2bb9d01 --- /dev/null +++ b/app/modules/accounting/gst_operator_agent_runtime/build_gst_operator_agent_exe.ps1 @@ -0,0 +1,84 @@ +param( + [string]$PythonCommand = "", + [switch]$KeepBuildEnvironment +) + +$ErrorActionPreference = "Stop" +$Root = Split-Path -Parent $MyInvocation.MyCommand.Path +$Source = Join-Path $Root "gst_operator_agent.py" +$Requirements = Join-Path $Root "requirements.txt" +$BuildRoot = Join-Path $env:TEMP "ARRR_GST_AGENT_160_BUILD" +$BuildVenv = Join-Path $BuildRoot "venv" +$Work = Join-Path $BuildRoot "work" +$Spec = Join-Path $BuildRoot "spec" +$Dist = Join-Path $Root "dist" +$ExeName = "ARRR_GST_Operator_Agent_Setup_1.6.0.exe" +$Exe = Join-Path $Dist $ExeName + +if (-not (Test-Path $Source)) { throw "Missing GST agent source: $Source" } +if (-not (Test-Path $Requirements)) { throw "Missing GST agent requirements: $Requirements" } + +if ($PythonCommand) { + $Python = $PythonCommand + $PythonPrefix = @() +} elseif (Get-Command py -ErrorAction SilentlyContinue) { + $Python = "py" + $PythonPrefix = @("-3") +} elseif (Get-Command python -ErrorAction SilentlyContinue) { + $Python = "python" + $PythonPrefix = @() +} else { + throw "Python 3 was not found. Install Python 3.11+ on the development workstation and run this builder again." +} + +Write-Host "ARRR GST Operator Agent EXE Builder 1.6.0" -ForegroundColor Cyan +Write-Host "Source : $Source" +Write-Host "Output : $Exe" + +if (Test-Path $BuildRoot) { Remove-Item $BuildRoot -Recurse -Force } +New-Item -ItemType Directory -Path $BuildRoot -Force | Out-Null +New-Item -ItemType Directory -Path $Dist -Force | Out-Null + +& $Python @PythonPrefix -m venv $BuildVenv +if ($LASTEXITCODE -ne 0) { throw "Could not create the isolated GST EXE build environment." } + +$BuildPython = Join-Path $BuildVenv "Scripts\python.exe" +& $BuildPython -m pip install --disable-pip-version-check -r $Requirements "pyinstaller==6.16.0" +if ($LASTEXITCODE -ne 0) { throw "Could not install GST EXE build dependencies." } + +& $BuildPython -m PyInstaller ` + --noconfirm ` + --clean ` + --onefile ` + --noconsole ` + --name "ARRR_GST_Operator_Agent_Setup_1.6.0" ` + --distpath $Dist ` + --workpath $Work ` + --specpath $Spec ` + --collect-all playwright ` + --hidden-import winreg ` + $Source + +if ($LASTEXITCODE -ne 0 -or -not (Test-Path $Exe)) { + throw "PyInstaller did not create $ExeName." +} + +Write-Host "Running packaged self-test..." -ForegroundColor Yellow +& $Exe --self-test +if ($LASTEXITCODE -ne 0) { throw "Packaged GST Operator Agent self-test failed." } + +$Hash = (Get-FileHash -Algorithm SHA256 -Path $Exe).Hash +[System.IO.File]::WriteAllText( + (Join-Path $Dist "ARRR_GST_Operator_Agent_Setup_1.6.0.sha256"), + "$Hash $ExeName`r`n", + [System.Text.UTF8Encoding]::new($false) +) + +if (-not $KeepBuildEnvironment) { + Remove-Item $BuildRoot -Recurse -Force -ErrorAction SilentlyContinue +} + +Write-Host "" +Write-Host "GST Operator Agent EXE created successfully." -ForegroundColor Green +Write-Host "EXE : $Exe" -ForegroundColor Green +Write-Host "SHA256 : $Hash" -ForegroundColor Green diff --git a/app/modules/accounting/gst_operator_agent_runtime/dist/ARRR_GST_Operator_Agent_Setup_1.6.0.exe b/app/modules/accounting/gst_operator_agent_runtime/dist/ARRR_GST_Operator_Agent_Setup_1.6.0.exe new file mode 100644 index 0000000..e7cb447 Binary files /dev/null and b/app/modules/accounting/gst_operator_agent_runtime/dist/ARRR_GST_Operator_Agent_Setup_1.6.0.exe differ diff --git a/app/modules/accounting/gst_operator_agent_runtime/dist/ARRR_GST_Operator_Agent_Setup_1.6.0.sha256 b/app/modules/accounting/gst_operator_agent_runtime/dist/ARRR_GST_Operator_Agent_Setup_1.6.0.sha256 new file mode 100644 index 0000000..557661c --- /dev/null +++ b/app/modules/accounting/gst_operator_agent_runtime/dist/ARRR_GST_Operator_Agent_Setup_1.6.0.sha256 @@ -0,0 +1 @@ +AC2EA1EF8F5D066181E4CD500E579D3796A1AAC2BDC28110404CDBEB86B54AA1 ARRR_GST_Operator_Agent_Setup_1.6.0.exe diff --git a/app/modules/accounting/gst_operator_agent_runtime/gst_operator_agent.py b/app/modules/accounting/gst_operator_agent_runtime/gst_operator_agent.py index db992c3..936c021 100644 --- a/app/modules/accounting/gst_operator_agent_runtime/gst_operator_agent.py +++ b/app/modules/accounting/gst_operator_agent_runtime/gst_operator_agent.py @@ -2,8 +2,10 @@ from __future__ import annotations import io import json +import os import re import shutil +import subprocess import sys import time import traceback @@ -14,10 +16,16 @@ from urllib.parse import parse_qs, urljoin, urlsplit import requests -VERSION = "1.5.1" -ROOT = Path(__file__).resolve().parent -DATA = ROOT / "data" -CONFIG_PATH = ROOT / "config.json" +VERSION = "1.6.0" +APP_NAME = "ARRR GST Operator Agent" +PROTOCOL_NAME = "arrrgst" +DEFAULT_ERP_BASE_URL = "https://office.arrrassociates.com" +SOURCE_ROOT = Path(__file__).resolve().parent +INSTALL_ROOT = Path(os.environ.get("LOCALAPPDATA") or (Path.home() / "AppData" / "Local")) / "ARRR" / "GSTOperatorAgent" +RUN_ROOT = INSTALL_ROOT if getattr(sys, "frozen", False) else SOURCE_ROOT +DATA = INSTALL_ROOT / "data" if getattr(sys, "frozen", False) else SOURCE_ROOT / "data" +CONFIG_PATH = INSTALL_ROOT / "config.json" if getattr(sys, "frozen", False) else SOURCE_ROOT / "config.json" +INSTALLED_EXE = INSTALL_ROOT / "ARRR_GST_Operator_Agent.exe" GST_LOGIN_URL = "https://services.gst.gov.in/services/login" SERVICES_DASHBOARD_URL = "https://services.gst.gov.in/services/auth/dashboard" RETURN_DASHBOARD_URL = "https://return.gst.gov.in/returns/auth/dashboard" @@ -41,7 +49,7 @@ def read_config() -> dict: try: return json.loads(CONFIG_PATH.read_text(encoding="utf-8")) except Exception: - return {"erp_base_url": "https://office.arrrassociates.com", "protocol": "arrrgst"} + return {"erp_base_url": DEFAULT_ERP_BASE_URL, "protocol": PROTOCOL_NAME} def safe(value: str, fallback: str = "item") -> str: @@ -1205,6 +1213,142 @@ def handle_protocol_url(raw_url: str) -> None: run_job_foreground(token) + +def _message_box(title: str, message: str, error: bool = False) -> None: + try: + import ctypes + flags = 0x10 if error else 0x40 + ctypes.windll.user32.MessageBoxW(None, str(message), str(title), flags) + except Exception: + print(f"{title}: {message}") + + +def _write_config(erp_base_url: str) -> None: + url = str(erp_base_url or DEFAULT_ERP_BASE_URL).strip().rstrip("/") + if not url.startswith("https://"): + raise ValueError("ERP base URL must use HTTPS.") + INSTALL_ROOT.mkdir(parents=True, exist_ok=True) + payload = {"erp_base_url": url, "protocol": PROTOCOL_NAME, "version": VERSION} + CONFIG_PATH.write_text(json.dumps(payload, indent=2), encoding="utf-8") + + +def _register_protocol(exe_path: Path) -> None: + if os.name != "nt": + raise RuntimeError("Windows is required to register the ARRR GST protocol.") + import winreg + base = rf"Software\Classes\{PROTOCOL_NAME}" + with winreg.CreateKey(winreg.HKEY_CURRENT_USER, base) as key: + winreg.SetValueEx(key, "", 0, winreg.REG_SZ, f"URL:{APP_NAME}") + winreg.SetValueEx(key, "URL Protocol", 0, winreg.REG_SZ, "") + with winreg.CreateKey(winreg.HKEY_CURRENT_USER, base + r"\DefaultIcon") as key: + winreg.SetValueEx(key, "", 0, winreg.REG_SZ, f'"{exe_path}",0') + with winreg.CreateKey(winreg.HKEY_CURRENT_USER, base + r"\shell\open\command") as key: + winreg.SetValueEx(key, "", 0, winreg.REG_SZ, f'"{exe_path}" "%1"') + + +def _register_uninstall(exe_path: Path) -> None: + if os.name != "nt": + return + import winreg + base = r"Software\Microsoft\Windows\CurrentVersion\Uninstall\ARRRGSTOperatorAgent" + with winreg.CreateKey(winreg.HKEY_CURRENT_USER, base) as key: + values = { + "DisplayName": APP_NAME, + "DisplayVersion": VERSION, + "Publisher": "ARRR & Associates", + "InstallLocation": str(INSTALL_ROOT), + "DisplayIcon": str(exe_path), + "UninstallString": f'"{exe_path}" --uninstall', + "QuietUninstallString": f'"{exe_path}" --uninstall --quiet', + } + for name, value in values.items(): + winreg.SetValueEx(key, name, 0, winreg.REG_SZ, value) + winreg.SetValueEx(key, "NoModify", 0, winreg.REG_DWORD, 1) + winreg.SetValueEx(key, "NoRepair", 0, winreg.REG_DWORD, 0) + + +def _remove_registry_tree(root, subkey: str) -> None: + import winreg + try: + with winreg.OpenKey(root, subkey, 0, winreg.KEY_READ | winreg.KEY_WRITE) as key: + while True: + try: + child = winreg.EnumKey(key, 0) + except OSError: + break + _remove_registry_tree(root, subkey + "\\" + child) + winreg.DeleteKey(root, subkey) + except FileNotFoundError: + pass + + +def install_self(erp_base_url: str = DEFAULT_ERP_BASE_URL, quiet: bool = False) -> int: + if os.name != "nt": + raise RuntimeError("ARRR GST Operator Agent installer supports Windows only.") + if not getattr(sys, "frozen", False): + raise RuntimeError("Run the compiled ARRR GST Operator Agent Setup EXE for installation.") + INSTALL_ROOT.mkdir(parents=True, exist_ok=True) + source_exe = Path(sys.executable).resolve() + target_exe = INSTALLED_EXE.resolve() + if source_exe != target_exe: + shutil.copy2(source_exe, target_exe) + _write_config(erp_base_url) + _register_protocol(target_exe) + _register_uninstall(target_exe) + # Clean legacy Python/venv installer artifacts without touching job data. + for legacy_name in (".venv", "gst_operator_agent.py", "requirements.txt", "README.txt", "install_gst_operator_agent.ps1", "uninstall_gst_operator_agent.ps1"): + legacy = INSTALL_ROOT / legacy_name + try: + if legacy.is_dir(): + shutil.rmtree(legacy, ignore_errors=True) + elif legacy.exists(): + legacy.unlink() + except Exception: + pass + if not quiet: + _message_box(APP_NAME, f"{APP_NAME} {VERSION} installed successfully.\n\nProtocol: arrrgst://\nERP: {str(erp_base_url).rstrip('/')}\n\nReturn to ARRR ERP and start the GST download again.") + return 0 + + +def uninstall_self(quiet: bool = False) -> int: + if os.name != "nt": + return 0 + import winreg + try: + _remove_registry_tree(winreg.HKEY_CURRENT_USER, rf"Software\Classes\{PROTOCOL_NAME}") + _remove_registry_tree(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Uninstall\ARRRGSTOperatorAgent") + except Exception: + pass + exe = Path(sys.executable).resolve() + cleanup = INSTALL_ROOT / "remove_agent.cmd" + cleanup.write_text( + "@echo off\r\n" + "ping 127.0.0.1 -n 3 >nul\r\n" + f'del /f /q "{exe}" >nul 2>&1\r\n' + f'rmdir /s /q "{INSTALL_ROOT}" >nul 2>&1\r\n' + 'del /f /q "%~f0" >nul 2>&1\r\n', + encoding="utf-8", + ) + subprocess.Popen(["cmd.exe", "/c", str(cleanup)], creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0)) + if not quiet: + _message_box(APP_NAME, f"{APP_NAME} has been unregistered and will now be removed.") + return 0 + + +def installed_status() -> tuple[bool, str]: + if os.name != "nt": + return False, "Windows required" + try: + import winreg + key_path = rf"Software\Classes\{PROTOCOL_NAME}\shell\open\command" + with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path) as key: + command = str(winreg.QueryValueEx(key, "")[0] or "") + ok = INSTALLED_EXE.exists() and str(INSTALLED_EXE).lower() in command.lower() + return ok, command + except Exception as exc: + return False, str(exc) + + def self_test() -> int: cfg = read_config() erp = str(cfg.get("erp_base_url") or "") @@ -1224,18 +1368,43 @@ def self_test() -> int: def main(): DATA.mkdir(parents=True, exist_ok=True) - if len(sys.argv) >= 2 and sys.argv[1] == "--self-test": + args = list(sys.argv[1:]) + if "--self-test" in args: raise SystemExit(self_test()) - if len(sys.argv) >= 2 and str(sys.argv[1]).lower().startswith("arrrgst:"): + if "--uninstall" in args: + raise SystemExit(uninstall_self(quiet="--quiet" in args)) + if "--install" in args: + erp = DEFAULT_ERP_BASE_URL + if "--erp-base-url" in args: + idx = args.index("--erp-base-url") + if idx + 1 >= len(args): + raise SystemExit("--erp-base-url requires a value") + erp = args[idx + 1] + raise SystemExit(install_self(erp, quiet="--quiet" in args)) + if args and str(args[0]).lower().startswith("arrrgst:"): try: - handle_protocol_url(sys.argv[1]) + handle_protocol_url(args[0]) except Exception as exc: _log(traceback.format_exc()) _show_error(str(exc)) raise SystemExit(1) return + if getattr(sys, "frozen", False): + # A downloaded setup EXE installs/repairs itself when double-clicked. + if Path(sys.executable).resolve() != INSTALLED_EXE.resolve(): + raise SystemExit(install_self(DEFAULT_ERP_BASE_URL, quiet=False)) + ok, command = installed_status() + if not ok: + raise SystemExit(install_self(str(read_config().get("erp_base_url") or DEFAULT_ERP_BASE_URL), quiet=False)) + _message_box( + APP_NAME, + f"{APP_NAME} {VERSION} is installed and ready.\n\n" + f"ERP: {read_config().get('erp_base_url')}\n" + "Protocol: arrrgst://", + ) + return print(f"ARRR GST Operator Agent {VERSION}") - print("This agent is launched by the arrrgst:// Windows protocol from ARRR ERP.") + print("Source mode. Build the Windows setup EXE using build_gst_operator_agent_exe.ps1.") if __name__ == "__main__": diff --git a/app/modules/accounting/gst_operator_agent_runtime/install_gst_operator_agent.ps1 b/app/modules/accounting/gst_operator_agent_runtime/install_gst_operator_agent.ps1 index c18c388..ed746ee 100644 --- a/app/modules/accounting/gst_operator_agent_runtime/install_gst_operator_agent.ps1 +++ b/app/modules/accounting/gst_operator_agent_runtime/install_gst_operator_agent.ps1 @@ -9,7 +9,7 @@ $Startup = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Startup $ThumbprintFile = Join-Path $InstallRoot "cert_thumbprint.txt" $ProtocolKey = "HKCU:\Software\Classes\arrrgst" -Write-Host "ARRR GST Operator Agent 1.5.1 - clean installation" -ForegroundColor Cyan +Write-Host "ARRR GST Operator Agent 1.6.0 - source fallback installation" -ForegroundColor Cyan Write-Host "Install root: $InstallRoot" # Clean legacy v1.0.x listener/startup/certificate/protocol state first. diff --git a/app/modules/accounting/gst_operator_agent_runtime/uninstall_gst_operator_agent.ps1 b/app/modules/accounting/gst_operator_agent_runtime/uninstall_gst_operator_agent.ps1 index 6569bd3..e172584 100644 --- a/app/modules/accounting/gst_operator_agent_runtime/uninstall_gst_operator_agent.ps1 +++ b/app/modules/accounting/gst_operator_agent_runtime/uninstall_gst_operator_agent.ps1 @@ -1,9 +1,17 @@ $ErrorActionPreference = "Stop" $InstallRoot = Join-Path $env:LOCALAPPDATA "ARRR\GSTOperatorAgent" +$InstalledExe = Join-Path $InstallRoot "ARRR_GST_Operator_Agent.exe" $Startup = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Startup\ARRR GST Operator Agent.cmd" $ThumbprintFile = Join-Path $InstallRoot "cert_thumbprint.txt" $ProtocolKey = "HKCU:\Software\Classes\arrrgst" +$UninstallKey = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\ARRRGSTOperatorAgent" +if (Test-Path $InstalledExe) { + & $InstalledExe --uninstall + exit $LASTEXITCODE +} + +# Legacy source-agent cleanup. try { Get-NetTCPConnection -LocalPort 8791 -State Listen -ErrorAction SilentlyContinue | ForEach-Object { if ($_.OwningProcess) { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue } @@ -15,5 +23,6 @@ if (Test-Path $ThumbprintFile) { } Remove-Item $Startup -Force -ErrorAction SilentlyContinue Remove-Item $ProtocolKey -Recurse -Force -ErrorAction SilentlyContinue +Remove-Item $UninstallKey -Recurse -Force -ErrorAction SilentlyContinue Remove-Item $InstallRoot -Recurse -Force -ErrorAction SilentlyContinue -Write-Host "ARRR GST Operator Agent removed, including legacy localhost certificate/startup state." -ForegroundColor Green +Write-Host "ARRR GST Operator Agent removed, including legacy protocol/certificate/startup state." -ForegroundColor Green diff --git a/app/modules/accounting/gst_reconciliation_ui.py b/app/modules/accounting/gst_reconciliation_ui.py index 8287096..b83952f 100644 --- a/app/modules/accounting/gst_reconciliation_ui.py +++ b/app/modules/accounting/gst_reconciliation_ui.py @@ -147,9 +147,11 @@ router = APIRouter(prefix="/tools/accounting/gst-reconciliation", tags=["account _TOKEN_PURPOSE = "gst_lightweight_operator_v3" _TOKEN_MINUTES = 15 _UPLOAD_ROOT = Path(tempfile.gettempdir()) / "audit_firm_gst_operator_uploads" -_OPERATOR_AGENT_VERSION = "1.5.1" +_OPERATOR_AGENT_VERSION = "1.6.0" _OPERATOR_RUNTIME_ROOT = Path(__file__).resolve().parent / "gst_operator_agent_runtime" -_OPERATOR_PACKAGE_FILES = ("gst_operator_agent.py", "requirements.txt", "README.txt", "install_gst_operator_agent.ps1", "uninstall_gst_operator_agent.ps1") +_OPERATOR_PACKAGE_FILES = ("gst_operator_agent.py", "requirements.txt", "README.txt", "install_gst_operator_agent.ps1", "uninstall_gst_operator_agent.ps1", "build_gst_operator_agent_exe.ps1") +_OPERATOR_EXE_NAME = f"ARRR_GST_Operator_Agent_Setup_{_OPERATOR_AGENT_VERSION}.exe" +_OPERATOR_EXE_PATH = _OPERATOR_RUNTIME_ROOT / "dist" / _OPERATOR_EXE_NAME def _fy_bounds(fy: str) -> tuple[date, date]: @@ -434,6 +436,35 @@ def page(request: Request, client_id: str = "", registration_id: str = "", perio @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 _OPERATOR_EXE_PATH.is_file(): + return FileResponse( + path=str(_OPERATOR_EXE_PATH), + media_type="application/vnd.microsoft.portable-executable", + filename=_OPERATOR_EXE_NAME, + ) + return JSONResponse( + { + "ok": False, + "error": ( + "The GST Operator Agent Windows EXE has not been built in this deployment. " + "Run app/modules/accounting/gst_operator_agent_runtime/build_gst_operator_agent_exe.ps1 " + "on the Windows development repository, commit the generated dist EXE, and redeploy." + ), + }, + status_code=503, + ) + finally: + db.close() + + +@router.get("/operator-agent/download-source") +def download_operator_agent_source(request: Request): + """Preserve the existing source/PowerShell package as a repair fallback.""" db = CommonSessionLocal() try: user, response = _require_partner(request, db, "accounting.learning.view") @@ -449,7 +480,7 @@ def download_operator_agent(request: Request): return JSONResponse({"ok": False, "error": f"GST Operator Agent package file is missing: {name}"}, status_code=500) archive.write(path, Path("ARRR_GST_Operator_Agent") / name) memory.seek(0) - headers = {"Content-Disposition": f'attachment; filename="ARRR_GST_Operator_Agent_{_OPERATOR_AGENT_VERSION}.zip"'} + headers = {"Content-Disposition": f'attachment; filename="ARRR_GST_Operator_Agent_Source_{_OPERATOR_AGENT_VERSION}.zip"'} return StreamingResponse(memory, media_type="application/zip", headers=headers) finally: db.close() diff --git a/app/modules/accounting/templates/accounting/gst_reconciliation.html b/app/modules/accounting/templates/accounting/gst_reconciliation.html index 0db420e..9e1fe76 100644 --- a/app/modules/accounting/templates/accounting/gst_reconciliation.html +++ b/app/modules/accounting/templates/accounting/gst_reconciliation.html @@ -55,11 +55,12 @@
No browser-to-localhost connection is required. When you start a GST download, Windows opens the installed agent through arrrgst://. Chrome/Edge may ask once for permission to open the ARRR GST Operator Agent.
+The Windows EXE installs or repairs the agent for the current Windows user, registers arrrgst://, and appears in Windows Installed Apps for uninstall. Python is not required on operator workstations. The source ZIP remains available only as a repair/development fallback.