Add GST Operator Agent EXE v1.6.0 with Windows path-safe build
This commit is contained in:
@@ -1,45 +1,57 @@
|
|||||||
ARRR GST Operator Agent 1.5.1
|
ARRR GST Operator Agent 1.6.0
|
||||||
|
================================
|
||||||
|
|
||||||
Purpose
|
Primary deployment: self-installing Windows EXE.
|
||||||
-------
|
|
||||||
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.
|
|
||||||
|
|
||||||
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
|
Source/repair fallback
|
||||||
------------
|
----------------------
|
||||||
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.
|
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
|
Commands
|
||||||
--------------
|
|
||||||
The first time ARRR ERP opens arrrgst://, Chrome/Edge may ask whether to open ARRR GST Operator Agent. Choose Open/Allow.
|
|
||||||
|
|
||||||
Security
|
|
||||||
--------
|
--------
|
||||||
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
|
Repair/install explicitly:
|
||||||
---------
|
ARRR_GST_Operator_Agent.exe --install --erp-base-url https://office.arrrassociates.com
|
||||||
Run uninstall_gst_operator_agent.ps1. It removes the protocol registration and also cleans legacy v1.0.x localhost listener/startup/certificate state.
|
|
||||||
|
|
||||||
|
Uninstall:
|
||||||
V1.5.1 OCTAGST-STYLE RETURN-DASHBOARD EXTRACTION
|
ARRR_GST_Operator_Agent.exe --uninstall
|
||||||
- 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.
|
|
||||||
|
|||||||
@@ -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
|
||||||
Vendored
BIN
Binary file not shown.
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
AC2EA1EF8F5D066181E4CD500E579D3796A1AAC2BDC28110404CDBEB86B54AA1 ARRR_GST_Operator_Agent_Setup_1.6.0.exe
|
||||||
@@ -2,8 +2,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
@@ -14,10 +16,16 @@ from urllib.parse import parse_qs, urljoin, urlsplit
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
VERSION = "1.5.1"
|
VERSION = "1.6.0"
|
||||||
ROOT = Path(__file__).resolve().parent
|
APP_NAME = "ARRR GST Operator Agent"
|
||||||
DATA = ROOT / "data"
|
PROTOCOL_NAME = "arrrgst"
|
||||||
CONFIG_PATH = ROOT / "config.json"
|
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"
|
GST_LOGIN_URL = "https://services.gst.gov.in/services/login"
|
||||||
SERVICES_DASHBOARD_URL = "https://services.gst.gov.in/services/auth/dashboard"
|
SERVICES_DASHBOARD_URL = "https://services.gst.gov.in/services/auth/dashboard"
|
||||||
RETURN_DASHBOARD_URL = "https://return.gst.gov.in/returns/auth/dashboard"
|
RETURN_DASHBOARD_URL = "https://return.gst.gov.in/returns/auth/dashboard"
|
||||||
@@ -41,7 +49,7 @@ def read_config() -> dict:
|
|||||||
try:
|
try:
|
||||||
return json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
|
return json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
|
||||||
except Exception:
|
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:
|
def safe(value: str, fallback: str = "item") -> str:
|
||||||
@@ -1205,6 +1213,142 @@ def handle_protocol_url(raw_url: str) -> None:
|
|||||||
run_job_foreground(token)
|
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:
|
def self_test() -> int:
|
||||||
cfg = read_config()
|
cfg = read_config()
|
||||||
erp = str(cfg.get("erp_base_url") or "")
|
erp = str(cfg.get("erp_base_url") or "")
|
||||||
@@ -1224,18 +1368,43 @@ def self_test() -> int:
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
DATA.mkdir(parents=True, exist_ok=True)
|
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())
|
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:
|
try:
|
||||||
handle_protocol_url(sys.argv[1])
|
handle_protocol_url(args[0])
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
_log(traceback.format_exc())
|
_log(traceback.format_exc())
|
||||||
_show_error(str(exc))
|
_show_error(str(exc))
|
||||||
raise SystemExit(1)
|
raise SystemExit(1)
|
||||||
return
|
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(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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ $Startup = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Startup
|
|||||||
$ThumbprintFile = Join-Path $InstallRoot "cert_thumbprint.txt"
|
$ThumbprintFile = Join-Path $InstallRoot "cert_thumbprint.txt"
|
||||||
$ProtocolKey = "HKCU:\Software\Classes\arrrgst"
|
$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"
|
Write-Host "Install root: $InstallRoot"
|
||||||
|
|
||||||
# Clean legacy v1.0.x listener/startup/certificate/protocol state first.
|
# Clean legacy v1.0.x listener/startup/certificate/protocol state first.
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
$InstallRoot = Join-Path $env:LOCALAPPDATA "ARRR\GSTOperatorAgent"
|
$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"
|
$Startup = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Startup\ARRR GST Operator Agent.cmd"
|
||||||
$ThumbprintFile = Join-Path $InstallRoot "cert_thumbprint.txt"
|
$ThumbprintFile = Join-Path $InstallRoot "cert_thumbprint.txt"
|
||||||
$ProtocolKey = "HKCU:\Software\Classes\arrrgst"
|
$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 {
|
try {
|
||||||
Get-NetTCPConnection -LocalPort 8791 -State Listen -ErrorAction SilentlyContinue | ForEach-Object {
|
Get-NetTCPConnection -LocalPort 8791 -State Listen -ErrorAction SilentlyContinue | ForEach-Object {
|
||||||
if ($_.OwningProcess) { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }
|
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 $Startup -Force -ErrorAction SilentlyContinue
|
||||||
Remove-Item $ProtocolKey -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item $ProtocolKey -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item $UninstallKey -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
Remove-Item $InstallRoot -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
|
||||||
|
|||||||
@@ -147,9 +147,11 @@ router = APIRouter(prefix="/tools/accounting/gst-reconciliation", tags=["account
|
|||||||
_TOKEN_PURPOSE = "gst_lightweight_operator_v3"
|
_TOKEN_PURPOSE = "gst_lightweight_operator_v3"
|
||||||
_TOKEN_MINUTES = 15
|
_TOKEN_MINUTES = 15
|
||||||
_UPLOAD_ROOT = Path(tempfile.gettempdir()) / "audit_firm_gst_operator_uploads"
|
_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_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]:
|
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")
|
@router.get("/operator-agent/download")
|
||||||
def download_operator_agent(request: Request):
|
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()
|
db = CommonSessionLocal()
|
||||||
try:
|
try:
|
||||||
user, response = _require_partner(request, db, "accounting.learning.view")
|
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)
|
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)
|
archive.write(path, Path("ARRR_GST_Operator_Agent") / name)
|
||||||
memory.seek(0)
|
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)
|
return StreamingResponse(memory, media_type="application/zip", headers=headers)
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|||||||
@@ -55,11 +55,12 @@
|
|||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2 rounded-lg border bg-slate-50 p-3 text-sm">
|
<div class="flex flex-wrap items-center gap-2 rounded-lg border bg-slate-50 p-3 text-sm">
|
||||||
<span>GST Operator Agent:</span>
|
<span>GST Operator Agent:</span>
|
||||||
<span class="font-semibold text-slate-700">Windows protocol launcher</span>
|
<span class="font-semibold text-slate-700">Windows EXE agent</span>
|
||||||
<span class="text-slate-400">v{{ operator_agent_version }}</span>
|
<span class="text-slate-400">v{{ operator_agent_version }}</span>
|
||||||
<a href="/tools/accounting/gst-reconciliation/operator-agent/download" class="ml-auto rounded border bg-white px-3 py-1.5 text-xs font-medium">Download / Clean Install Agent</a>
|
<a href="/tools/accounting/gst-reconciliation/operator-agent/download" class="ml-auto rounded bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white">Download Windows Installer (.exe)</a>
|
||||||
|
<a href="/tools/accounting/gst-reconciliation/operator-agent/download-source" class="rounded border bg-white px-3 py-1.5 text-xs font-medium">Repair Source ZIP</a>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-slate-500">No browser-to-localhost connection is required. When you start a GST download, Windows opens the installed agent through <b>arrrgst://</b>. Chrome/Edge may ask once for permission to open the ARRR GST Operator Agent.</p>
|
<p class="text-xs text-slate-500">The Windows EXE installs or repairs the agent for the current Windows user, registers <b>arrrgst://</b>, 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.</p>
|
||||||
|
|
||||||
<label class="text-sm">Credential Vault Entry
|
<label class="text-sm">Credential Vault Entry
|
||||||
<select name="credential_id" required class="mt-1 w-full rounded border p-2" {% if not credentials %}disabled{% endif %}>
|
<select name="credential_id" required class="mt-1 w-full rounded border p-2" {% if not credentials %}disabled{% endif %}>
|
||||||
|
|||||||
Reference in New Issue
Block a user