Add client financial year Accounting Mirror registry

This commit is contained in:
A R R R Associates
2026-09-18 20:58:41 +05:30
parent 93f2e02c35
commit 8a0d5b212d
11 changed files with 630 additions and 71 deletions
+142 -3
View File
@@ -22,6 +22,12 @@ 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, client_folder_parts, sanitize_segment
from app.modules.accounting.agent_bridge import request_agent_command
from app.modules.accounting.accounting_mirror_registry import (
get_registered_mirror,
list_registered_mirrors,
sync_discovered_mirrors,
upsert_registered_mirror,
)
from app.modules.registrations.models import ClientRegistration, RegistrationType
@@ -247,6 +253,30 @@ def tally_tool(
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
online = _node_online(node)
# Synchronise the cloud-side Client + FY registry from mirrors already
# present on the branch Storage Node. This is read-only and also backfills
# mirrors created before the registry feature was introduced.
if selected_client and node and online:
try:
discovered_response = request_agent_command(
node.node_code,
"accounting_mirror_list",
{"client_id": int(selected_client.id)},
timeout_seconds=20,
)
if discovered_response.get("ok"):
sync_discovered_mirrors(
db,
tenant_id=scope.tenant_id,
client_id=int(selected_client.id),
storage_node_id=int(node.id),
discovered=(discovered_response.get("result") or {}).get("mirrors") or [],
requested_by_user_id=int(user.id),
)
except Exception:
# Mirror discovery must never block the existing Tally page.
pass
live_result = None
analysis_history = []
command_error = error or ""
@@ -304,6 +334,15 @@ def tally_tool(
else selected_fy_end.isoformat()
)
registered_mirrors = (
list_registered_mirrors(db, scope.tenant_id, int(selected_client.id))
if selected_client else []
)
selected_mirror_registration = (
get_registered_mirror(db, scope.tenant_id, int(selected_client.id), selected_financial_year)
if selected_client else None
)
return _render(
request,
db,
@@ -325,6 +364,8 @@ def tally_tool(
selected_financial_year=selected_financial_year,
workspace_financial_year=workspace_financial_year,
mirror_fy_options=mirror_fy_options,
registered_mirrors=registered_mirrors,
selected_mirror_registration=selected_mirror_registration,
command_error=command_error,
analysis_history=analysis_history,
)
@@ -764,6 +805,55 @@ def sync_tally_transactions(
finally:
db.close()
@router.get("/mirror-registration/status")
def mirror_registration_status(request: Request, client_id: int, financial_year: str):
db = CommonSessionLocal()
try:
user, response = _require_partner(request, db, "accounting.tally.view")
if response:
return JSONResponse({"ok": False, "error": "Access denied."}, status_code=403)
client, _clients, scope = _find_visible_client(db, request, user, client_id)
if not client:
return JSONResponse({"ok": False, "error": "Client is not available."}, status_code=404)
try:
_financial_year_bounds(financial_year)
except Exception:
return JSONResponse({"ok": False, "error": "Invalid financial year."}, status_code=400)
row = get_registered_mirror(db, scope.tenant_id, int(client.id), financial_year)
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
file_exists = False
mirror = {}
if node and _node_online(node):
try:
storage_payload = _accounting_storage_payload(client, financial_year)
status = request_agent_command(node.node_code, "accounting_mirror_status", storage_payload, timeout_seconds=20)
if status.get("ok"):
mirror = (status.get("result") or {}).get("mirror") or {}
file_exists = bool(mirror.get("ready"))
if file_exists:
row = upsert_registered_mirror(
db, tenant_id=scope.tenant_id, client_id=int(client.id), financial_year=financial_year,
accounting_relative_dir=storage_payload["accounting_relative_dir"], storage_node_id=int(node.id),
mirror=mirror, requested_by_user_id=int(user.id), replacement=False,
)
db.commit()
except Exception:
pass
exists = bool(row or file_exists)
return JSONResponse({
"ok": True,
"exists": exists,
"financial_year": financial_year,
"client_name": client.client_name,
"company_name": (row.company_name if row else str((mirror.get("company") or {}).get("company_name") or "")),
"last_synced_at": (row.last_synced_at_utc.isoformat() if row and row.last_synced_at_utc else ""),
})
finally:
db.close()
@router.post("/full-export")
def start_full_accounting_export(
request: Request,
@@ -772,6 +862,7 @@ def start_full_accounting_export(
date_from: str = Form(...),
date_to: str = Form(...),
financial_year: str = Form(""),
replace_existing: str = Form(""),
csrf_token: str = Form(...),
):
validate_csrf(request, csrf_token)
@@ -800,13 +891,45 @@ def start_full_accounting_export(
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)
storage_payload = _accounting_storage_payload(client, target_fy)
existing_registration = get_registered_mirror(db, scope.tenant_id, int(client.id), target_fy)
existing_file = False
try:
current_status = request_agent_command(
node.node_code, "accounting_mirror_status", storage_payload, timeout_seconds=20
)
current_mirror = ((current_status.get("result") or {}).get("mirror") or {}) if current_status.get("ok") else {}
existing_file = bool(current_mirror.get("ready"))
if existing_file:
upsert_registered_mirror(
db, tenant_id=scope.tenant_id, client_id=int(client.id), financial_year=target_fy,
accounting_relative_dir=storage_payload["accounting_relative_dir"], storage_node_id=int(node.id),
mirror=current_mirror, requested_by_user_id=int(user.id), replacement=False,
)
db.commit()
except Exception:
existing_file = bool(existing_registration)
replace_confirmed = str(replace_existing or "").strip().lower() in {"1", "true", "yes", "on"}
if (existing_registration or existing_file) and not replace_confirmed:
message = (
f"An Accounting Mirror already exists for {client.client_name} - FY {target_fy}. "
"Use Replace Existing Mirror to refresh it; the current mirror will remain active if the new export fails."
)
return RedirectResponse(
url=f"/tools/tally?client_id={client.id}&financial_year={quote(target_fy)}&error={quote(message)}",
status_code=303,
)
result = request_agent_command(
node.node_code, "accounting_full_export_start",
{
**_accounting_storage_payload(client, target_fy),
**storage_payload,
"tally_guid": str(tally_guid or "").strip(),
"date_from": start.isoformat(), "date_to": end.isoformat(),
"requested_by_user_id": int(user.id), "timeout_seconds": 1800,
"replacement_requested": bool(existing_registration or existing_file),
}, timeout_seconds=20,
)
if not result.get("ok"):
@@ -819,7 +942,7 @@ def start_full_accounting_export(
@router.get("/full-export/status")
def full_accounting_export_status(request: Request, client_id: int):
def full_accounting_export_status(request: Request, client_id: int, financial_year: str = ""):
db = CommonSessionLocal()
try:
user, response = _require_partner(request, db, "accounting.tally.view")
@@ -837,7 +960,23 @@ def full_accounting_export_status(request: Request, client_id: int):
)
if not result.get("ok"):
return JSONResponse({"ok": False, "error": str(result.get("error") or "Progress lookup failed.")}, status_code=500)
return JSONResponse({"ok": True, **(result.get("result") or {})})
result_payload = result.get("result") or {}
job = result_payload.get("job") or {}
if str(job.get("status") or "").lower() == "completed":
fy = str(job.get("financial_year") or financial_year or "").strip()
if re.fullmatch(r"\d{4}-\d{2}", fy):
storage_payload = _accounting_storage_payload(client, fy)
try:
upsert_registered_mirror(
db, tenant_id=scope.tenant_id, client_id=int(client.id), financial_year=fy,
accounting_relative_dir=storage_payload["accounting_relative_dir"], storage_node_id=int(node.id),
mirror=job.get("mirror") or {}, job=job, requested_by_user_id=int(user.id),
replacement=bool(job.get("replacement_requested")),
)
db.commit()
except Exception:
db.rollback()
return JSONResponse({"ok": True, **result_payload})
finally:
db.close()