Add client financial year Accounting Mirror registry
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user