175 lines
6.3 KiB
Python
175 lines
6.3 KiB
Python
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()
|