349 lines
11 KiB
Python
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()
|