Route cash payment review through local agent with safe progress

This commit is contained in:
A R R R Associates
2026-09-03 23:15:43 +05:30
parent 84478191a2
commit 3a4370d35e
5 changed files with 99 additions and 8 deletions
+1 -1
View File
@@ -196,7 +196,7 @@ def cash_payment_review(
"split_window_days": int(setting.split_window_days), "split_window_days": int(setting.split_window_days),
"near_limit_percent": float(setting.near_limit_percent), "near_limit_percent": float(setting.near_limit_percent),
}, },
timeout_seconds=120, timeout_seconds=300,
) )
if result.get("ok"): if result.get("ok"):
review = (result.get("result") or {}).get("cash_payment_review") review = (result.get("result") or {}).get("cash_payment_review")
@@ -20,8 +20,8 @@
{% if page_mode != 'allocation' %} {% if page_mode != 'allocation' %}
<section class="grid gap-4 lg:grid-cols-3"> <section class="grid gap-4 lg:grid-cols-3">
<div class="rounded-2xl bg-white p-5 shadow-soft lg:col-span-2"> <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> <h2 class="text-lg font-semibold text-slate-900">Cash 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"> <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> <div>
<label class="text-xs font-semibold uppercase text-slate-500">Client</label> <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()"> <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">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> <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"> <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> </form>
</div> </div>
@@ -63,6 +63,9 @@
{% if review %} {% if review %}
<section class="space-y-4"> <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"> <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)] %} {% 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> <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 %} {% endif %}
</section> </section>
{% endif %} {% 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> </div>
{% endblock %} {% endblock %}
+1 -1
View File
@@ -4,7 +4,7 @@ import io
from pathlib import Path from pathlib import Path
import zipfile import zipfile
ERP_LOCAL_AGENT_VERSION = "1.22.5" ERP_LOCAL_AGENT_VERSION = "1.22.6"
ERP_LOCAL_AGENT_NAME = "ERP Local Agent" ERP_LOCAL_AGENT_NAME = "ERP Local Agent"
RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime" RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime"
_DETERMINISTIC_ZIP_TIMESTAMP = (2026, 1, 1, 0, 0, 0) _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" AGENT_NAME = "ERP Local Agent"
@@ -1,7 +1,8 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timezone from datetime import datetime, timezone, date as _dt_date, timedelta as _timedelta
import re import re
import time
from typing import Any from typing import Any
from . import __version__ from . import __version__
@@ -661,7 +662,13 @@ class AgentCommandProcessor:
if not 1 <= near_limit_percent <= 100: if not 1 <= near_limit_percent <= 100:
raise ValueError("Near-limit percentage must be between 1 and 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") ledger_masters = self.tally.export_master_collection(company_name, "ledgers")
time.sleep(pace_seconds)
cash_ledgers = set() cash_ledgers = set()
for ledger in ledger_masters: for ledger in ledger_masters:
name = str(ledger.get("name") or "").strip() name = str(ledger.get("name") or "").strip()
@@ -672,7 +679,27 @@ class AgentCommandProcessor:
if not cash_ledgers: if not cash_ledgers:
cash_ledgers.add("cash") 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 = [] payments = []
for voucher in vouchers: for voucher in vouchers:
if str(voucher.get("is_cancelled") or "").strip().lower() in {"yes", "true", "1"}: if str(voucher.get("is_cancelled") or "").strip().lower() in {"yes", "true", "1"}:
@@ -798,6 +825,9 @@ class AgentCommandProcessor:
"possible_split_payments": split_patterns, "possible_split_payments": split_patterns,
"cash_payments": payments, "cash_payments": payments,
"read_only": True, "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.", "review_note": "Possible split-payment patterns are review indicators only and do not determine tax treatment.",
}, },
"agent": self._agent_info(), "agent": self._agent_info(),