Complete Phase 1 Tally UI and local agent integration
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from urllib.parse import quote
|
||||
|
||||
from fastapi import APIRouter, Form, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.db.common import CommonSessionLocal
|
||||
from app.core.security.csrf import get_or_create_csrf_token, validate_csrf
|
||||
from app.core.security.session_auth import get_current_user
|
||||
from app.core.templating import templates
|
||||
from app.modules.clients.models import Client
|
||||
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
|
||||
from app.modules.core.rbac.permission_guard import require_permission
|
||||
from app.modules.documents.services import build_document_scope, get_active_storage_node_for_branch
|
||||
from app.modules.accounting.agent_bridge import request_agent_command
|
||||
|
||||
|
||||
router = APIRouter(prefix="/tools/tally", tags=["accounting-tally-ui"])
|
||||
|
||||
|
||||
def _denied():
|
||||
from app.core.http_responses import ui_access_denied
|
||||
return ui_access_denied()
|
||||
|
||||
|
||||
def _require_partner(request: Request, db, permission: str):
|
||||
user = get_current_user(request, db)
|
||||
if not user:
|
||||
return None, RedirectResponse(url="/login", status_code=303)
|
||||
roles = set(get_user_roles(db, user.id))
|
||||
if "Partner" not in roles:
|
||||
return None, _denied()
|
||||
try:
|
||||
require_permission(db, user, permission)
|
||||
except Exception:
|
||||
return None, _denied()
|
||||
return user, None
|
||||
|
||||
|
||||
def _visible_clients(db, request: Request, user):
|
||||
scope = build_document_scope(request, db, user)
|
||||
stmt = select(Client).where(Client.tenant_id == scope.tenant_id, Client.partner_id == user.id)
|
||||
if scope.branch_id is not None:
|
||||
stmt = stmt.where(Client.branch_id == scope.branch_id)
|
||||
return db.execute(stmt.order_by(Client.client_name.asc(), Client.id.asc())).scalars().all(), scope
|
||||
|
||||
|
||||
def _find_visible_client(db, request: Request, user, client_id: int):
|
||||
clients, scope = _visible_clients(db, request, user)
|
||||
client = next((row for row in clients if int(row.id) == int(client_id)), None)
|
||||
return client, clients, scope
|
||||
|
||||
|
||||
def _node_online(node) -> bool:
|
||||
if not node or not node.last_seen_at_utc:
|
||||
return False
|
||||
seen = node.last_seen_at_utc
|
||||
if seen.tzinfo is None:
|
||||
seen = seen.replace(tzinfo=timezone.utc)
|
||||
return (datetime.now(timezone.utc) - seen).total_seconds() <= 180
|
||||
|
||||
|
||||
def _render(request: Request, db, user, **context):
|
||||
base = {
|
||||
"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),
|
||||
}
|
||||
base.update(context)
|
||||
return templates.TemplateResponse(
|
||||
"modules/accounting/templates/accounting/tally.html",
|
||||
base,
|
||||
)
|
||||
|
||||
|
||||
@router.get("")
|
||||
def tally_tool(request: Request, client_id: int | None = None, refresh: int = 0, initialized: int = 0, error: str = ""):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_partner(request, db, "accounting.tally.view")
|
||||
if response:
|
||||
return response
|
||||
|
||||
clients, scope = _visible_clients(db, request, user)
|
||||
selected_client = next((row for row in clients if client_id and int(row.id) == int(client_id)), None)
|
||||
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
|
||||
online = _node_online(node)
|
||||
|
||||
live_result = None
|
||||
command_error = error or ""
|
||||
if refresh:
|
||||
try:
|
||||
require_permission(db, user, "accounting.tally.connect")
|
||||
except Exception:
|
||||
return _denied()
|
||||
if refresh and node and online:
|
||||
payload = {}
|
||||
if selected_client:
|
||||
payload = {
|
||||
"client_id": int(selected_client.id),
|
||||
"client_name": selected_client.client_name,
|
||||
}
|
||||
try:
|
||||
response_data = request_agent_command(node.node_code, "phase1_status", payload, timeout_seconds=20)
|
||||
if response_data.get("ok"):
|
||||
live_result = response_data.get("result") or {}
|
||||
else:
|
||||
command_error = str(response_data.get("error") or "Local agent command failed.")
|
||||
except Exception as exc:
|
||||
command_error = str(exc)
|
||||
|
||||
return _render(
|
||||
request,
|
||||
db,
|
||||
user,
|
||||
title="Tally Connection",
|
||||
clients=clients,
|
||||
selected_client=selected_client,
|
||||
storage_node=node,
|
||||
agent_online=online,
|
||||
live_result=live_result,
|
||||
initialized=bool(initialized),
|
||||
command_error=command_error,
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/initialize")
|
||||
def initialize_accounting_storage(
|
||||
request: Request,
|
||||
client_id: int = Form(...),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
validate_csrf(request, csrf_token)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_partner(request, db, "accounting.act.initialize")
|
||||
if response:
|
||||
return response
|
||||
client, _clients, scope = _find_visible_client(db, request, user, client_id)
|
||||
if not client:
|
||||
return _denied()
|
||||
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
|
||||
if not node or not _node_online(node):
|
||||
return RedirectResponse(
|
||||
url=f"/tools/tally?client_id={client.id}&error={quote('ERP Local Agent is offline for the active branch.')}",
|
||||
status_code=303,
|
||||
)
|
||||
try:
|
||||
result = request_agent_command(
|
||||
node.node_code,
|
||||
"accounting_initialize",
|
||||
{"client_id": int(client.id), "client_name": client.client_name},
|
||||
timeout_seconds=20,
|
||||
)
|
||||
if not result.get("ok"):
|
||||
raise RuntimeError(str(result.get("error") or "Accounting storage initialization failed."))
|
||||
except Exception as exc:
|
||||
return RedirectResponse(
|
||||
url=f"/tools/tally?client_id={client.id}&error={quote(str(exc))}",
|
||||
status_code=303,
|
||||
)
|
||||
return RedirectResponse(
|
||||
url=f"/tools/tally?client_id={client.id}&refresh=1&initialized=1",
|
||||
status_code=303,
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
Reference in New Issue
Block a user