Files
arrr-erp/app/modules/accounting/chart_ui.py
T
2026-08-23 13:35:37 +05:30

349 lines
11 KiB
Python

from __future__ import annotations
from urllib.parse import urlencode
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.templating import templates
from app.modules.accounting.agent_bridge import request_agent_command
from app.modules.accounting.chart_models import AccountingChartSyncRun
from app.modules.accounting.chart_service import (
LEDGER_ROLES,
ROLE_LABELS,
chart_summary,
effective_role,
list_groups,
list_ledgers,
persist_chart_snapshot,
root_group_options,
set_manual_ledger_role,
)
from app.modules.accounting.ui import (
_accounting_storage_payload,
_find_visible_client,
_node_online,
_require_partner,
_visible_clients,
)
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
from app.modules.documents.services import get_active_storage_node_for_branch
router = APIRouter(
prefix="/tools/accounting/chart-of-accounts",
tags=["accounting-chart-of-accounts-ui"],
)
def _go(client_id=0, message="", error="", **filters):
q = {}
if client_id:
q["client_id"] = client_id
if message:
q["message"] = message[:350]
if error:
q["error"] = error[:350]
for key, value in filters.items():
if value not in (None, "", "all"):
q[key] = value
return RedirectResponse(
"/tools/accounting/chart-of-accounts" + ("?" + urlencode(q) if q else ""),
status_code=303,
)
@router.get("")
def page(
request: Request,
client_id: int | None = None,
tally_guid: str = "",
view: str = "ledgers",
q: str = "",
role: str = "",
root_group: str = "",
page: int = 1,
per_page: int = 50,
message: str = "",
error: str = "",
):
db = CommonSessionLocal()
try:
user, denied = _require_partner(request, db, "accounting.tally.view")
if denied:
return denied
clients, scope = _visible_clients(db, request, user)
selected = next(
(client for client in clients if client_id and int(client.id) == int(client_id)),
None,
)
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
online = bool(node and _node_online(node))
tally = None
accounting = None
command_error = error or ""
if selected and node and online:
try:
response = request_agent_command(
node.node_code,
"phase6_status",
_accounting_storage_payload(selected),
timeout_seconds=20,
)
if response.get("ok"):
result = response.get("result") or {}
tally = result.get("tally")
accounting = result.get("accounting")
else:
command_error = str(response.get("error") or "Local Agent status failed.")
except Exception as exc:
command_error = str(exc)
mapped_guids = set()
mapping_names = {}
if accounting:
for mapping in accounting.get("mappings") or []:
guid = str(mapping.get("tally_guid") or "").strip()
if guid:
mapped_guids.add(guid)
mapping_names[guid] = str(mapping.get("company_name") or "").strip()
if tally_guid and tally_guid not in mapped_guids:
tally_guid = ""
available_mappings = [
{
"guid": guid,
"name": mapping_names.get(guid, guid),
"currently_loaded": bool(
any(
str(c.get("guid") or "").strip() == guid
for c in ((tally or {}).get("companies") or [])
)
),
}
for guid in sorted(mapped_guids, key=lambda g: mapping_names.get(g, g).casefold())
]
if not tally_guid and len(available_mappings) == 1:
tally_guid = available_mappings[0]["guid"]
summary = {"groups": 0, "ledgers": 0, "manual": 0, "review": 0, "classified": 0}
ledgers = []
groups = []
total = 0
pages = 1
roots = []
last_sync = None
if selected:
summary = chart_summary(
db,
tenant_id=scope.tenant_id,
client_id=selected.id,
tally_guid=tally_guid,
)
roots = root_group_options(
db,
tenant_id=scope.tenant_id,
client_id=selected.id,
tally_guid=tally_guid,
)
if view == "groups":
groups = list_groups(
db,
tenant_id=scope.tenant_id,
client_id=selected.id,
tally_guid=tally_guid,
)
else:
ledgers, total, page, pages = list_ledgers(
db,
tenant_id=scope.tenant_id,
client_id=selected.id,
tally_guid=tally_guid,
q=q,
role=role,
root_group=root_group,
page=page,
per_page=per_page,
)
sync_stmt = select(AccountingChartSyncRun).where(
AccountingChartSyncRun.tenant_id == scope.tenant_id,
AccountingChartSyncRun.client_id == selected.id,
)
if tally_guid:
sync_stmt = sync_stmt.where(AccountingChartSyncRun.tally_guid == tally_guid)
last_sync = db.execute(
sync_stmt.order_by(AccountingChartSyncRun.id.desc()).limit(1)
).scalar_one_or_none()
return templates.TemplateResponse(
"modules/accounting/templates/accounting/chart_of_accounts.html",
{
"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),
"title": "Chart of Accounts",
"clients": clients,
"selected_client": selected,
"node_online": online,
"tally": tally,
"accounting": accounting,
"available_mappings": available_mappings,
"selected_tally_guid": tally_guid,
"summary": summary,
"ledgers": ledgers,
"groups": groups,
"root_groups": roots,
"role_options": LEDGER_ROLES,
"role_labels": ROLE_LABELS,
"effective_role": effective_role,
"view": view if view in {"ledgers", "groups"} else "ledgers",
"q": q,
"role_filter": role,
"root_group_filter": root_group,
"page": page,
"pages": pages,
"per_page": per_page,
"total": total,
"last_sync": last_sync,
"message": message,
"error": command_error,
},
)
finally:
db.close()
@router.post("/sync")
def sync_chart(
request: Request,
client_id: int = Form(...),
tally_guid: str = Form(...),
csrf_token: str = Form(...),
):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user, denied = _require_partner(request, db, "accounting.tally.sync_masters")
if denied:
return denied
client, _clients, scope = _find_visible_client(db, request, user, client_id)
if not client:
return _go(error="Client is not visible.")
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
if not node or not _node_online(node):
return _go(client.id, error="ERP Local Agent is offline for the active branch.")
response = request_agent_command(
node.node_code,
"accounting_chart_of_accounts",
{
**_accounting_storage_payload(client),
"tally_guid": str(tally_guid or "").strip(),
"requested_by_user_id": int(user.id),
},
timeout_seconds=180,
)
if not response.get("ok"):
raise RuntimeError(str(response.get("error") or "Chart of Accounts synchronization failed."))
result = response.get("result") or {}
chart = result.get("chart_of_accounts") or {}
groups = chart.get("groups") or []
ledgers = chart.get("ledgers") or []
company_name = str(
chart.get("company_name")
or (result.get("sync") or {}).get("company_name")
or ""
).strip()
run = persist_chart_snapshot(
db,
tenant_id=scope.tenant_id,
client_id=client.id,
tally_guid=str(tally_guid or "").strip(),
company_name=company_name,
groups=groups,
ledgers=ledgers,
user_id=user.id,
)
return _go(
client.id,
tally_guid=tally_guid,
message=(
f"Chart of Accounts refreshed: {run.group_count} group(s), "
f"{run.ledger_count} ledger(s)."
),
)
except Exception as exc:
db.rollback()
return _go(client_id, tally_guid=tally_guid, error=str(exc))
finally:
db.close()
@router.post("/ledger/{ledger_id}/role")
def update_ledger_role(
request: Request,
ledger_id: int,
client_id: int = Form(...),
tally_guid: str = Form(""),
role_code: str = Form(""),
note: str = Form(""),
q: str = Form(""),
role_filter: str = Form(""),
root_group: str = Form(""),
page: int = Form(1),
per_page: int = Form(50),
csrf_token: str = Form(...),
):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user, denied = _require_partner(request, db, "accounting.learning.manage")
if denied:
return denied
client, _clients, scope = _find_visible_client(db, request, user, client_id)
if not client:
return _go(error="Client is not visible.")
set_manual_ledger_role(
db,
tenant_id=scope.tenant_id,
client_id=client.id,
ledger_id=ledger_id,
role_code=role_code,
note=note,
user_id=user.id,
)
return _go(
client.id,
tally_guid=tally_guid,
q=q,
role=role_filter,
root_group=root_group,
page=page,
per_page=per_page,
message="Ledger role updated.",
)
except Exception as exc:
db.rollback()
return _go(client_id, tally_guid=tally_guid, error=str(exc))
finally:
db.close()