diff --git a/app/modules/accounting/cash_payment_ui.py b/app/modules/accounting/cash_payment_ui.py
index 16be99a..19b38d2 100644
--- a/app/modules/accounting/cash_payment_ui.py
+++ b/app/modules/accounting/cash_payment_ui.py
@@ -252,7 +252,11 @@ def cash_payment_review(
@router.post("/ledgers")
async def cash_payment_ledgers(request: Request):
- """Load Cash ledgers using the same name-resolved live Tally master snapshot path as Opening Balance Review."""
+ """Load Cash ledgers from the common client .act SQLite master snapshot.
+
+ 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.
+ """
form = await request.form()
validate_csrf(request, str(form.get("csrf_token") or ""))
db = CommonSessionLocal()
@@ -287,18 +291,18 @@ async def cash_payment_ledgers(request: Request):
"tally_guid": tally_guid,
"company_name": company_name,
}
- # Use the same proven live Tally master-snapshot mechanism used by
- # Opening Balance Review. The Local Agent resolves the selected open
- # company, reads the standard Tally masters and returns either only
- # Cash-in-Hand ledgers or all ledgers for explicit manual selection.
- # No separate cash-ledger cache/job is required for this selector.
+ # 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,
+ timeout_seconds=180 if force_refresh else 45,
)
except Exception as exc:
return JSONResponse({"ok": False, "error": str(exc)}, status_code=502)
diff --git a/app/modules/accounting/templates/accounting/cash_payment_review.html b/app/modules/accounting/templates/accounting/cash_payment_review.html
index 17aa98b..d4890c7 100644
--- a/app/modules/accounting/templates/accounting/cash_payment_review.html
+++ b/app/modules/accounting/templates/accounting/cash_payment_review.html
@@ -67,17 +67,23 @@
{% if selected_cash_ledger_name %}{% endif %}
-
+
-
Uses the same live Tally master-reading mechanism as Opening Balance Review. By default only ledgers under Cash-in-Hand are shown; choose “All Tally ledgers” only for manual selection when required.
+
Reads the common ledger master already stored in the client .act SQLite database. TallyPrime is contacted only when you click “Refresh Master Data” or when no local master snapshot exists yet.
Preparing Cash ledger list…0%
+
+
1. Local Master
.act SQLite
+
2. Cash Filter
Cash-in-Hand
+
3. Ledger Ready
No Tally query
+
+
Master source: local .act SQLite. Use Refresh Master Data only when Tally masters have changed.
-
Select the Tally company, choose a Cash-in-Hand ledger, and review its cash transactions for the selected period. The existing background extraction, threshold analysis and dual VPS/Local Agent progress are preserved.
+
Select the Tally company and Cash ledger from the local .act master snapshot. Only the selected ledger’s relevant transactions are then extracted from Tally into SQLite; compliance analysis runs from SQLite. Existing threshold rules, allocation workflow and dual VPS/Local Agent progress are preserved.
@@ -191,7 +197,7 @@
4. Analyse
5. Complete
-
The confirmed Cash ledger is used throughout this job. The Local Agent reads Tally in safe one-day batches and stores only vouchers containing that ledger in the cash-review cache. You can close this ERP page; the background job continues and remains visible on the Local Agent dashboard.
+
The confirmed Cash ledger comes from the common local .act master snapshot. The Local Agent then reads Tally in safe one-day batches and stores only vouchers containing that ledger in SQLite. Analysis runs from SQLite. You can close this ERP page; the background job continues and remains visible on the Local Agent dashboard.
@@ -232,8 +238,21 @@
const ledgerProgressBar = document.getElementById('cash-ledger-progress-bar');
const ledgerProgressPct = document.getElementById('cash-ledger-progress-pct');
const ledgerProgressStage = document.getElementById('cash-ledger-progress-stage');
+ const masterStep1 = document.getElementById('cash-master-step-1');
+ const masterStep2 = document.getElementById('cash-master-step-2');
+ const masterStep3 = document.getElementById('cash-master-step-3');
+ const masterMeta = document.getElementById('cash-master-meta');
let ledgerPollTimer = null;
+ function paintMasterSteps(step) {
+ [masterStep1, masterStep2, masterStep3].forEach(function(el, idx) {
+ if (!el) return;
+ el.className = 'rounded-lg border px-2 py-2 text-center ' + (idx < step
+ ? 'border-emerald-200 bg-emerald-50 text-emerald-900'
+ : 'border-slate-200 bg-slate-50 text-slate-700');
+ });
+ }
+
function renderLedgerOptions() {
const previous = ledgerSelect.value;
const rows = (ledgerScope && ledgerScope.value === 'all') ? allLedgerRows : cashLedgerRows;
@@ -267,6 +286,12 @@
from_cache: Boolean(data.from_cache)
};
renderLedgerOptions();
+ paintMasterSteps(3);
+ if (masterMeta) {
+ const synced = data.master_synced_at_utc ? new Date(data.master_synced_at_utc).toLocaleString() : 'time not available';
+ const source = data.master_refreshed ? 'TallyPrime → local .act SQLite (refreshed now)' : 'local .act SQLite';
+ masterMeta.textContent = 'Master source: ' + source + ' · Last master sync: ' + synced + ' · ' + String(data.total_ledgers_examined || data.ledger_count || 0) + ' ledger(s) in common snapshot.';
+ }
}
function updateLedgerProgress(job) {
@@ -275,6 +300,9 @@
ledgerProgressBar.style.width=p+'%';
ledgerProgressPct.textContent=Math.round(p)+'%';
ledgerProgressStage.textContent=job.stage||job.status||'Working…';
+ if (p >= 90) paintMasterSteps(3);
+ else if (p >= 60) paintMasterSteps(2);
+ else if (p >= 15) paintMasterSteps(1);
}
async function pollLedgers(jobId, clientId, dateFrom) {
@@ -310,9 +338,20 @@
fd.set('ledger_scope', selectedScope);
if(forceRefresh) fd.set('force_refresh','1');
ledgerButton.disabled = true; if(ledgerRefreshButton) ledgerRefreshButton.disabled=true;
- ledgerHelp.textContent = selectedScope === 'all'
- ? 'Reading all Tally ledgers using the proven master snapshot…'
- : 'Reading Tally masters and selecting Cash-in-Hand ledgers…';
+ paintMasterSteps(0);
+ ledgerProgress.classList.remove('hidden');
+ ledgerProgressBar.style.width='15%'; ledgerProgressPct.textContent='15%';
+ if (forceRefresh) {
+ ledgerProgressStage.textContent='Refreshing common accounting masters from TallyPrime…';
+ ledgerHelp.textContent='Refreshing the shared .act master snapshot once, then filtering the Cash-in-Hand hierarchy locally.';
+ if (masterMeta) masterMeta.textContent='Master source: refreshing TallyPrime → local .act SQLite.';
+ } else {
+ ledgerProgressStage.textContent='Reading common ledger master from local SQLite…';
+ ledgerHelp.textContent = selectedScope === 'all'
+ ? 'Reading all ledgers from the common local .act master snapshot…'
+ : 'Reading local .act masters and selecting Cash-in-Hand ledgers…';
+ if (masterMeta) masterMeta.textContent='Master source: local .act SQLite. TallyPrime is not queried for this normal load.';
+ }
try {
const response = await fetch('/tools/accounting/cash-payments/ledgers', {method:'POST', body:fd, headers:{'Accept':'application/json'}});
const data = await response.json();
@@ -323,8 +362,9 @@
ledgerProgress.classList.remove('hidden');
ledgerProgressBar.style.width='100%'; ledgerProgressPct.textContent='100%';
ledgerProgressStage.textContent = data.cash_only
- ? String(data.ledger_count || 0)+' Cash-in-Hand ledger(s) ready'
- : String(data.ledger_count || 0)+' ledger(s) ready';
+ ? String(data.ledger_count || 0)+' Cash-in-Hand ledger(s) ready from SQLite'
+ : String(data.ledger_count || 0)+' ledger(s) ready from SQLite';
+ paintMasterSteps(3);
window.setTimeout(()=>ledgerProgress.classList.add('hidden'),1200);
return;
}
diff --git a/app/modules/documents/agent_package.py b/app/modules/documents/agent_package.py
index da8f3ca..18f0a77 100644
--- a/app/modules/documents/agent_package.py
+++ b/app/modules/documents/agent_package.py
@@ -4,7 +4,7 @@ import io
from pathlib import Path
import zipfile
-ERP_LOCAL_AGENT_VERSION = "1.22.21"
+ERP_LOCAL_AGENT_VERSION = "1.22.22"
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)
diff --git a/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py b/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py
index a38def6..aa7ece2 100644
--- a/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py
+++ b/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py
@@ -1,2 +1,2 @@
-__version__ = "1.22.21"
+__version__ = "1.22.22"
AGENT_NAME = "ERP Local Agent"
diff --git a/app/modules/documents/local_agent_runtime/erp_local_agent/commands.py b/app/modules/documents/local_agent_runtime/erp_local_agent/commands.py
index 72ec1b9..00ae1bf 100644
--- a/app/modules/documents/local_agent_runtime/erp_local_agent/commands.py
+++ b/app/modules/documents/local_agent_runtime/erp_local_agent/commands.py
@@ -179,6 +179,8 @@ class AgentCommandProcessor:
"cash_payment_open_company_selection_capability": True,
"cash_payment_opening_balance_master_path_capability": True,
"cash_payment_selected_company_resolver_capability": True,
+ "cash_payment_common_act_master_capability": True,
+ "cash_payment_sqlite_first_ledger_capability": True,
"opening_balance_balance_sheet_only_capability": True,
"tally_writeback_capability": True,
}
@@ -743,18 +745,19 @@ class AgentCommandProcessor:
def _cash_payment_cash_ledgers(self, payload: dict[str, Any]) -> dict[str, Any]:
- """Return Cash-in-Hand ledgers using the proven Opening Balance master path.
+ """Return Cash ledgers from the common client .act SQLite master snapshot.
- Cash Payment no longer uses a separate Tally collection or a separate
- ledger-master job merely to populate the selector. It deliberately reuses
- the same open-company resolution and ``fetch_accounting_masters`` call used
- by Opening Balance Review, then filters those already-returned masters.
- The legacy async full-ledger cache functions remain untouched for backward
- compatibility and for any older callers.
+ The normal Cash Payment selector does not contact TallyPrime. It reuses the
+ same common ``tally_groups`` and ``tally_ledgers`` tables populated by the
+ accounting master/opening-balance workflow. A live Tally master refresh is
+ performed only when the user explicitly requests Refresh Master Data, or
+ when no usable local snapshot exists for the selected company.
"""
+ client_id = int(payload.get("client_id"))
expected_company_name = str(payload.get("company_name") or "").strip()
- expected_guid = str(payload.get("tally_guid") or "").strip()
+ requested_guid = str(payload.get("tally_guid") or "").strip()
ledger_scope = str(payload.get("ledger_scope") or "cash").strip().lower()
+ force_refresh = bool(payload.get("force_refresh"))
if ledger_scope not in {"cash", "all"}:
ledger_scope = "cash"
@@ -763,29 +766,143 @@ class AgentCommandProcessor:
"job_id": progress_id,
"job_kind": "cash_ledger_discovery",
"status": "running",
- "stage": "Reading Tally masters using Opening Balance mechanism",
+ "stage": "Reading common ledger master from local SQLite",
"percent": 20,
"company_name": expected_company_name,
"ledger_count": 0,
+ "source": "Local .act SQLite",
})
+
+ def _read_local_snapshot(tally_guid: str, company_name: str) -> tuple[str, str, list[dict[str, Any]], list[dict[str, Any]], str]:
+ with self.store.connect(client_id) as db:
+ guid = str(tally_guid or "").strip()
+ name = str(company_name or "").strip()
+
+ if guid:
+ count_row = db.execute(
+ "SELECT COUNT(*) FROM tally_ledgers WHERE tally_guid=?",
+ (guid,),
+ ).fetchone()
+ if not count_row or int(count_row[0] or 0) == 0:
+ guid = ""
+
+ if not guid and name:
+ guid_row = db.execute(
+ """SELECT tally_guid, company_name, MAX(synced_at_utc) AS synced_at
+ FROM tally_ledgers
+ WHERE lower(trim(company_name))=lower(trim(?))
+ GROUP BY tally_guid, company_name
+ ORDER BY MAX(synced_at_utc) DESC
+ LIMIT 1""",
+ (name,),
+ ).fetchone()
+ if guid_row:
+ guid = str(guid_row["tally_guid"] or "")
+ name = str(guid_row["company_name"] or name)
+
+ if not guid:
+ return "", name, [], [], ""
+
+ group_rows = [
+ dict(row) for row in db.execute(
+ """SELECT master_guid AS guid, name, parent, reserved_name, synced_at_utc
+ FROM tally_groups
+ WHERE tally_guid=?
+ ORDER BY name COLLATE NOCASE""",
+ (guid,),
+ ).fetchall()
+ ]
+ ledger_rows = [
+ dict(row) for row in db.execute(
+ """SELECT master_guid AS guid, name, parent, reserved_name,
+ opening_balance, closing_balance, synced_at_utc
+ FROM tally_ledgers
+ WHERE tally_guid=?
+ ORDER BY name COLLATE NOCASE""",
+ (guid,),
+ ).fetchall()
+ ]
+ sync_row = db.execute(
+ """SELECT MAX(synced_at_utc) AS synced_at
+ FROM tally_ledgers
+ WHERE tally_guid=?""",
+ (guid,),
+ ).fetchone()
+ synced_at = str((sync_row["synced_at"] if sync_row else "") or "")
+ company_row = db.execute(
+ """SELECT company_name
+ FROM tally_ledgers
+ WHERE tally_guid=? AND trim(company_name)<>''
+ ORDER BY synced_at_utc DESC LIMIT 1""",
+ (guid,),
+ ).fetchone()
+ if company_row:
+ name = str(company_row["company_name"] or name)
+ return guid, name, group_rows, ledger_rows, synced_at
+
try:
- # Resolve by the exact company name selected by the user, exactly like
- # Opening Balance Review. Do not use Tally's global CURRENTCOMPANY as
- # a veto when multiple companies are open; Tally may report whichever
- # company currently has UI focus.
- company, resolved_guid, guid_refreshed = self._cash_payment_selected_company(payload)
+ resolved_guid, resolved_name, group_rows, ledger_rows, synced_at = _read_local_snapshot(
+ requested_guid, expected_company_name
+ )
+ refreshed = False
- masters = self.tally.fetch_accounting_masters(company.name)
- group_rows = list(masters.get("groups") or [])
- ledger_rows = list(masters.get("ledgers") or [])
+ if force_refresh or not ledger_rows:
+ _cash_progress_snapshot({
+ "job_id": progress_id,
+ "job_kind": "cash_ledger_discovery",
+ "status": "running",
+ "stage": "Refreshing common accounting masters from TallyPrime",
+ "percent": 45,
+ "company_name": expected_company_name,
+ "ledger_count": len(ledger_rows),
+ "source": "TallyPrime → .act SQLite",
+ })
+ company, actual_guid, guid_refreshed = self._cash_payment_selected_company(payload)
+ actual_guid = str(actual_guid or requested_guid or "").strip()
+ if not actual_guid:
+ raise ValueError("Tally did not return a GUID for the selected company.")
+
+ masters = self.tally.fetch_accounting_masters(company.name)
+ mapping = self.store.get_active_mapping_by_guid(client_id, actual_guid)
+ self.store.replace_master_snapshot(
+ client_id,
+ mapping={**mapping, "tally_guid": actual_guid, "company_name": company.name},
+ masters=masters,
+ requested_by_user_id=(
+ int(payload.get("requested_by_user_id"))
+ if payload.get("requested_by_user_id") not in (None, "")
+ else None
+ ),
+ )
+ refreshed = True
+ resolved_guid, resolved_name, group_rows, ledger_rows, synced_at = _read_local_snapshot(
+ actual_guid, company.name
+ )
+ if not ledger_rows:
+ raise ValueError("The refreshed Tally master snapshot did not contain any ledgers.")
+ else:
+ guid_refreshed = bool(
+ requested_guid and resolved_guid and requested_guid != resolved_guid
+ )
+
+ _cash_progress_snapshot({
+ "job_id": progress_id,
+ "job_kind": "cash_ledger_discovery",
+ "status": "running",
+ "stage": "Filtering Cash-in-Hand hierarchy from local SQLite",
+ "percent": 75,
+ "company_name": resolved_name or expected_company_name,
+ "ledger_count": len(ledger_rows),
+ "source": "Local .act SQLite",
+ })
- # Build the complete Cash-in-Hand descendant-group hierarchy from the
- # same Group masters already returned by Tally. This supports both
- # ledgers directly under Cash-in-Hand and user-created child groups.
cash_group_keys = {"cash-in-hand", "cash in hand"}
changed = True
while changed:
changed = False
+ normalized_cash = {
+ x.casefold().replace("-", " ").strip() for x in cash_group_keys
+ }
for group in group_rows:
name = str(group.get("name") or "").strip()
parent = str(group.get("parent") or "").strip()
@@ -793,24 +910,34 @@ class AgentCommandProcessor:
continue
parent_key = parent.casefold().replace("-", " ").strip()
name_key = name.casefold().replace("-", " ").strip()
- normalized_cash = {x.replace("-", " ") for x in cash_group_keys}
if parent_key in normalized_cash and name_key not in normalized_cash:
cash_group_keys.add(name_key)
changed = True
all_ledgers = self._cash_ledger_rows(ledger_rows)
+ normalized_groups = {
+ x.casefold().replace("-", " ").strip() for x in cash_group_keys
+ }
cash_ledgers: list[dict[str, Any]] = []
- normalized_groups = {x.casefold().replace("-", " ").strip() for x in cash_group_keys}
for row in all_ledgers:
parent_key = str(row.get("parent") or "").casefold().replace("-", " ").strip()
reserved = str(row.get("reserved_name") or "").strip().casefold()
name_key = str(row.get("name") or "").strip().casefold()
- is_cash = parent_key in normalized_groups or reserved == "cash" or name_key == "cash"
+ is_cash = (
+ parent_key in normalized_groups
+ or reserved == "cash"
+ or name_key == "cash"
+ )
row["is_cash_candidate"] = bool(is_cash)
if is_cash:
cash_ledgers.append(row)
- all_ledgers.sort(key=lambda x: (not bool(x.get("is_cash_candidate")), str(x.get("name") or "").casefold()))
+ all_ledgers.sort(
+ key=lambda x: (
+ not bool(x.get("is_cash_candidate")),
+ str(x.get("name") or "").casefold(),
+ )
+ )
cash_ledgers.sort(key=lambda x: str(x.get("name") or "").casefold())
returned = all_ledgers if ledger_scope == "all" else cash_ledgers
@@ -818,25 +945,31 @@ class AgentCommandProcessor:
"job_id": progress_id,
"job_kind": "cash_ledger_discovery",
"status": "completed",
- "stage": f"{len(returned)} ledger(s) ready from Tally master snapshot",
+ "stage": f"{len(returned)} ledger(s) ready from local SQLite",
"percent": 100,
- "company_name": company.name,
+ "company_name": resolved_name or expected_company_name,
"ledger_count": len(returned),
+ "source": "Local .act SQLite",
})
return {
"status": "completed",
- "company": company.as_dict(),
- "company_name": company.name,
+ "company": {
+ "guid": resolved_guid,
+ "name": resolved_name or expected_company_name,
+ },
+ "company_name": resolved_name or expected_company_name,
"company_guid": resolved_guid,
- "selected_guid_refreshed": guid_refreshed,
+ "selected_guid_refreshed": bool(guid_refreshed),
"ledgers": returned,
"cash_candidates": cash_ledgers,
"ledger_count": len(returned),
"total_ledgers_examined": len(all_ledgers),
"cash_ledger_count": len(cash_ledgers),
"cash_only": ledger_scope != "all",
- "from_cache": False,
- "source": "opening_balance_master_snapshot",
+ "from_cache": not refreshed,
+ "master_refreshed": refreshed,
+ "master_synced_at_utc": synced_at,
+ "source": "common_act_sqlite_master",
"read_only": True,
"agent": self._agent_info(),
}
@@ -845,10 +978,11 @@ class AgentCommandProcessor:
"job_id": progress_id,
"job_kind": "cash_ledger_discovery",
"status": "failed",
- "stage": "Tally master snapshot failed",
+ "stage": "Cash ledger preparation failed",
"percent": 100,
"company_name": expected_company_name,
"ledger_count": 0,
+ "source": "Local .act SQLite",
"error": str(exc),
})
raise
@@ -1503,31 +1637,60 @@ class AgentCommandProcessor:
status="running", stage="Selected Tally company resolved by name", current_date=""
)
- self._cash_cache_update(client_id, job_id, status="running", stage="Reading cash ledger masters", current_date="")
- # One master request only, then a generous cooling gap before vouchers.
- masters = None
- master_error = None
- for attempt in range(1, 3):
- try:
- masters = self.tally.export_master_collection(company_name, "ledgers") or []
- self._cash_cache_update(client_id, job_id, tally_requests=(self._cash_cache_job_row(client_id, job_id) or {}).get("tally_requests", 0) + 1)
- break
- except Exception as exc:
- master_error = exc
- row = self._cash_cache_job_row(client_id, job_id) or {}
- self._cash_cache_update(client_id, job_id, retry_count=int(row.get("retry_count") or 0) + 1, stage="Cooling after slow Tally response")
- time.sleep(15.0)
- if masters is None:
- raise ValueError(f"TallyPrime did not return ledger masters after a slow retry. Details: {master_error}")
- master_by_key = {str(x.get("name") or "").strip().casefold(): str(x.get("name") or "").strip() for x in masters if str(x.get("name") or "").strip()}
- requested_ledgers = [str(x or "").strip() for x in (payload.get("cash_ledger_names") or []) if str(x or "").strip()]
+ # Ledger identity is already known from the common .act SQLite master
+ # snapshot used by Opening Balance/Depreciation. Do not ask TallyPrime
+ # for the ledger master again before voucher extraction.
+ self._cash_cache_update(
+ client_id,
+ job_id,
+ status="running",
+ stage="Confirming selected Cash ledger from local SQLite",
+ current_date="",
+ )
+ requested_ledgers = [
+ str(x or "").strip()
+ for x in (payload.get("cash_ledger_names") or [])
+ if str(x or "").strip()
+ ]
+ if not requested_ledgers:
+ raise ValueError("Select at least one Cash-in-Hand ledger before starting extraction.")
+
+ with self.store.connect(client_id) as db:
+ local_rows = db.execute(
+ """SELECT name
+ FROM tally_ledgers
+ WHERE tally_guid=?""",
+ (str(payload.get("tally_guid") or "").strip(),),
+ ).fetchall()
+ if not local_rows and company_name:
+ local_rows = db.execute(
+ """SELECT name
+ FROM tally_ledgers
+ WHERE lower(trim(company_name))=lower(trim(?))""",
+ (company_name,),
+ ).fetchall()
+
+ master_by_key = {
+ str(row["name"] or "").strip().casefold(): str(row["name"] or "").strip()
+ for row in local_rows
+ if str(row["name"] or "").strip()
+ }
missing = [x for x in requested_ledgers if x.casefold() not in master_by_key]
if missing:
- raise ValueError("The confirmed Cash ledger is no longer available in TallyPrime: " + ", ".join(missing))
+ raise ValueError(
+ "The selected Cash ledger is not present in the local accounting master snapshot: "
+ + ", ".join(missing)
+ + ". Click Refresh Master Data once and retry."
+ )
+
cash_ledgers = {x.casefold() for x in requested_ledgers}
confirmed_names = [master_by_key[x.casefold()] for x in requested_ledgers]
- self._cash_cache_update(client_id, job_id, cash_ledgers_json=json.dumps(confirmed_names), stage="Cash ledger confirmed; cooling before voucher extraction")
- time.sleep(max(5.0, pause_seconds))
+ self._cash_cache_update(
+ client_id,
+ job_id,
+ cash_ledgers_json=json.dumps(confirmed_names),
+ stage="Cash ledger confirmed from SQLite; preparing voucher extraction",
+ )
with self.store.connect(client_id) as db:
pending = db.execute(