Add direct Cash-in-Hand ledger selection for cash payment review
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):
|
||||||
"""Return cached ledgers immediately or start a non-blocking Tally master job."""
|
"""Load Cash-in-Hand ledgers directly; keep full-ledger sync only as manual fallback."""
|
||||||
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()
|
||||||
@@ -268,32 +268,46 @@ async def cash_payment_ledgers(request: Request):
|
|||||||
selected_client = next((row for row in clients if int(row.id) == client_id), None)
|
selected_client = next((row for row in clients if int(row.id) == client_id), None)
|
||||||
if not selected_client:
|
if not selected_client:
|
||||||
return JSONResponse({"ok": False, "error": "Select a valid client."}, status_code=400)
|
return JSONResponse({"ok": False, "error": "Select a valid client."}, status_code=400)
|
||||||
|
|
||||||
tally_guid = str(form.get("tally_guid") or "").strip()
|
tally_guid = str(form.get("tally_guid") or "").strip()
|
||||||
company_name = str(form.get("company_name") or "").strip()
|
company_name = str(form.get("company_name") or "").strip()
|
||||||
date_from = str(form.get("date_from") or date.today().isoformat()).strip()
|
date_from = str(form.get("date_from") or date.today().isoformat()).strip()
|
||||||
force_refresh = str(form.get("force_refresh") or "").strip().lower() in {"1","true","yes","on"}
|
ledger_scope = str(form.get("ledger_scope") or "cash").strip().lower()
|
||||||
|
force_refresh = str(form.get("force_refresh") or "").strip().lower() in {"1", "true", "yes", "on"}
|
||||||
if not tally_guid:
|
if not tally_guid:
|
||||||
return JSONResponse({"ok": False, "error": "Select an open Tally company."}, status_code=400)
|
return JSONResponse({"ok": False, "error": "Select an open Tally company."}, status_code=400)
|
||||||
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
|
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
|
||||||
if not node or not _node_online(node):
|
if not node or not _node_online(node):
|
||||||
return JSONResponse({"ok": False, "error": "ERP Local Agent is offline."}, status_code=409)
|
return JSONResponse({"ok": False, "error": "ERP Local Agent is offline."}, status_code=409)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fy = _financial_year_for_date(date.fromisoformat(date_from))
|
fy = _financial_year_for_date(date.fromisoformat(date_from))
|
||||||
result = request_agent_command(
|
payload = {
|
||||||
node.node_code,
|
**_accounting_storage_payload(selected_client, fy),
|
||||||
"accounting_cash_payment_ledgers_start",
|
"tally_guid": tally_guid,
|
||||||
{
|
"company_name": company_name,
|
||||||
**_accounting_storage_payload(selected_client, fy),
|
}
|
||||||
"tally_guid": tally_guid,
|
if ledger_scope == "all":
|
||||||
"company_name": company_name,
|
# Explicit manual fallback preserves the existing full-master async flow.
|
||||||
"force_refresh": force_refresh,
|
payload["force_refresh"] = force_refresh
|
||||||
},
|
result = request_agent_command(
|
||||||
timeout_seconds=20,
|
node.node_code,
|
||||||
)
|
"accounting_cash_payment_ledgers_start",
|
||||||
|
payload,
|
||||||
|
timeout_seconds=20,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Normal path: small filtered Tally collection, no full ledger cache.
|
||||||
|
result = request_agent_command(
|
||||||
|
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"):
|
||||||
return JSONResponse({"ok": False, "error": str(result.get("error") or "Could not start Tally ledger sync.")}, status_code=409)
|
return JSONResponse({"ok": False, "error": str(result.get("error") or "Could not load Tally ledgers.")}, status_code=409)
|
||||||
return JSONResponse({"ok": True, **(result.get("result") or {})})
|
return JSONResponse({"ok": True, **(result.get("result") or {})})
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-xs font-semibold uppercase text-slate-500">Cash Ledger</label>
|
<label class="text-xs font-semibold uppercase text-slate-500">Cash Ledger</label>
|
||||||
<select id="cash-ledger-scope" class="mt-1 w-full rounded-xl border-slate-300 text-xs">
|
<select id="cash-ledger-scope" name="ledger_scope" class="mt-1 w-full rounded-xl border-slate-300 text-xs">
|
||||||
<option value="cash" selected>Ledgers under Cash-in-Hand</option>
|
<option value="cash" selected>Ledgers under Cash-in-Hand</option>
|
||||||
<option value="all">All Tally ledgers — manual selection</option>
|
<option value="all">All Tally ledgers — manual selection</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -306,15 +306,27 @@
|
|||||||
const companyOption = companySelect && companySelect.options[companySelect.selectedIndex];
|
const companyOption = companySelect && companySelect.options[companySelect.selectedIndex];
|
||||||
const companyName = companyOption ? String(companyOption.dataset.companyName || companyOption.textContent || '').trim() : '';
|
const companyName = companyOption ? String(companyOption.dataset.companyName || companyOption.textContent || '').trim() : '';
|
||||||
if (companyName) fd.set('company_name', companyName);
|
if (companyName) fd.set('company_name', companyName);
|
||||||
|
const selectedScope = ledgerScope ? String(ledgerScope.value || 'cash') : 'cash';
|
||||||
|
fd.set('ledger_scope', selectedScope);
|
||||||
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 = forceRefresh ? 'Starting background refresh from Tally…' : 'Loading cached ledgers or starting background Tally sync…';
|
ledgerHelp.textContent = selectedScope === 'all'
|
||||||
|
? (forceRefresh ? 'Starting background full-ledger refresh from Tally…' : 'Loading the full ledger list for manual selection…')
|
||||||
|
: 'Reading Cash-in-Hand ledgers directly from the selected Tally company…';
|
||||||
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();
|
||||||
if (!response.ok || !data.ok) throw new Error(data.error || 'Could not load Tally ledgers.');
|
if (!response.ok || !data.ok) throw new Error(data.error || 'Could not load Tally ledgers.');
|
||||||
if ((data.ledgers||[]).length) {
|
if (Array.isArray(data.ledgers)) {
|
||||||
populateLedgers(data); ledgerButton.disabled=false; if(ledgerRefreshButton) ledgerRefreshButton.disabled=false; return;
|
populateLedgers(data);
|
||||||
|
ledgerButton.disabled=false; if(ledgerRefreshButton) ledgerRefreshButton.disabled=false;
|
||||||
|
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';
|
||||||
|
window.setTimeout(()=>ledgerProgress.classList.add('hidden'),1200);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const job=data.job||{};
|
const job=data.job||{};
|
||||||
if(!job.job_id) throw new Error('Local Agent did not return a ledger-sync job id.');
|
if(!job.job_id) throw new Error('Local Agent did not return a ledger-sync job id.');
|
||||||
@@ -328,7 +340,10 @@
|
|||||||
if (ledgerButton) ledgerButton.addEventListener('click', function(){loadCashLedgers(false);});
|
if (ledgerButton) ledgerButton.addEventListener('click', function(){loadCashLedgers(false);});
|
||||||
if (ledgerRefreshButton) ledgerRefreshButton.addEventListener('click', function(){loadCashLedgers(true);});
|
if (ledgerRefreshButton) ledgerRefreshButton.addEventListener('click', function(){loadCashLedgers(true);});
|
||||||
const companySelect = document.getElementById('cash-tally-company');
|
const companySelect = document.getElementById('cash-tally-company');
|
||||||
if (ledgerScope) ledgerScope.addEventListener('change', function(){ renderLedgerOptions(); });
|
if (ledgerScope) ledgerScope.addEventListener('change', function(){
|
||||||
|
ledgerSelect.innerHTML = '<option value="">Select Cash ledger</option>';
|
||||||
|
if (companySelect && companySelect.value) loadCashLedgers(false);
|
||||||
|
});
|
||||||
if (companySelect) companySelect.addEventListener('change', function(){
|
if (companySelect) companySelect.addEventListener('change', function(){
|
||||||
allLedgerRows = []; cashLedgerRows = []; lastLedgerMeta = {ledger_count:0, company_name:'', from_cache:false};
|
allLedgerRows = []; cashLedgerRows = []; lastLedgerMeta = {ledger_count:0, company_name:'', from_cache:false};
|
||||||
ledgerSelect.innerHTML = '<option value="">Select Cash ledger</option>';
|
ledgerSelect.innerHTML = '<option value="">Select Cash ledger</option>';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import io
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
ERP_LOCAL_AGENT_VERSION = "1.22.17"
|
ERP_LOCAL_AGENT_VERSION = "1.22.18"
|
||||||
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.17"
|
__version__ = "1.22.18"
|
||||||
AGENT_NAME = "ERP Local Agent"
|
AGENT_NAME = "ERP Local Agent"
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ class AgentCommandProcessor:
|
|||||||
result = self._bank_posting_preflight(payload)
|
result = self._bank_posting_preflight(payload)
|
||||||
elif action == "accounting_bank_reconciliation_extract":
|
elif action == "accounting_bank_reconciliation_extract":
|
||||||
result = self._bank_reconciliation_extract(payload)
|
result = self._bank_reconciliation_extract(payload)
|
||||||
|
elif action == "accounting_cash_payment_cash_ledgers":
|
||||||
|
result = self._cash_payment_cash_ledgers(payload)
|
||||||
elif action == "accounting_cash_payment_ledgers":
|
elif action == "accounting_cash_payment_ledgers":
|
||||||
result = self._cash_payment_ledgers(payload)
|
result = self._cash_payment_ledgers(payload)
|
||||||
elif action == "accounting_cash_payment_ledgers_start":
|
elif action == "accounting_cash_payment_ledgers_start":
|
||||||
@@ -716,6 +718,61 @@ class AgentCommandProcessor:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _cash_payment_cash_ledgers(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Discover Cash-in-Hand ledgers directly from the selected open company.
|
||||||
|
|
||||||
|
The normal Cash Payment workflow must not depend on a full Tally ledger-master
|
||||||
|
export. This command uses a narrow Tally collection and returns only the
|
||||||
|
Cash-in-Hand hierarchy. The existing full-ledger async cache remains available
|
||||||
|
for the explicit manual-selection fallback.
|
||||||
|
"""
|
||||||
|
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()
|
||||||
|
progress_id = "CASHDISC-" + uuid.uuid4().hex[:12].upper()
|
||||||
|
_cash_progress_snapshot({
|
||||||
|
"job_id": progress_id, "job_kind": "cash_ledger_discovery",
|
||||||
|
"status": "running", "stage": "Reading Cash-in-Hand ledgers from Tally",
|
||||||
|
"percent": 25, "company_name": expected_company_name, "ledger_count": 0,
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
company, company_name = self._resolve_open_company(payload)
|
||||||
|
rows = self.tally.cash_in_hand_ledgers(company_name) or []
|
||||||
|
ledgers = self._cash_ledger_rows(rows)
|
||||||
|
# Every row returned by this filtered Tally collection belongs to the
|
||||||
|
# Cash-in-Hand hierarchy; mark it as a candidate even when it sits below
|
||||||
|
# a user-created child group rather than directly under Cash-in-Hand.
|
||||||
|
for row in ledgers:
|
||||||
|
row["is_cash_candidate"] = True
|
||||||
|
_cash_progress_snapshot({
|
||||||
|
"job_id": progress_id, "job_kind": "cash_ledger_discovery",
|
||||||
|
"status": "completed",
|
||||||
|
"stage": f"{len(ledgers)} Cash-in-Hand ledger(s) ready",
|
||||||
|
"percent": 100, "company_name": company_name, "ledger_count": len(ledgers),
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
"status": "completed",
|
||||||
|
"company": {"guid": str(company.get("guid") or tally_guid), "name": company_name},
|
||||||
|
"company_name": company_name,
|
||||||
|
"ledgers": ledgers,
|
||||||
|
"cash_candidates": ledgers,
|
||||||
|
"ledger_count": len(ledgers),
|
||||||
|
"cash_only": True,
|
||||||
|
"from_cache": False,
|
||||||
|
"read_only": True,
|
||||||
|
"agent": self._agent_info(),
|
||||||
|
}
|
||||||
|
except Exception as exc:
|
||||||
|
_cash_progress_snapshot({
|
||||||
|
"job_id": progress_id, "job_kind": "cash_ledger_discovery",
|
||||||
|
"status": "failed", "stage": "Cash-in-Hand ledger discovery failed",
|
||||||
|
"percent": 100, "company_name": expected_company_name,
|
||||||
|
"ledger_count": 0, "error": str(exc),
|
||||||
|
})
|
||||||
|
raise
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _cash_ledger_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
def _cash_ledger_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
ledgers: list[dict[str, Any]] = []
|
ledgers: list[dict[str, Any]] = []
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ function gb(n){return (n/1073741824).toFixed(1)+' GB'}
|
|||||||
function setProgress(title,pct,text,active=true){let b=document.getElementById('progressBox');b.style.display='block';document.getElementById('progressTitle').textContent=title;let p=Math.max(0,Math.min(100,Number(pct)||0));lastServerPercent=Math.max(lastServerPercent,p);document.getElementById('progressPct').textContent=Math.round(p)+'%';document.getElementById('progressFill').style.width=p+'%';document.getElementById('progressMsg').textContent=text||'';document.getElementById('progressFill').className='progressfill'+(active&&p<100?' pulse':'')}
|
function setProgress(title,pct,text,active=true){let b=document.getElementById('progressBox');b.style.display='block';document.getElementById('progressTitle').textContent=title;let p=Math.max(0,Math.min(100,Number(pct)||0));lastServerPercent=Math.max(lastServerPercent,p);document.getElementById('progressPct').textContent=Math.round(p)+'%';document.getElementById('progressFill').style.width=p+'%';document.getElementById('progressMsg').textContent=text||'';document.getElementById('progressFill').className='progressfill'+(active&&p<100?' pulse':'')}
|
||||||
function applyProgress(u){let p=(u&&u.progress)||{};if(!p.operation)return;let title=p.operation==='check'?'Checking for Updates':p.operation==='download'?'Downloading Update':'Applying Update';setProgress(title,p.percent||0,p.message||p.phase||'',!['updated','downloaded','current','available','error'].includes(p.status));if(p.operation==='install'&&p.target_version){installTarget=p.target_version}}
|
function applyProgress(u){let p=(u&&u.progress)||{};if(!p.operation)return;let title=p.operation==='check'?'Checking for Updates':p.operation==='download'?'Downloading Update':'Applying Update';setProgress(title,p.percent||0,p.message||p.phase||'',!['updated','downloaded','current','available','error'].includes(p.status));if(p.operation==='install'&&p.target_version){installTarget=p.target_version}}
|
||||||
function setCashStep(id,state){let e=document.getElementById(id);e.className='cashstep'+(state?' '+state:'')}
|
function setCashStep(id,state){let e=document.getElementById(id);e.className='cashstep'+(state?' '+state:'')}
|
||||||
async function refreshCashProgress(){try{const r=await fetch('/api/cash-progress',{cache:'no-store'});const d=await r.json();const j=d.job||{};if(!j.job_id){document.getElementById('cashJobSection').style.display='none';return}document.getElementById('cashJobSection').style.display='block';let p=Math.max(0,Math.min(100,Number(j.percent)||0));document.getElementById('cashJobPct').textContent=Math.round(p)+'%';document.getElementById('cashJobFill').style.width=p+'%';document.getElementById('cashJobStage').textContent=j.stage||j.status||'Working…';let isLedger=String(j.job_kind||'')==='ledger_master';document.querySelector('#cashJobSection h2').textContent=isLedger?'Cash Payment — Tally Ledger Sync':'Cash Payment Compliance';document.getElementById('cashJobSub').textContent=(j.company_name||'Tally company')+(j.cash_ledgers&&j.cash_ledgers.length?' • Cash ledger: '+j.cash_ledgers.join(', '):'');if(isLedger){document.getElementById('cashDays').parentElement.querySelector('.label').textContent='Ledgers';document.getElementById('cashDays').textContent=String(j.ledger_count||0);document.getElementById('cashVouchers').parentElement.querySelector('.label').textContent='Status';document.getElementById('cashVouchers').textContent=j.status||'working';document.getElementById('cashRequests').parentElement.querySelector('.label').textContent='Progress';document.getElementById('cashRequests').textContent=Math.round(p)+'%';document.getElementById('cashDate').parentElement.querySelector('.label').textContent='Source';document.getElementById('cashDate').textContent='Tally ledger master';}else{document.getElementById('cashDays').parentElement.querySelector('.label').textContent='Days';document.getElementById('cashDays').textContent=String(j.completed_days||0)+' / '+String(j.total_days||0);document.getElementById('cashVouchers').parentElement.querySelector('.label').textContent='Cash vouchers';document.getElementById('cashVouchers').textContent=String(j.vouchers_cached||0);document.getElementById('cashRequests').parentElement.querySelector('.label').textContent='Tally requests';document.getElementById('cashRequests').textContent=String(j.tally_requests||0);document.getElementById('cashDate').parentElement.querySelector('.label').textContent='Current date';document.getElementById('cashDate').textContent=j.current_date||'—';}let st=String(j.stage||'').toLowerCase(),status=String(j.status||'').toLowerCase();if(isLedger){setCashStep('cs1',(p>=15?'done':(p>0?'run':'')));setCashStep('cs2','');setCashStep('cs3','');setCashStep('cs4','');setCashStep('cs5','');}else{setCashStep('cs1',(p>=10?'done':(p>0?'run':'')));setCashStep('cs2',(p>=20?'done':(st.includes('ledger')?'run':'')));setCashStep('cs3',(p>=93?'done':(p>=20?'run':'')));setCashStep('cs4',(status==='completed'?'done':(st.includes('analys')?'run':'')));setCashStep('cs5',(status==='completed'?'done':''));}let er=document.getElementById('cashError');if(j.error){er.style.display='block';er.textContent=j.error}else{er.style.display='none'}}catch(e){}}
|
async function refreshCashProgress(){try{const r=await fetch('/api/cash-progress',{cache:'no-store'});const d=await r.json();const j=d.job||{};if(!j.job_id){document.getElementById('cashJobSection').style.display='none';return}document.getElementById('cashJobSection').style.display='block';let p=Math.max(0,Math.min(100,Number(j.percent)||0));document.getElementById('cashJobPct').textContent=Math.round(p)+'%';document.getElementById('cashJobFill').style.width=p+'%';document.getElementById('cashJobStage').textContent=j.stage||j.status||'Working…';let kind=String(j.job_kind||'');let isLedger=(kind==='ledger_master'||kind==='cash_ledger_discovery');document.querySelector('#cashJobSection h2').textContent=isLedger?'Cash Payment — Tally Ledger Sync':'Cash Payment Compliance';document.getElementById('cashJobSub').textContent=(j.company_name||'Tally company')+(j.cash_ledgers&&j.cash_ledgers.length?' • Cash ledger: '+j.cash_ledgers.join(', '):'');if(isLedger){document.getElementById('cashDays').parentElement.querySelector('.label').textContent='Ledgers';document.getElementById('cashDays').textContent=String(j.ledger_count||0);document.getElementById('cashVouchers').parentElement.querySelector('.label').textContent='Status';document.getElementById('cashVouchers').textContent=j.status||'working';document.getElementById('cashRequests').parentElement.querySelector('.label').textContent='Progress';document.getElementById('cashRequests').textContent=Math.round(p)+'%';document.getElementById('cashDate').parentElement.querySelector('.label').textContent='Source';document.getElementById('cashDate').textContent=(kind==='cash_ledger_discovery'?'Cash-in-Hand':'Tally ledger master');}else{document.getElementById('cashDays').parentElement.querySelector('.label').textContent='Days';document.getElementById('cashDays').textContent=String(j.completed_days||0)+' / '+String(j.total_days||0);document.getElementById('cashVouchers').parentElement.querySelector('.label').textContent='Cash vouchers';document.getElementById('cashVouchers').textContent=String(j.vouchers_cached||0);document.getElementById('cashRequests').parentElement.querySelector('.label').textContent='Tally requests';document.getElementById('cashRequests').textContent=String(j.tally_requests||0);document.getElementById('cashDate').parentElement.querySelector('.label').textContent='Current date';document.getElementById('cashDate').textContent=j.current_date||'—';}let st=String(j.stage||'').toLowerCase(),status=String(j.status||'').toLowerCase();if(isLedger){setCashStep('cs1',(p>=15?'done':(p>0?'run':'')));setCashStep('cs2','');setCashStep('cs3','');setCashStep('cs4','');setCashStep('cs5','');}else{setCashStep('cs1',(p>=10?'done':(p>0?'run':'')));setCashStep('cs2',(p>=20?'done':(st.includes('ledger')?'run':'')));setCashStep('cs3',(p>=93?'done':(p>=20?'run':'')));setCashStep('cs4',(status==='completed'?'done':(st.includes('analys')?'run':'')));setCashStep('cs5',(status==='completed'?'done':''));}let er=document.getElementById('cashError');if(j.error){er.style.display='block';er.textContent=j.error}else{er.style.display='none'}}catch(e){}}
|
||||||
async function refresh(){try{const r=await fetch('/api/status',{cache:'no-store'});if(!r.ok)throw new Error('status unavailable');const s=await r.json();offlineTicks=0;
|
async function refresh(){try{const r=await fetch('/api/status',{cache:'no-store'});if(!r.ok)throw new Error('status unavailable');const s=await r.json();offlineTicks=0;
|
||||||
let c=s.agent.connection_state||'unknown';let ce=document.getElementById('connection');ce.textContent=c.charAt(0).toUpperCase()+c.slice(1);ce.className='value '+(c==='connected'?'ok':'bad');
|
let c=s.agent.connection_state||'unknown';let ce=document.getElementById('connection');ce.textContent=c.charAt(0).toUpperCase()+c.slice(1);ce.className='value '+(c==='connected'?'ok':'bad');
|
||||||
document.getElementById('heartbeat').textContent=s.agent.last_heartbeat_utc?'Last heartbeat: '+s.agent.last_heartbeat_utc:'No heartbeat recorded';
|
document.getElementById('heartbeat').textContent=s.agent.last_heartbeat_utc?'Last heartbeat: '+s.agent.last_heartbeat_utc:'No heartbeat recorded';
|
||||||
|
|||||||
@@ -327,6 +327,34 @@ class TallyLiveConnector:
|
|||||||
raw = self._post_xml(xml)
|
raw = self._post_xml(xml)
|
||||||
return self._parse_master_rows(raw, spec["tag"])
|
return self._parse_master_rows(raw, spec["tag"])
|
||||||
|
|
||||||
|
def cash_in_hand_ledgers(self, company_name: str) -> list[dict]:
|
||||||
|
"""Return only ledgers belonging to Tally's Cash-in-Hand group.
|
||||||
|
|
||||||
|
This intentionally avoids enumerating the complete ledger master. Tally's
|
||||||
|
CHILDOF + BELONGSTO collection semantics include ledgers held directly under
|
||||||
|
Cash-in-Hand as well as ledgers under descendant groups.
|
||||||
|
"""
|
||||||
|
company = str(company_name or "").strip()
|
||||||
|
if not company:
|
||||||
|
raise ValueError("Tally company name is required for Cash-in-Hand ledger discovery.")
|
||||||
|
spec = self.MASTER_SPECS["ledgers"]
|
||||||
|
xml = f"""<ENVELOPE>
|
||||||
|
<HEADER><VERSION>1</VERSION><TALLYREQUEST>Export</TALLYREQUEST><TYPE>Collection</TYPE><ID>ARRRCashInHandLedgers</ID></HEADER>
|
||||||
|
<BODY><DESC>
|
||||||
|
<STATICVARIABLES>{self._static_variables(company)}</STATICVARIABLES>
|
||||||
|
<TDL><TDLMESSAGE>
|
||||||
|
<COLLECTION NAME="ARRRCashInHandLedgers" ISMODIFY="No">
|
||||||
|
<TYPE>Ledger</TYPE>
|
||||||
|
<CHILDOF>Cash-in-Hand</CHILDOF>
|
||||||
|
<BELONGSTO>Yes</BELONGSTO>
|
||||||
|
<FETCH>{spec['fetch']}</FETCH>
|
||||||
|
</COLLECTION>
|
||||||
|
</TDLMESSAGE></TDL>
|
||||||
|
</DESC></BODY>
|
||||||
|
</ENVELOPE>"""
|
||||||
|
raw = self._post_xml(xml)
|
||||||
|
return self._parse_master_rows(raw, spec["tag"])
|
||||||
|
|
||||||
def fetch_accounting_masters(self, company_name: str) -> dict[str, list[dict]]:
|
def fetch_accounting_masters(self, company_name: str) -> dict[str, list[dict]]:
|
||||||
result: dict[str, list[dict]] = {}
|
result: dict[str, list[dict]] = {}
|
||||||
for key in self.MASTER_SPECS:
|
for key in self.MASTER_SPECS:
|
||||||
|
|||||||
Reference in New Issue
Block a user