Reuse opening balance Tally master flow for cash ledger selection
This commit is contained in:
@@ -252,7 +252,7 @@ def cash_payment_review(
|
|||||||
|
|
||||||
@router.post("/ledgers")
|
@router.post("/ledgers")
|
||||||
async def cash_payment_ledgers(request: Request):
|
async def cash_payment_ledgers(request: Request):
|
||||||
"""Load Cash-in-Hand ledgers directly; keep full-ledger sync only as manual fallback."""
|
"""Load Cash ledgers using the same live Tally master snapshot path as Opening Balance Review."""
|
||||||
form = await request.form()
|
form = await request.form()
|
||||||
validate_csrf(request, str(form.get("csrf_token") or ""))
|
validate_csrf(request, str(form.get("csrf_token") or ""))
|
||||||
db = CommonSessionLocal()
|
db = CommonSessionLocal()
|
||||||
@@ -287,23 +287,19 @@ async def cash_payment_ledgers(request: Request):
|
|||||||
"tally_guid": tally_guid,
|
"tally_guid": tally_guid,
|
||||||
"company_name": company_name,
|
"company_name": company_name,
|
||||||
}
|
}
|
||||||
if ledger_scope == "all":
|
# Use the same proven live Tally master-snapshot mechanism used by
|
||||||
# Explicit manual fallback preserves the existing full-master async flow.
|
# Opening Balance Review. The Local Agent resolves the selected open
|
||||||
payload["force_refresh"] = force_refresh
|
# company, reads the standard Tally masters and returns either only
|
||||||
result = request_agent_command(
|
# Cash-in-Hand ledgers or all ledgers for explicit manual selection.
|
||||||
node.node_code,
|
# No separate cash-ledger cache/job is required for this selector.
|
||||||
"accounting_cash_payment_ledgers_start",
|
payload["ledger_scope"] = ledger_scope
|
||||||
payload,
|
payload["force_refresh"] = force_refresh
|
||||||
timeout_seconds=20,
|
result = request_agent_command(
|
||||||
)
|
node.node_code,
|
||||||
else:
|
"accounting_cash_payment_cash_ledgers",
|
||||||
# Normal path: small filtered Tally collection, no full ledger cache.
|
payload,
|
||||||
result = request_agent_command(
|
timeout_seconds=180,
|
||||||
node.node_code,
|
)
|
||||||
"accounting_cash_payment_cash_ledgers",
|
|
||||||
payload,
|
|
||||||
timeout_seconds=30,
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return JSONResponse({"ok": False, "error": str(exc)}, status_code=502)
|
return JSONResponse({"ok": False, "error": str(exc)}, status_code=502)
|
||||||
if not result.get("ok"):
|
if not result.get("ok"):
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
<button type="button" id="cash-load-ledgers" class="rounded-xl border border-slate-300 bg-white px-3 py-2 text-xs font-semibold text-slate-700">Load Cash Ledgers</button>
|
<button type="button" id="cash-load-ledgers" class="rounded-xl border border-slate-300 bg-white px-3 py-2 text-xs font-semibold text-slate-700">Load Cash Ledgers</button>
|
||||||
<button type="button" id="cash-refresh-ledgers" class="rounded-xl border border-slate-300 bg-white px-3 py-2 text-xs font-semibold text-slate-700">Refresh</button>
|
<button type="button" id="cash-refresh-ledgers" class="rounded-xl border border-slate-300 bg-white px-3 py-2 text-xs font-semibold text-slate-700">Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
<p id="cash-ledger-help" class="mt-1 text-xs text-slate-500">By default only ledgers classified under Cash-in-Hand are shown. Choose “All Tally ledgers” only when you need to manually select a ledger that Tally has grouped differently.</p>
|
<p id="cash-ledger-help" class="mt-1 text-xs text-slate-500">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.</p>
|
||||||
<div id="cash-ledger-progress" class="mt-2 hidden rounded-xl border border-sky-200 bg-sky-50 p-3">
|
<div id="cash-ledger-progress" class="mt-2 hidden rounded-xl border border-sky-200 bg-sky-50 p-3">
|
||||||
<div class="flex items-center justify-between text-xs font-semibold text-sky-900"><span id="cash-ledger-progress-stage">Preparing Cash ledger list…</span><span id="cash-ledger-progress-pct">0%</span></div>
|
<div class="flex items-center justify-between text-xs font-semibold text-sky-900"><span id="cash-ledger-progress-stage">Preparing Cash ledger list…</span><span id="cash-ledger-progress-pct">0%</span></div>
|
||||||
<div class="mt-2 h-2 overflow-hidden rounded-full bg-sky-100"><div id="cash-ledger-progress-bar" class="h-full rounded-full bg-sky-600 transition-all duration-500" style="width:0%"></div></div>
|
<div class="mt-2 h-2 overflow-hidden rounded-full bg-sky-100"><div id="cash-ledger-progress-bar" class="h-full rounded-full bg-sky-600 transition-all duration-500" style="width:0%"></div></div>
|
||||||
@@ -311,8 +311,8 @@
|
|||||||
if(forceRefresh) fd.set('force_refresh','1');
|
if(forceRefresh) fd.set('force_refresh','1');
|
||||||
ledgerButton.disabled = true; if(ledgerRefreshButton) ledgerRefreshButton.disabled=true;
|
ledgerButton.disabled = true; if(ledgerRefreshButton) ledgerRefreshButton.disabled=true;
|
||||||
ledgerHelp.textContent = selectedScope === 'all'
|
ledgerHelp.textContent = selectedScope === 'all'
|
||||||
? (forceRefresh ? 'Starting background full-ledger refresh from Tally…' : 'Loading the full ledger list for manual selection…')
|
? 'Reading all Tally ledgers using the proven master snapshot…'
|
||||||
: 'Reading Cash-in-Hand ledgers directly from the selected Tally company…';
|
: 'Reading Tally masters and selecting Cash-in-Hand ledgers…';
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/tools/accounting/cash-payments/ledgers', {method:'POST', body:fd, headers:{'Accept':'application/json'}});
|
const response = await fetch('/tools/accounting/cash-payments/ledgers', {method:'POST', body:fd, headers:{'Accept':'application/json'}});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import io
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
ERP_LOCAL_AGENT_VERSION = "1.22.18"
|
ERP_LOCAL_AGENT_VERSION = "1.22.19"
|
||||||
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.18"
|
__version__ = "1.22.19"
|
||||||
AGENT_NAME = "ERP Local Agent"
|
AGENT_NAME = "ERP Local Agent"
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ class AgentCommandProcessor:
|
|||||||
"cash_payment_async_ledger_master_capability": True,
|
"cash_payment_async_ledger_master_capability": True,
|
||||||
"cash_payment_ledger_cache_capability": True,
|
"cash_payment_ledger_cache_capability": True,
|
||||||
"cash_payment_open_company_selection_capability": True,
|
"cash_payment_open_company_selection_capability": True,
|
||||||
|
"cash_payment_opening_balance_master_path_capability": True,
|
||||||
"opening_balance_balance_sheet_only_capability": True,
|
"opening_balance_balance_sheet_only_capability": True,
|
||||||
"tally_writeback_capability": True,
|
"tally_writeback_capability": True,
|
||||||
}
|
}
|
||||||
@@ -719,57 +720,115 @@ class AgentCommandProcessor:
|
|||||||
|
|
||||||
|
|
||||||
def _cash_payment_cash_ledgers(self, payload: dict[str, Any]) -> dict[str, Any]:
|
def _cash_payment_cash_ledgers(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Discover Cash-in-Hand ledgers directly from the selected open company.
|
"""Return Cash-in-Hand ledgers using the proven Opening Balance master path.
|
||||||
|
|
||||||
The normal Cash Payment workflow must not depend on a full Tally ledger-master
|
Cash Payment no longer uses a separate Tally collection or a separate
|
||||||
export. This command uses a narrow Tally collection and returns only the
|
ledger-master job merely to populate the selector. It deliberately reuses
|
||||||
Cash-in-Hand hierarchy. The existing full-ledger async cache remains available
|
the same open-company resolution and ``fetch_accounting_masters`` call used
|
||||||
for the explicit manual-selection fallback.
|
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.
|
||||||
"""
|
"""
|
||||||
client_id = int(payload.get("client_id"))
|
|
||||||
tally_guid = str(payload.get("tally_guid") or "").strip()
|
|
||||||
if not tally_guid:
|
|
||||||
raise ValueError("Open Tally company GUID is required.")
|
|
||||||
expected_company_name = str(payload.get("company_name") or "").strip()
|
expected_company_name = str(payload.get("company_name") or "").strip()
|
||||||
progress_id = "CASHDISC-" + uuid.uuid4().hex[:12].upper()
|
expected_guid = str(payload.get("tally_guid") or "").strip()
|
||||||
|
ledger_scope = str(payload.get("ledger_scope") or "cash").strip().lower()
|
||||||
|
if ledger_scope not in {"cash", "all"}:
|
||||||
|
ledger_scope = "cash"
|
||||||
|
|
||||||
|
progress_id = "CASHMASTER-" + uuid.uuid4().hex[:12].upper()
|
||||||
_cash_progress_snapshot({
|
_cash_progress_snapshot({
|
||||||
"job_id": progress_id, "job_kind": "cash_ledger_discovery",
|
"job_id": progress_id,
|
||||||
"status": "running", "stage": "Reading Cash-in-Hand ledgers from Tally",
|
"job_kind": "cash_ledger_discovery",
|
||||||
"percent": 25, "company_name": expected_company_name, "ledger_count": 0,
|
"status": "running",
|
||||||
|
"stage": "Reading Tally masters using Opening Balance mechanism",
|
||||||
|
"percent": 20,
|
||||||
|
"company_name": expected_company_name,
|
||||||
|
"ledger_count": 0,
|
||||||
})
|
})
|
||||||
try:
|
try:
|
||||||
company, company_name = self._resolve_open_company(payload)
|
# This is intentionally the same company-resolution helper used by
|
||||||
rows = self.tally.cash_in_hand_ledgers(company_name) or []
|
# _opening_balance_snapshot(). Company name is the human selection;
|
||||||
ledgers = self._cash_ledger_rows(rows)
|
# GUID is verified when supplied so another open company cannot leak in.
|
||||||
# Every row returned by this filtered Tally collection belongs to the
|
company = self._opening_balance_company(expected_company_name)
|
||||||
# Cash-in-Hand hierarchy; mark it as a candidate even when it sits below
|
if expected_guid and company.guid and str(company.guid).strip() != expected_guid:
|
||||||
# a user-created child group rather than directly under Cash-in-Hand.
|
raise ValueError(
|
||||||
for row in ledgers:
|
"Selected Tally company GUID does not match the currently loaded company. "
|
||||||
row["is_cash_candidate"] = True
|
"Reload Tally companies and select the company again."
|
||||||
|
)
|
||||||
|
|
||||||
|
masters = self.tally.fetch_accounting_masters(company.name)
|
||||||
|
group_rows = list(masters.get("groups") or [])
|
||||||
|
ledger_rows = list(masters.get("ledgers") or [])
|
||||||
|
|
||||||
|
# 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
|
||||||
|
for group in group_rows:
|
||||||
|
name = str(group.get("name") or "").strip()
|
||||||
|
parent = str(group.get("parent") or "").strip()
|
||||||
|
if not name:
|
||||||
|
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)
|
||||||
|
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"
|
||||||
|
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()))
|
||||||
|
cash_ledgers.sort(key=lambda x: str(x.get("name") or "").casefold())
|
||||||
|
returned = all_ledgers if ledger_scope == "all" else cash_ledgers
|
||||||
|
|
||||||
_cash_progress_snapshot({
|
_cash_progress_snapshot({
|
||||||
"job_id": progress_id, "job_kind": "cash_ledger_discovery",
|
"job_id": progress_id,
|
||||||
|
"job_kind": "cash_ledger_discovery",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"stage": f"{len(ledgers)} Cash-in-Hand ledger(s) ready",
|
"stage": f"{len(returned)} ledger(s) ready from Tally master snapshot",
|
||||||
"percent": 100, "company_name": company_name, "ledger_count": len(ledgers),
|
"percent": 100,
|
||||||
|
"company_name": company.name,
|
||||||
|
"ledger_count": len(returned),
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"company": {"guid": str(company.get("guid") or tally_guid), "name": company_name},
|
"company": company.as_dict(),
|
||||||
"company_name": company_name,
|
"company_name": company.name,
|
||||||
"ledgers": ledgers,
|
"ledgers": returned,
|
||||||
"cash_candidates": ledgers,
|
"cash_candidates": cash_ledgers,
|
||||||
"ledger_count": len(ledgers),
|
"ledger_count": len(returned),
|
||||||
"cash_only": True,
|
"total_ledgers_examined": len(all_ledgers),
|
||||||
|
"cash_ledger_count": len(cash_ledgers),
|
||||||
|
"cash_only": ledger_scope != "all",
|
||||||
"from_cache": False,
|
"from_cache": False,
|
||||||
|
"source": "opening_balance_master_snapshot",
|
||||||
"read_only": True,
|
"read_only": True,
|
||||||
"agent": self._agent_info(),
|
"agent": self._agent_info(),
|
||||||
}
|
}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
_cash_progress_snapshot({
|
_cash_progress_snapshot({
|
||||||
"job_id": progress_id, "job_kind": "cash_ledger_discovery",
|
"job_id": progress_id,
|
||||||
"status": "failed", "stage": "Cash-in-Hand ledger discovery failed",
|
"job_kind": "cash_ledger_discovery",
|
||||||
"percent": 100, "company_name": expected_company_name,
|
"status": "failed",
|
||||||
"ledger_count": 0, "error": str(exc),
|
"stage": "Tally master snapshot failed",
|
||||||
|
"percent": 100,
|
||||||
|
"company_name": expected_company_name,
|
||||||
|
"ledger_count": 0,
|
||||||
|
"error": str(exc),
|
||||||
})
|
})
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user