Route cash payment review through local agent with safe progress
This commit is contained in:
@@ -196,7 +196,7 @@ def cash_payment_review(
|
||||
"split_window_days": int(setting.split_window_days),
|
||||
"near_limit_percent": float(setting.near_limit_percent),
|
||||
},
|
||||
timeout_seconds=120,
|
||||
timeout_seconds=300,
|
||||
)
|
||||
if result.get("ok"):
|
||||
review = (result.get("result") or {}).get("cash_payment_review")
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
{% if page_mode != 'allocation' %}
|
||||
<section class="grid gap-4 lg:grid-cols-3">
|
||||
<div class="rounded-2xl bg-white p-5 shadow-soft lg:col-span-2">
|
||||
<h2 class="text-lg font-semibold text-slate-900">Tax Payment Compliance Review</h2>
|
||||
<form method="get" action="/tools/accounting/cash-payments" class="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<h2 class="text-lg font-semibold text-slate-900">Cash Payment Compliance Review</h2>
|
||||
<form id="cash-review-form" method="get" action="/tools/accounting/cash-payments" class="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div>
|
||||
<label class="text-xs font-semibold uppercase text-slate-500">Client</label>
|
||||
<select name="client_id" class="mt-1 w-full rounded-xl border-slate-300 text-sm" required onchange="this.form.submit()">
|
||||
@@ -43,7 +43,7 @@
|
||||
<div><label class="text-xs font-semibold uppercase text-slate-500">From</label><input type="date" name="date_from" value="{{ date_from }}" class="mt-1 w-full rounded-xl border-slate-300 text-sm" required></div>
|
||||
<div><label class="text-xs font-semibold uppercase text-slate-500">To</label><input type="date" name="date_to" value="{{ date_to }}" class="mt-1 w-full rounded-xl border-slate-300 text-sm" required></div>
|
||||
<input type="hidden" name="run" value="1">
|
||||
<div class="md:col-span-2 xl:col-span-4"><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white" {% if not selected_client %}disabled{% endif %}>Run Tally Review</button></div>
|
||||
<div class="md:col-span-2 xl:col-span-4"><button id="cash-review-button" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white" {% if not selected_client %}disabled{% endif %}>Run Tally Review</button><p class="mt-2 text-xs text-slate-500">Safe-paced mode reads Tally in monthly batches with a short pause between requests to reduce load on TallyPrime.</p></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -63,6 +63,9 @@
|
||||
|
||||
{% if review %}
|
||||
<section class="space-y-4">
|
||||
{% if review.safe_paced_mode %}
|
||||
<div class="rounded-2xl border border-sky-200 bg-sky-50 px-4 py-3 text-sm text-sky-800">Completed in safe-paced mode using {{ review.tally_request_batches or 0 }} Tally voucher batch(es), with approximately {{ review.tally_pause_ms or 0 }} ms pause between batches.</div>
|
||||
{% endif %}
|
||||
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
|
||||
{% for label, value in [('Vouchers reviewed', review.summary.vouchers_reviewed), ('Cash payments', review.summary.cash_payment_vouchers), ('Single-voucher exceptions', review.summary.single_voucher_exceptions), ('Same-day exceptions', review.summary.same_day_exceptions), ('Possible split patterns', review.summary.possible_split_patterns)] %}
|
||||
<div class="rounded-2xl bg-white p-4 shadow-soft"><div class="text-xs font-semibold uppercase text-slate-500">{{ label }}</div><div class="mt-2 text-2xl font-semibold text-slate-900">{{ value }}</div></div>
|
||||
@@ -115,5 +118,63 @@
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
<div id="cash-review-progress" class="fixed inset-0 z-[100] hidden items-center justify-center bg-slate-950/40 p-4 backdrop-blur-sm">
|
||||
<div class="w-full max-w-xl rounded-2xl bg-white p-6 shadow-2xl">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-slate-900">Cash Payment Analysis in Progress</h3>
|
||||
<p id="cash-progress-text" class="mt-1 text-sm text-slate-600">Sending the review request to the ERP Local Agent…</p>
|
||||
</div>
|
||||
<div id="cash-progress-percent" class="text-lg font-semibold text-brand-700">5%</div>
|
||||
</div>
|
||||
<div class="mt-5 h-3 overflow-hidden rounded-full bg-slate-200">
|
||||
<div id="cash-progress-bar" class="h-full rounded-full bg-brand-600 transition-all duration-700" style="width:5%"></div>
|
||||
</div>
|
||||
<div class="mt-4 grid grid-cols-4 gap-2 text-center text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
||||
<span>Agent</span><span>Tally</span><span>Vouchers</span><span>Analysis</span>
|
||||
</div>
|
||||
<p class="mt-4 text-xs text-slate-500">Tally is deliberately read in smaller monthly batches with pauses between requests. Please keep TallyPrime and the ERP Local Agent open until this finishes.</p>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
const form = document.getElementById('cash-review-form');
|
||||
if (!form) return;
|
||||
const overlay = document.getElementById('cash-review-progress');
|
||||
const bar = document.getElementById('cash-progress-bar');
|
||||
const pct = document.getElementById('cash-progress-percent');
|
||||
const text = document.getElementById('cash-progress-text');
|
||||
const button = document.getElementById('cash-review-button');
|
||||
const stages = [
|
||||
[8, 'Sending the review request to the ERP Local Agent…'],
|
||||
[18, 'Connecting to TallyPrime on the workstation…'],
|
||||
[32, 'Reading cash and ledger masters…'],
|
||||
[48, 'Reading vouchers in safe monthly batches…'],
|
||||
[64, 'Continuing voucher scan with controlled pauses…'],
|
||||
[78, 'Checking same-day cash payment aggregation…'],
|
||||
[88, 'Checking possible split-payment patterns…'],
|
||||
[94, 'Preparing the compliance report…']
|
||||
];
|
||||
form.addEventListener('submit', function (event) {
|
||||
const submitter = event.submitter;
|
||||
// Client dropdown auto-submit should remain quick and should not show a review progress dialog.
|
||||
if (!submitter || submitter.id !== 'cash-review-button') return;
|
||||
overlay.classList.remove('hidden');
|
||||
overlay.classList.add('flex');
|
||||
button.disabled = true;
|
||||
let i = 0;
|
||||
const advance = function () {
|
||||
if (i >= stages.length) return;
|
||||
const stage = stages[i++];
|
||||
bar.style.width = stage[0] + '%';
|
||||
pct.textContent = stage[0] + '%';
|
||||
text.textContent = stage[1];
|
||||
if (i < stages.length) window.setTimeout(advance, i < 4 ? 1300 : 2200);
|
||||
};
|
||||
advance();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,7 +4,7 @@ import io
|
||||
from pathlib import Path
|
||||
import zipfile
|
||||
|
||||
ERP_LOCAL_AGENT_VERSION = "1.22.5"
|
||||
ERP_LOCAL_AGENT_VERSION = "1.22.6"
|
||||
ERP_LOCAL_AGENT_NAME = "ERP Local Agent"
|
||||
RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime"
|
||||
_DETERMINISTIC_ZIP_TIMESTAMP = (2026, 1, 1, 0, 0, 0)
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
__version__ = "1.22.5"
|
||||
__version__ = "1.22.6"
|
||||
AGENT_NAME = "ERP Local Agent"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timezone, date as _dt_date, timedelta as _timedelta
|
||||
import re
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from . import __version__
|
||||
@@ -661,7 +662,13 @@ class AgentCommandProcessor:
|
||||
if not 1 <= near_limit_percent <= 100:
|
||||
raise ValueError("Near-limit percentage must be between 1 and 100.")
|
||||
|
||||
# TallyPrime is intentionally queried in a paced manner. A full-year XML
|
||||
# voucher export in one request can make desktop Tally unresponsive on
|
||||
# larger books. Load masters once, pause briefly, then read vouchers in
|
||||
# month-sized batches with a small gap between calls.
|
||||
pace_seconds = 0.75
|
||||
ledger_masters = self.tally.export_master_collection(company_name, "ledgers")
|
||||
time.sleep(pace_seconds)
|
||||
cash_ledgers = set()
|
||||
for ledger in ledger_masters:
|
||||
name = str(ledger.get("name") or "").strip()
|
||||
@@ -672,7 +679,27 @@ class AgentCommandProcessor:
|
||||
if not cash_ledgers:
|
||||
cash_ledgers.add("cash")
|
||||
|
||||
vouchers = self.tally.export_vouchers(company_name, date_from, date_to)
|
||||
try:
|
||||
scan_from = _dt_date.fromisoformat(date_from)
|
||||
scan_to = _dt_date.fromisoformat(date_to)
|
||||
except ValueError as exc:
|
||||
raise ValueError("Cash payment review dates must be valid YYYY-MM-DD dates.") from exc
|
||||
if scan_to < scan_from:
|
||||
raise ValueError("Cash payment review To date cannot be before From date.")
|
||||
|
||||
vouchers = []
|
||||
batch_count = 0
|
||||
cursor = scan_from
|
||||
while cursor <= scan_to:
|
||||
next_month = (cursor.replace(day=28) + _timedelta(days=4)).replace(day=1)
|
||||
batch_to = min(scan_to, next_month - _timedelta(days=1))
|
||||
batch = self.tally.export_vouchers(company_name, cursor.isoformat(), batch_to.isoformat())
|
||||
vouchers.extend(batch or [])
|
||||
batch_count += 1
|
||||
cursor = batch_to + _timedelta(days=1)
|
||||
if cursor <= scan_to:
|
||||
time.sleep(pace_seconds)
|
||||
|
||||
payments = []
|
||||
for voucher in vouchers:
|
||||
if str(voucher.get("is_cancelled") or "").strip().lower() in {"yes", "true", "1"}:
|
||||
@@ -798,6 +825,9 @@ class AgentCommandProcessor:
|
||||
"possible_split_payments": split_patterns,
|
||||
"cash_payments": payments,
|
||||
"read_only": True,
|
||||
"safe_paced_mode": True,
|
||||
"tally_request_batches": batch_count,
|
||||
"tally_pause_ms": int(pace_seconds * 1000),
|
||||
"review_note": "Possible split-payment patterns are review indicators only and do not determine tax treatment.",
|
||||
},
|
||||
"agent": self._agent_info(),
|
||||
|
||||
Reference in New Issue
Block a user