Add async SQLite job manager for cash payment extraction
This commit is contained in:
@@ -307,6 +307,52 @@ async def start_cash_payment_cache(request: Request):
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/cache/cancel")
|
||||
async def cancel_cash_payment_cache(request: Request):
|
||||
form = await request.form()
|
||||
validate_csrf(request, str(form.get("csrf_token") or ""))
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_partner(request, db, "accounting.tally.view")
|
||||
if response:
|
||||
return JSONResponse({"ok": False, "error": "Access denied."}, status_code=403)
|
||||
clients, scope = _visible_clients(db, request, user)
|
||||
try:
|
||||
client_id = int(form.get("client_id") or 0)
|
||||
except Exception:
|
||||
client_id = 0
|
||||
selected_client = next((row for row in clients if int(row.id) == client_id), None)
|
||||
if not selected_client:
|
||||
return JSONResponse({"ok": False, "error": "Select a valid client."}, status_code=400)
|
||||
job_id = str(form.get("job_id") or "").strip()
|
||||
date_from = str(form.get("date_from") or "").strip()
|
||||
if not job_id:
|
||||
return JSONResponse({"ok": False, "error": "Extraction job id is required."}, status_code=400)
|
||||
try:
|
||||
parsed_from = date.fromisoformat(date_from)
|
||||
except Exception:
|
||||
return JSONResponse({"ok": False, "error": "Valid extraction From date is required."}, status_code=400)
|
||||
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)
|
||||
result = request_agent_command(
|
||||
node.node_code,
|
||||
"accounting_cash_payment_cache_cancel",
|
||||
{
|
||||
**_accounting_storage_payload(selected_client, _financial_year_for_date(parsed_from)),
|
||||
"job_id": job_id,
|
||||
},
|
||||
timeout_seconds=15,
|
||||
)
|
||||
if not result.get("ok"):
|
||||
return JSONResponse({"ok": False, "error": str(result.get("error") or "Could not cancel extraction.")}, status_code=409)
|
||||
return JSONResponse({"ok": True, **(result.get("result") or {})})
|
||||
except Exception as exc:
|
||||
return JSONResponse({"ok": False, "error": str(exc)}, status_code=502)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/cache/progress")
|
||||
def cash_payment_cache_progress(request: Request, client_id: int, job_id: str, date_from: str):
|
||||
db = CommonSessionLocal()
|
||||
|
||||
@@ -152,7 +152,10 @@
|
||||
</div>
|
||||
<p class="mt-4 text-xs text-slate-500">Only one day is requested from TallyPrime at a time. The agent waits about 3 seconds after every successful day. If Tally responds slowly, it cools for 20 seconds and retries only once. The tunnel heartbeat stays responsive because progress is read from SQLite, not from Tally.</p>
|
||||
<div id="cash-progress-error" class="mt-4 hidden rounded-xl border border-amber-200 bg-amber-50 p-3 text-sm text-amber-900"></div>
|
||||
<div class="mt-4 flex justify-end"><button type="button" id="cash-progress-close" class="hidden rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700">Close</button></div>
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button type="button" id="cash-progress-cancel" class="hidden rounded-xl border border-amber-300 bg-amber-50 px-4 py-2 text-sm font-semibold text-amber-900">Cancel Extraction</button>
|
||||
<button type="button" id="cash-progress-close" class="hidden rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
@@ -170,13 +173,18 @@
|
||||
const currentDate = document.getElementById('cash-progress-date');
|
||||
const errorBox = document.getElementById('cash-progress-error');
|
||||
const closeButton = document.getElementById('cash-progress-close');
|
||||
const cancelButton = document.getElementById('cash-progress-cancel');
|
||||
let pollTimer = null;
|
||||
let activeJobId = '';
|
||||
let activeClientId = '';
|
||||
let activeDateFrom = '';
|
||||
|
||||
function show() {
|
||||
overlay.classList.remove('hidden');
|
||||
overlay.classList.add('flex');
|
||||
errorBox.classList.add('hidden');
|
||||
closeButton.classList.add('hidden');
|
||||
cancelButton.classList.add('hidden');
|
||||
}
|
||||
function stopPolling() {
|
||||
if (pollTimer) window.clearTimeout(pollTimer);
|
||||
@@ -189,6 +197,7 @@
|
||||
errorBox.textContent = message || 'The extraction did not complete.';
|
||||
errorBox.classList.remove('hidden');
|
||||
closeButton.classList.remove('hidden');
|
||||
cancelButton.classList.add('hidden');
|
||||
}
|
||||
function update(job) {
|
||||
const value = Math.max(0, Math.min(100, Number(job.percent || 0)));
|
||||
@@ -199,6 +208,7 @@
|
||||
vouchers.textContent = String(job.vouchers_cached || 0);
|
||||
requests.textContent = String(job.tally_requests || 0);
|
||||
currentDate.textContent = job.current_date || '—';
|
||||
if (job.can_cancel) cancelButton.classList.remove('hidden'); else cancelButton.classList.add('hidden');
|
||||
}
|
||||
async function poll(jobId, clientId, dateFrom, tallyGuid, dateTo) {
|
||||
try {
|
||||
@@ -224,6 +234,10 @@
|
||||
window.location.assign(target.toString());
|
||||
return;
|
||||
}
|
||||
if (job.status === 'cancelled') {
|
||||
fail('Extraction was cancelled safely. Already cached days remain available and can be resumed later.');
|
||||
return;
|
||||
}
|
||||
if (job.status === 'paused' || job.status === 'failed') {
|
||||
fail(job.error || 'Tally extraction paused. Restart/verify TallyPrime and click Extract / Refresh & Analyze again to resume.');
|
||||
return;
|
||||
@@ -245,12 +259,36 @@
|
||||
const data = await response.json();
|
||||
if (!response.ok || !data.ok) throw new Error(data.error || 'Could not start extraction.');
|
||||
const job = data.job || {};
|
||||
activeJobId = job.job_id || '';
|
||||
activeClientId = String(fd.get('client_id') || '');
|
||||
activeDateFrom = String(fd.get('date_from') || '');
|
||||
update(job);
|
||||
poll(job.job_id, fd.get('client_id'), fd.get('date_from'), fd.get('tally_guid'), fd.get('date_to'));
|
||||
} catch (err) {
|
||||
fail(err && err.message ? err.message : String(err));
|
||||
}
|
||||
});
|
||||
cancelButton.addEventListener('click', async function () {
|
||||
if (!activeJobId || !activeClientId) return;
|
||||
cancelButton.disabled = true;
|
||||
text.textContent = 'Requesting a safe stop after the current Tally request…';
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.set('csrf_token', document.getElementById('cash-csrf-token').value);
|
||||
fd.set('client_id', activeClientId);
|
||||
fd.set('job_id', activeJobId);
|
||||
fd.set('date_from', activeDateFrom);
|
||||
const response = await fetch('/tools/accounting/cash-payments/cache/cancel', {method: 'POST', body: fd, headers: {'Accept': 'application/json'}});
|
||||
const data = await response.json();
|
||||
if (!response.ok || !data.ok) throw new Error(data.error || 'Could not cancel extraction.');
|
||||
if (data.job) update(data.job);
|
||||
cancelButton.classList.add('hidden');
|
||||
} catch (err) {
|
||||
errorBox.textContent = err && err.message ? err.message : String(err);
|
||||
errorBox.classList.remove('hidden');
|
||||
cancelButton.disabled = false;
|
||||
}
|
||||
});
|
||||
closeButton.addEventListener('click', function () {
|
||||
overlay.classList.add('hidden'); overlay.classList.remove('flex');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user