Complete Phase 1 Tally UI and local agent integration

This commit is contained in:
A R R R Associates
2026-08-18 14:39:19 +05:30
parent b262861608
commit 2ecaa0b088
15 changed files with 837 additions and 7 deletions
+174
View File
@@ -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()