Fix cash ledger SQLite result transport to VPS
This commit is contained in:
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from urllib.parse import quote
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import RedirectResponse, JSONResponse
|
||||
@@ -252,10 +253,11 @@ def cash_payment_review(
|
||||
|
||||
@router.post("/ledgers")
|
||||
async def cash_payment_ledgers(request: Request):
|
||||
"""Load Cash ledgers from the common client .act SQLite master snapshot.
|
||||
"""Queue Cash-ledger preparation and return a durable VPS-known job id.
|
||||
|
||||
Normal loads are local-only. A live Tally master refresh happens only when
|
||||
the user explicitly clicks Refresh Master Data or when no local snapshot exists.
|
||||
The Local Agent reads the common client .act master in the background. Normal
|
||||
loads are SQLite-only. Refresh Master Data is the only path that is allowed to
|
||||
contact TallyPrime for a master refresh.
|
||||
"""
|
||||
form = await request.form()
|
||||
validate_csrf(request, str(form.get("csrf_token") or ""))
|
||||
@@ -284,37 +286,72 @@ async def cash_payment_ledgers(request: Request):
|
||||
if not node or not _node_online(node):
|
||||
return JSONResponse({"ok": False, "error": "ERP Local Agent is offline."}, status_code=409)
|
||||
|
||||
# The VPS generates the job id before sending the command. Even when a
|
||||
# WebSocket ACK is lost, the browser can poll this known id and retrieve the
|
||||
# ledger payload once the Local Agent has completed the SQLite read.
|
||||
job_id = "CASHMASTER-" + uuid.uuid4().hex[:16].upper()
|
||||
try:
|
||||
fy = _financial_year_for_date(date.fromisoformat(date_from))
|
||||
payload = {
|
||||
**_accounting_storage_payload(selected_client, fy),
|
||||
"tally_guid": tally_guid,
|
||||
"company_name": company_name,
|
||||
"ledger_scope": ledger_scope,
|
||||
"force_refresh": force_refresh,
|
||||
"job_id": job_id,
|
||||
"requested_by_user_id": int(user.id),
|
||||
}
|
||||
# Reuse the common accounting master tables already stored in the
|
||||
# client .act SQLite file. This avoids repeatedly querying TallyPrime
|
||||
# just to populate a ledger selector. The Local Agent performs a live
|
||||
# master refresh only when force_refresh is explicitly requested, or
|
||||
# when no usable local snapshot exists yet.
|
||||
payload["ledger_scope"] = ledger_scope
|
||||
payload["force_refresh"] = force_refresh
|
||||
result = request_agent_command(
|
||||
node.node_code,
|
||||
"accounting_cash_payment_cash_ledgers",
|
||||
payload,
|
||||
timeout_seconds=180 if force_refresh else 45,
|
||||
timeout_seconds=12,
|
||||
)
|
||||
except Exception as exc:
|
||||
return JSONResponse({"ok": False, "error": str(exc)}, status_code=502)
|
||||
if not result.get("ok"):
|
||||
return JSONResponse({"ok": False, "error": str(result.get("error") or "Could not load Tally ledgers.")}, status_code=409)
|
||||
return JSONResponse({"ok": True, **(result.get("result") or {})})
|
||||
if result.get("ok"):
|
||||
body = result.get("result") or {}
|
||||
job = body.get("job") or {
|
||||
"job_id": job_id,
|
||||
"status": "queued",
|
||||
"stage": "Local Agent accepted Cash ledger job",
|
||||
"percent": 5,
|
||||
}
|
||||
return JSONResponse({"ok": True, "accepted": True, "job": job, **body})
|
||||
return JSONResponse({
|
||||
"ok": False,
|
||||
"error": str(result.get("error") or "Local Agent rejected the Cash ledger request."),
|
||||
}, status_code=409)
|
||||
except Exception:
|
||||
# A transport timeout does not mean the Local Agent failed. The job id
|
||||
# is durable and known to the VPS; return it immediately and let the
|
||||
# browser poll. This is specifically designed for intermittent tunnel
|
||||
# keepalive/ACK loss.
|
||||
return JSONResponse({
|
||||
"ok": True,
|
||||
"accepted": True,
|
||||
"transport_pending": True,
|
||||
"job": {
|
||||
"job_id": job_id,
|
||||
"status": "queued",
|
||||
"stage": "Command sent; waiting for Local Agent acknowledgment",
|
||||
"percent": 5,
|
||||
"company_name": company_name,
|
||||
},
|
||||
})
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/ledgers/progress")
|
||||
def cash_payment_ledgers_progress(request: Request, client_id: int, job_id: str, date_from: str):
|
||||
def cash_payment_ledgers_progress(
|
||||
request: Request,
|
||||
client_id: int,
|
||||
job_id: str,
|
||||
date_from: str,
|
||||
tally_guid: str = "",
|
||||
company_name: str = "",
|
||||
ledger_scope: str = "cash",
|
||||
force_refresh: int = 0,
|
||||
):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_partner(request, db, "accounting.tally.view")
|
||||
@@ -326,23 +363,79 @@ def cash_payment_ledgers_progress(request: Request, client_id: int, job_id: str,
|
||||
return JSONResponse({"ok": False, "error": "Client not found."}, status_code=404)
|
||||
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
|
||||
if not node or not _node_online(node):
|
||||
return JSONResponse({"ok": False, "error": "ERP Local Agent is offline."}, status_code=409)
|
||||
return JSONResponse({
|
||||
"ok": True,
|
||||
"transport_pending": True,
|
||||
"job": {
|
||||
"job_id": job_id,
|
||||
"status": "running",
|
||||
"stage": "Waiting for ERP Local Agent tunnel",
|
||||
"percent": 85,
|
||||
"company_name": company_name,
|
||||
},
|
||||
})
|
||||
fy = _financial_year_for_date(date.fromisoformat(date_from))
|
||||
payload = {
|
||||
**_accounting_storage_payload(selected_client, fy),
|
||||
"job_id": str(job_id),
|
||||
"tally_guid": str(tally_guid or ""),
|
||||
"company_name": str(company_name or ""),
|
||||
"ledger_scope": str(ledger_scope or "cash"),
|
||||
"force_refresh": bool(force_refresh),
|
||||
"requested_by_user_id": int(user.id),
|
||||
}
|
||||
try:
|
||||
fy = _financial_year_for_date(date.fromisoformat(date_from))
|
||||
result = request_agent_command(
|
||||
node.node_code,
|
||||
"accounting_cash_payment_ledgers_status",
|
||||
{
|
||||
**_accounting_storage_payload(selected_client, fy),
|
||||
"job_id": str(job_id),
|
||||
},
|
||||
timeout_seconds=15,
|
||||
payload,
|
||||
timeout_seconds=10,
|
||||
)
|
||||
except Exception as exc:
|
||||
return JSONResponse({"ok": False, "error": str(exc)}, status_code=502)
|
||||
if not result.get("ok"):
|
||||
return JSONResponse({"ok": False, "error": str(result.get("error") or "Could not read ledger-sync progress.")}, status_code=409)
|
||||
return JSONResponse({"ok": True, **(result.get("result") or {})})
|
||||
except Exception:
|
||||
return JSONResponse({
|
||||
"ok": True,
|
||||
"transport_pending": True,
|
||||
"job": {
|
||||
"job_id": job_id,
|
||||
"status": "running",
|
||||
"stage": "Local work complete or running; waiting to transfer result to VPS",
|
||||
"percent": 90,
|
||||
"company_name": company_name,
|
||||
},
|
||||
})
|
||||
|
||||
if result.get("ok"):
|
||||
return JSONResponse({"ok": True, **(result.get("result") or {})})
|
||||
|
||||
error = str(result.get("error") or "")
|
||||
# If the initial start command itself was lost before reaching the Local
|
||||
# Agent, resend the same deterministic job id. The Local Agent treats this
|
||||
# idempotently, so this cannot create duplicate work.
|
||||
if "not found" in error.casefold():
|
||||
try:
|
||||
restarted = request_agent_command(
|
||||
node.node_code,
|
||||
"accounting_cash_payment_cash_ledgers",
|
||||
payload,
|
||||
timeout_seconds=10,
|
||||
)
|
||||
if restarted.get("ok"):
|
||||
return JSONResponse({"ok": True, **(restarted.get("result") or {})})
|
||||
except Exception:
|
||||
pass
|
||||
return JSONResponse({
|
||||
"ok": True,
|
||||
"transport_pending": True,
|
||||
"job": {
|
||||
"job_id": job_id,
|
||||
"status": "queued",
|
||||
"stage": "Retrying Cash ledger job delivery to Local Agent",
|
||||
"percent": 10,
|
||||
"company_name": company_name,
|
||||
},
|
||||
})
|
||||
|
||||
return JSONResponse({"ok": False, "error": error or "Could not read Cash ledger progress."}, status_code=409)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user