Repair accounting mirror TDL and add stable TDL link

This commit is contained in:
A R R R Associates
2026-09-06 17:59:34 +05:30
parent 22ae95c529
commit 6411cb14fa
7 changed files with 106 additions and 111 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ import io
from pathlib import Path
import zipfile
ERP_LOCAL_AGENT_VERSION = "1.24.1"
ERP_LOCAL_AGENT_VERSION = "1.24.2"
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)
@@ -65,7 +65,7 @@ def _build_zip(*, env_text: str | None, include_env: bool, include_admin_readme:
text = (
f"ERP Local Agent {ERP_LOCAL_AGENT_VERSION}\n"
"Existing storage, WebSocket tunnel, dashboard, Tally mapping and client .act functionality are preserved.\n"
"Existing master/transaction sync and depreciation are preserved. Phase 5 adds neutral historical-ledger evidence read capability for server-side learning.\n"
"Existing master/transaction sync, accounting tools and controlled write-back are preserved.\nAccounting Mirror uses the proven v3.0.1 ODBC runtime for ledgers, vouchers, stock movement, HSN and GST rates.\nTally TDL is exposed through a stable local path and localhost URL so later agent updates can repair/replace the TDL without changing the Tally link.\n"
"Dashboard: http://127.0.0.1:8788\nDesktop dashboard is single-instance: repeated shortcut clicks focus the existing window without starting another Local Agent.\nUpdate dashboard shows check, download, install, restart and completion progress.\n"
)
_write_zip_bytes(dst, "README_ERP_LOCAL_AGENT.txt", text.encode("utf-8"))
@@ -1,2 +1,2 @@
__version__ = "1.24.1"
__version__ = "1.24.2"
AGENT_NAME = "ERP Local Agent"
@@ -14,7 +14,7 @@ import subprocess
import sys
from typing import Any
MIRROR_SCHEMA_VERSION = "3.1"
MIRROR_SCHEMA_VERSION = "3.0.1"
DEFAULT_DSN = "TallyODBC64_9000"
def _utc_now() -> str:
@@ -64,9 +64,9 @@ class AccountingMirrorEngine:
current_mirror=accounting_db.with_name(f"{stem}_mirror.act"),
cache_root=cache_root,
work_root=cache_root / "sync_work",
extractor_exe=runtime_dir / "TallyAccountingMirrorV31.exe",
tdl_file=self.runtime_root / "ERP_Accounting_Mirror_v3_1.tdl",
loader_file=self.runtime_root / "build_act_v3_1.py",
extractor_exe=runtime_dir / "TallyAccountingMirrorV301.exe",
tdl_file=self.runtime_root / "ERP_Accounting_Mirror_v3_0_1.tdl",
loader_file=self.runtime_root / "build_act_v3_0_1.py",
)
@staticmethod
@@ -81,7 +81,7 @@ class AccountingMirrorEngine:
raise RuntimeError("Microsoft .NET Framework 4.x C# compiler was not found.")
def _ensure_extractor(self, paths: MirrorPaths) -> None:
source = self.runtime_root / "TallyAccountingMirrorV31.cs"
source = self.runtime_root / "TallyAccountingMirrorV301.cs"
rebuild = not paths.extractor_exe.is_file()
if not rebuild:
try:
@@ -227,7 +227,7 @@ class AccountingMirrorEngine:
self._ensure_extractor(paths)
shutil.rmtree(paths.work_root, ignore_errors=True)
paths.work_root.mkdir(parents=True, exist_ok=True)
output_dir = paths.work_root / "mirror_output_v3_1"
output_dir = paths.work_root / "mirror_output_v3_0_1"
cp = subprocess.run(
[str(paths.extractor_exe), "--dsn", dsn or DEFAULT_DSN, "--out", str(output_dir)],
capture_output=True, text=True, timeout=max(60, int(timeout_seconds))
@@ -237,7 +237,7 @@ class AccountingMirrorEngine:
if cp.returncode != 0:
raise RuntimeError("Accounting Mirror ODBC extraction failed: " + log[-4000:])
loader_copy = paths.work_root / "build_act_v3_1.py"
loader_copy = paths.work_root / "build_act_v3_0_1.py"
shutil.copy2(paths.loader_file, loader_copy)
lp = subprocess.run(
[sys.executable, str(loader_copy), "--source", output_dir.name],
@@ -249,7 +249,7 @@ class AccountingMirrorEngine:
if lp.returncode != 0:
raise RuntimeError("Accounting Mirror SQLite build failed: " + llog[-4000:])
# Loader v3.0.1 writes this name even when invoked as v3.1.
# Proven v3.0.1 loader output.
candidate = output_dir / "accounting_mirror_v3_0_1.act"
if not candidate.is_file():
raise RuntimeError("Accounting Mirror database was not produced.")
@@ -1,5 +1,6 @@
from __future__ import annotations
import hashlib
import json
import shutil
import threading
@@ -22,9 +23,56 @@ class AgentDashboard:
self.install_dir = Path(install_dir).resolve()
self.server: ThreadingHTTPServer | None = None
@staticmethod
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _embedded_tdl(self) -> Path:
preferred = self.install_dir / "erp_local_agent" / "mirror_runtime" / "ERP_Accounting_Mirror_v3_0_1.tdl"
if preferred.is_file():
return preferred
compatibility = self.install_dir / "erp_local_agent" / "mirror_runtime" / "ERP_Accounting_Mirror_v3_1.tdl"
if compatibility.is_file():
return compatibility
raise RuntimeError("ERP Accounting Mirror TDL is missing from the Local Agent runtime.")
def _managed_tdl(self) -> Path:
return self.install_dir / "tally" / "ERP_Accounting_Mirror.tdl"
def _ensure_managed_tdl(self, force: bool = False) -> dict[str, Any]:
source = self._embedded_tdl()
target = self._managed_tdl()
target.parent.mkdir(parents=True, exist_ok=True)
source_hash = self._sha256(source)
target_hash = self._sha256(target) if target.is_file() else ""
repaired = False
if force or source_hash != target_hash:
temp = target.with_suffix(".tdl.new")
shutil.copy2(source, temp)
temp.replace(target)
target_hash = self._sha256(target)
repaired = True
return {
"version": "3.0.1",
"path": str(target),
"url": f"http://{self.config.dashboard_host}:{self.config.dashboard_port}/tally/ERP_Accounting_Mirror.tdl",
"sha256": target_hash,
"source_path": str(source),
"repaired": repaired,
"exists": target.is_file(),
}
def start(self) -> None:
if not self.config.dashboard_enabled:
return
try:
self._ensure_managed_tdl()
except Exception as exc:
self.logger.warning("Could not prepare managed Tally TDL: %s", exc)
dashboard = self
class Handler(BaseHTTPRequestHandler):
@@ -58,6 +106,17 @@ class AgentDashboard:
return self._json(dashboard.history())
if self.path.startswith("/api/cash-progress"):
return self._json(dashboard.cash_progress())
if self.path.split("?", 1)[0] == "/tally/ERP_Accounting_Mirror.tdl":
info = dashboard._ensure_managed_tdl()
payload = Path(info["path"]).read_bytes()
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Cache-Control", "no-store")
self.send_header("Content-Disposition", 'inline; filename="ERP_Accounting_Mirror.tdl"')
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
return
self.send_error(404)
def do_POST(self):
@@ -76,6 +135,10 @@ class AgentDashboard:
status = TallyLiveConnector().status()
dashboard.db.record_tally_status(status)
return self._json({"ok": True, "tally": status})
if self.path == "/api/tally/tdl/repair":
info = dashboard._ensure_managed_tdl(force=True)
dashboard.db.record_event("INFO", "tally_tdl_repaired", f"Managed TDL refreshed: {info['path']}")
return self._json({"ok": True, "tdl": info})
self.send_error(404)
except Exception as exc:
dashboard.logger.exception("Dashboard action failed: %s", exc)
@@ -120,6 +183,7 @@ class AgentDashboard:
"free_bytes": free,
},
"tally": tally,
"tdl": self._ensure_managed_tdl(),
"update": self.updater.status(),
"dashboard": {"host": self.config.dashboard_host, "port": self.config.dashboard_port},
}
@@ -149,7 +213,7 @@ class AgentDashboard:
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>ERP Local Agent</title>
<style>
body{font-family:Segoe UI,Arial,sans-serif;background:#f4f6f8;color:#17202a;margin:0}.wrap{max-width:1100px;margin:28px auto;padding:0 18px}h1{margin:0 0 4px}.muted{color:#6b7280}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px;margin:22px 0}.card{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:18px;box-shadow:0 1px 2px rgba(0,0,0,.04)}.label{font-size:12px;text-transform:uppercase;letter-spacing:.06em;color:#6b7280}.value{font-size:22px;font-weight:650;margin-top:8px}.ok{color:#087f5b}.bad{color:#c92a2a}.warn{color:#b26a00}button{border:0;border-radius:8px;padding:10px 14px;margin:4px 6px 4px 0;font-weight:600;cursor:pointer;background:#111827;color:#fff}button.secondary{background:#e5e7eb;color:#111827}button:disabled{opacity:.5;cursor:not-allowed}.section{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:18px;margin:14px 0}table{width:100%;border-collapse:collapse;font-size:13px}th,td{text-align:left;padding:8px;border-bottom:1px solid #eee;vertical-align:top}#message{margin-top:10px;padding:10px;border-radius:8px;display:none}.msgok{display:block!important;background:#e6fcf5;color:#087f5b}.msgbad{display:block!important;background:#fff5f5;color:#c92a2a}code{background:#f3f4f6;padding:2px 5px;border-radius:4px}.progressbox{margin-top:14px;padding:14px;border:1px solid #dbe3ea;border-radius:10px;background:#f8fafc}.progresshead{display:flex;justify-content:space-between;gap:12px;align-items:center}.progressbar{height:12px;background:#e5e7eb;border-radius:999px;overflow:hidden;margin-top:10px}.progressfill{height:100%;width:0%;background:#111827;transition:width .45s ease}.progressmsg{margin-top:8px;font-size:13px;color:#4b5563}.pulse{animation:pulse 1.2s ease-in-out infinite}.cashsteps{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:8px;margin-top:12px}.cashstep{padding:9px 10px;border-radius:8px;background:#f3f4f6;font-size:12px}.cashstep.done{background:#e6fcf5;color:#087f5b}.cashstep.run{background:#e7f5ff;color:#1864ab;font-weight:700}.metricgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin-top:12px}.metric{background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:10px}.metric b{display:block;font-size:18px;margin-top:3px}@keyframes pulse{0%,100%{opacity:.65}50%{opacity:1}}
body{font-family:Segoe UI,Arial,sans-serif;background:#f4f6f8;color:#17202a;margin:0}.wrap{max-width:1100px;margin:28px auto;padding:0 18px}h1{margin:0 0 4px}.muted{color:#6b7280}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px;margin:22px 0}.card{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:18px;box-shadow:0 1px 2px rgba(0,0,0,.04)}.label{font-size:12px;text-transform:uppercase;letter-spacing:.06em;color:#6b7280}.value{font-size:22px;font-weight:650;margin-top:8px}.ok{color:#087f5b}.bad{color:#c92a2a}.warn{color:#b26a00}button{border:0;border-radius:8px;padding:10px 14px;margin:4px 6px 4px 0;font-weight:600;cursor:pointer;background:#111827;color:#fff}button.secondary{background:#e5e7eb;color:#111827}.copyrow{display:grid;grid-template-columns:1fr auto;gap:8px;align-items:center;margin-top:8px}.copybox{width:100%;box-sizing:border-box;border:1px solid #d1d5db;border-radius:8px;padding:10px;background:#f8fafc;font-family:Consolas,monospace;font-size:12px;color:#111827}button:disabled{opacity:.5;cursor:not-allowed}.section{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:18px;margin:14px 0}table{width:100%;border-collapse:collapse;font-size:13px}th,td{text-align:left;padding:8px;border-bottom:1px solid #eee;vertical-align:top}#message{margin-top:10px;padding:10px;border-radius:8px;display:none}.msgok{display:block!important;background:#e6fcf5;color:#087f5b}.msgbad{display:block!important;background:#fff5f5;color:#c92a2a}code{background:#f3f4f6;padding:2px 5px;border-radius:4px}.progressbox{margin-top:14px;padding:14px;border:1px solid #dbe3ea;border-radius:10px;background:#f8fafc}.progresshead{display:flex;justify-content:space-between;gap:12px;align-items:center}.progressbar{height:12px;background:#e5e7eb;border-radius:999px;overflow:hidden;margin-top:10px}.progressfill{height:100%;width:0%;background:#111827;transition:width .45s ease}.progressmsg{margin-top:8px;font-size:13px;color:#4b5563}.pulse{animation:pulse 1.2s ease-in-out infinite}.cashsteps{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:8px;margin-top:12px}.cashstep{padding:9px 10px;border-radius:8px;background:#f3f4f6;font-size:12px}.cashstep.done{background:#e6fcf5;color:#087f5b}.cashstep.run{background:#e7f5ff;color:#1864ab;font-weight:700}.metricgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin-top:12px}.metric{background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:10px}.metric b{display:block;font-size:18px;margin-top:3px}@keyframes pulse{0%,100%{opacity:.65}50%{opacity:1}}
</style></head>
<body><div class="wrap"><h1>ERP Local Agent</h1><div class="muted">Local dashboard — available only on this computer</div>
<div class="grid">
@@ -162,6 +226,7 @@ body{font-family:Segoe UI,Arial,sans-serif;background:#f4f6f8;color:#17202a;marg
<button id="check" onclick="checkUpdate()">Check for Update</button><button class="secondary" id="download" onclick="downloadUpdate()">Download Update</button><button id="install" onclick="installUpdate()">Install & Restart Safely</button><div id="message"></div>
<div id="progressBox" class="progressbox" style="display:none"><div class="progresshead"><strong id="progressTitle">Update</strong><span id="progressPct">0%</span></div><div class="progressbar"><div id="progressFill" class="progressfill"></div></div><div id="progressMsg" class="progressmsg">Waiting…</div></div></div>
<div class="section"><h2>Tally</h2><button onclick="action('/api/tally/check')">Test Tally Connection</button><p class="muted">Tally is checked locally at <code>127.0.0.1:9000</code>. Port 9000 is not exposed to the internet.</p></div>
<div class="section"><h2>Tally Accounting Mirror TDL</h2><p class="muted">Link Tally once to the stable local path below. Local Agent updates keep this same file path and refresh its contents, so future TDL repairs do not require a new Tally link.</p><div class="label">Stable Local TDL Path</div><div class="copyrow"><input id="tdlPath" class="copybox" readonly value="Loading…"><button onclick="copyText('tdlPath')">Copy Path</button></div><div class="label" style="margin-top:12px">Local TDL URL</div><div class="copyrow"><input id="tdlUrl" class="copybox" readonly value="Loading…"><button onclick="copyText('tdlUrl')">Copy URL</button></div><div id="tdlMeta" class="muted" style="margin-top:8px"></div><button class="secondary" onclick="repairTdl()">Repair / Refresh TDL</button><p class="muted">For Tally's Local TDL setting, use the stable file path if your TallyPrime build does not accept an HTTP URL. The URL is also kept stable for tools that support URL loading.</p></div>
<div class="section" id="cashJobSection" style="display:none"><div class="progresshead"><div><h2 style="margin:0">Cash Payment Compliance</h2><div id="cashJobSub" class="muted">Background Tally job</div></div><strong id="cashJobPct">0%</strong></div><div class="progressbar"><div id="cashJobFill" class="progressfill"></div></div><div id="cashJobStage" class="progressmsg">Waiting…</div><div class="metricgrid"><div class="metric"><span class="label">Days</span><b id="cashDays">0 / 0</b></div><div class="metric"><span class="label">Cash vouchers</span><b id="cashVouchers">0</b></div><div class="metric"><span class="label">Tally requests</span><b id="cashRequests">0</b></div><div class="metric"><span class="label">Current date</span><b id="cashDate">—</b></div></div><div class="cashsteps"><div id="cs1" class="cashstep">1. Connect to Tally</div><div id="cs2" class="cashstep">2. Confirm Cash ledger</div><div id="cs3" class="cashstep">3. Extract transactions</div><div id="cs4" class="cashstep">4. Analyse threshold</div><div id="cs5" class="cashstep">5. Complete</div></div><div id="cashError" class="msgbad" style="margin-top:10px"></div></div>
<div class="section"><h2>Recent History</h2><div id="history" class="muted">Loading…</div></div>
</div>
@@ -171,13 +236,14 @@ 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 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:'')}
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 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…';document.getElementById('cashJobSub').textContent=(j.company_name||'Tally company')+(j.cash_ledgers&&j.cash_ledgers.length?' • Cash ledger: '+j.cash_ledgers.join(', '):'');document.getElementById('cashDays').textContent=String(j.completed_days||0)+' / '+String(j.total_days||0);document.getElementById('cashVouchers').textContent=String(j.vouchers_cached||0);document.getElementById('cashRequests').textContent=String(j.tally_requests||0);document.getElementById('cashDate').textContent=j.current_date||'—';let st=String(j.stage||'').toLowerCase(),status=String(j.status||'').toLowerCase();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;
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('version').textContent=s.agent.version;document.getElementById('latest').textContent='Latest: '+s.update.latest_version+(s.update.update_available?' • update available':' • current');
let te=document.getElementById('tally');let known=s.tally.connected!==null;te.textContent=!known?'Not checked':(s.tally.connected?'Connected':'Offline');te.className='value '+(!known?'warn':(s.tally.connected?'ok':'bad'));document.getElementById('companies').textContent=!known?'Use Test Tally Connection':(s.tally.connected?(s.tally.company_count+' loaded compan'+(s.tally.company_count===1?'y':'ies')):(s.tally.error||''));
document.getElementById('storage').textContent=gb(s.storage.free_bytes);document.getElementById('storageRoot').textContent=s.storage.root;
if(s.tdl){document.getElementById('tdlPath').value=s.tdl.path||'';document.getElementById('tdlUrl').value=s.tdl.url||'';document.getElementById('tdlMeta').textContent='Mirror '+(s.tdl.version||'')+' • SHA256 '+String(s.tdl.sha256||'').slice(0,16)+'…'+(s.tdl.repaired?' • refreshed now':'');}
document.getElementById('check').disabled=!!s.update.busy;document.getElementById('download').disabled=!s.update.update_available||!!s.update.busy;document.getElementById('install').disabled=!s.update.downloaded||!!s.update.busy;applyProgress(s.update);
if(installWatching&&installTarget&&s.agent.version===installTarget){setProgress('Update Complete',100,'ERP Local Agent '+installTarget+' is running. Updated dashboard loaded.',false);msg('Update completed successfully.',true);installWatching=false;setTimeout(()=>location.reload(),1200)}
await refreshHistory();await refreshCashProgress();}catch(e){if(installWatching){offlineTicks++;let estimated=Math.min(92,Math.max(lastServerPercent,35+offlineTicks*3));setProgress('Applying Update',estimated,'Local Agent is restarting. Waiting for the updated dashboard to come back online…',true)}else{console.log(e)}}}
@@ -187,6 +253,8 @@ async function checkUpdate(){setProgress('Checking for Updates',10,'Checking ERP
async function downloadUpdate(){setProgress('Downloading Update',5,'Starting download…',true);msg('Downloading update…',true);try{const r=await fetch('/api/update/download',{method:'POST'});const j=await r.json();if(!r.ok||!j.ok)throw new Error(j.error||'Download failed');msg('Download started. You can keep this dashboard open.',true);await refresh()}catch(e){setProgress('Download Update',100,e.message,false);msg(e.message,false)}}
async function action(url){msg('Working…',true);try{const r=await fetch(url,{method:'POST'});const j=await r.json();if(!r.ok||!j.ok)throw new Error(j.error||'Operation failed');msg('Completed successfully.',true);await refresh()}catch(e){msg(e.message,false)}}
async function installUpdate(){if(!confirm('Install the downloaded update now? The Local Agent worker will restart safely and this dashboard will show progress until the updated dashboard returns.'))return;let latest=document.getElementById('latest').textContent.replace('Latest: ','').split(' •')[0];installTarget=latest;installWatching=true;offlineTicks=0;lastServerPercent=5;setProgress('Applying Update',5,'Queuing update with the Local Agent supervisor…',true);msg('Starting safe update installation…',true);try{const r=await fetch('/api/update/install',{method:'POST'});const j=await r.json();if(!r.ok||!j.ok)throw new Error(j.error||'Install failed');if(j.target_version)installTarget=j.target_version;msg(j.message||'Update queued. Keep this dashboard open.',true);setProgress('Applying Update',8,'Update queued. Waiting for supervisor…',true)}catch(e){installWatching=false;setProgress('Apply Update',100,e.message,false);msg(e.message,false)}}
async function copyText(id){let e=document.getElementById(id);let v=e.value||'';try{await navigator.clipboard.writeText(v);msg('Copied to clipboard.',true)}catch(err){e.focus();e.select();document.execCommand('copy');msg('Copied to clipboard.',true)}}
async function repairTdl(){msg('Refreshing the managed TDL…',true);try{const r=await fetch('/api/tally/tdl/repair',{method:'POST'});const j=await r.json();if(!r.ok||!j.ok)throw new Error(j.error||'TDL repair failed');msg('TDL refreshed successfully. Restart/reload TDL in TallyPrime if it is already open.',true);await refresh()}catch(e){msg(e.message,false)}}
function msg(t,ok){let e=document.getElementById('message');e.textContent=t;e.className=ok?'msgok':'msgbad'}
refresh();setInterval(refresh,1000);
</script></body></html>'''
@@ -163,45 +163,3 @@
;; ============================================================
;; END
;; ============================================================
;; ================================================================
;; ACCOUNTING MIRROR v3.1 - ADDITIONAL MASTER COLLECTIONS
;; These are flat read-only ODBC master tables used to replace the
;; former XML master synchronization path.
;; ================================================================
[Collection : ERPAMGroupMaster]
Type : Group
Fetch : Name, GUID, Parent, ReservedName, IsRevenue, IsDeemedPositive, IsActive
Is ODBC Table : Yes
[Collection : ERPAMVoucherTypeMaster]
Type : VoucherType
Fetch : Name, GUID, Parent, NumberingMethod, IsActive
Is ODBC Table : Yes
[Collection : ERPAMStockGroupMaster]
Type : StockGroup
Fetch : Name, GUID, Parent, BaseUnits, GSTApplicable, GSTTypeofSupply, IsActive
Is ODBC Table : Yes
[Collection : ERPAMStockCategoryMaster]
Type : StockCategory
Fetch : Name, GUID, Parent, IsActive
Is ODBC Table : Yes
[Collection : ERPAMUnitMaster]
Type : Unit
Fetch : Name, GUID, OriginalName, IsSimpleUnit, BaseUnits, AdditionalUnits, Conversion
Is ODBC Table : Yes
[Collection : ERPAMCostCentreMaster]
Type : CostCentre
Fetch : Name, GUID, Parent, IsActive
Is ODBC Table : Yes
[Collection : ERPAMCostCategoryMaster]
Type : CostCategory
Fetch : Name, GUID, Parent, IsActive
Is ODBC Table : Yes
@@ -6,7 +6,7 @@ using System.Globalization;
using System.IO;
using System.Text;
class TallyAccountingMirrorV31
class TallyAccountingMirrorV301
{
static string Safe(object v) {
if (v == null || v == DBNull.Value) return "";
@@ -58,29 +58,9 @@ class TallyAccountingMirrorV31
Console.Out.Flush();
}
static void TryExtract(OdbcConnection cn, string label, string sql, string path,
out int rows, out double sec)
{
try
{
Extract(cn, label, sql, path, out rows, out sec);
}
catch (Exception ex)
{
rows = 0;
sec = 0;
Console.WriteLine(label + " - OPTIONAL QUERY FAILED: " + ex.Message);
using (var w = new StreamWriter(path, false, new UTF8Encoding(true)))
{
w.WriteLine("");
}
}
}
static int Main(string[] args)
{
string dsn="TallyODBC64_9000", outDir="mirror_output_v3_1";
string dsn="TallyODBC64_9000", outDir="mirror_output_v3_0_1";
for(int i=0;i<args.Length;i++) {
if(args[i]=="--dsn" && i+1<args.Length) dsn=args[++i];
else if(args[i]=="--out" && i+1<args.Length) outDir=args[++i];
@@ -89,7 +69,7 @@ class TallyAccountingMirrorV31
try {
var total=Stopwatch.StartNew();
Console.WriteLine("ERP ACCOUNTING MIRROR v3.1 - FULL ACCOUNTING READ MIRROR");
Console.WriteLine("ERP ACCOUNTING MIRROR v3.0.1 - ERP INTEGRATION CANDIDATE");
Console.WriteLine("READ ONLY | ODBC | NO XML | NO TALLY WRITES");
Console.WriteLine("BEFORE CONNECT");
@@ -112,37 +92,6 @@ class TallyAccountingMirrorV31
"SELECT $GUID,$AlterID,$Name,$Parent,$BaseUnits,$OpeningBalance,$OpeningValue,$GSTApplicable,$GSTRepUoM,$GSTTypeofSupply,$GSTItemHSNCodeEx FROM ERPAMStockMaster",
Path.Combine(outDir,"stock_master.csv"),out s,out ss);
int grp, vt, sg, stockCat, un, cc, ccat;
double sgrp, svt, ssg, ssc, sun, scc, sccat;
TryExtract(cn,"GROUP MASTER",
"SELECT $GUID,$Name,$Parent,$ReservedName,$IsRevenue,$IsDeemedPositive,$IsActive FROM ERPAMGroupMaster",
Path.Combine(outDir,"group_master.csv"),out grp,out sgrp);
TryExtract(cn,"VOUCHER TYPE MASTER",
"SELECT $GUID,$Name,$Parent,$NumberingMethod,$IsActive FROM ERPAMVoucherTypeMaster",
Path.Combine(outDir,"voucher_type_master.csv"),out vt,out svt);
TryExtract(cn,"STOCK GROUP MASTER",
"SELECT $GUID,$Name,$Parent,$BaseUnits,$GSTApplicable,$GSTTypeofSupply,$IsActive FROM ERPAMStockGroupMaster",
Path.Combine(outDir,"stock_group_master.csv"),out sg,out ssg);
TryExtract(cn,"STOCK CATEGORY MASTER",
"SELECT $GUID,$Name,$Parent,$IsActive FROM ERPAMStockCategoryMaster",
Path.Combine(outDir,"stock_category_master.csv"),out stockCat,out ssc);
TryExtract(cn,"UNIT MASTER",
"SELECT $GUID,$Name,$OriginalName,$IsSimpleUnit,$BaseUnits,$AdditionalUnits,$Conversion FROM ERPAMUnitMaster",
Path.Combine(outDir,"unit_master.csv"),out un,out sun);
TryExtract(cn,"COST CENTRE MASTER",
"SELECT $GUID,$Name,$Parent,$IsActive FROM ERPAMCostCentreMaster",
Path.Combine(outDir,"cost_centre_master.csv"),out cc,out scc);
TryExtract(cn,"COST CATEGORY MASTER",
"SELECT $GUID,$Name,$Parent,$IsActive FROM ERPAMCostCategoryMaster",
Path.Combine(outDir,"cost_category_master.csv"),out ccat,out sccat);
Extract(cn,"STOCK HSN MASTER",
"SELECT $Name,$GUID,$AlterID,$Parent,$BaseUnits,$GSTApplicable,$GSTTypeofSupply,"+
"$HSNSource1,$HSNApplicableFrom1,$HSNCode1,$HSNValue1,$HSNDescription1,"+
@@ -179,7 +128,7 @@ class TallyAccountingMirrorV31
Console.WriteLine("SUMMARY | Company="+c+
" | Ledger="+l+
" | Stock="+s+
" | Groups="+grp+" | VoucherTypes="+vt+" | StockGroups="+sg+" | StockCategories="+stockCat+" | Units="+un+" | CostCentres="+cc+" | CostCategories="+ccat+" | StockHSN="+tax+" | StockGST="+gstRows+
" | StockHSN="+tax+" | StockGST="+gstRows+
" | Voucher="+v+
" | VoucherLedger="+vl+
" | Inventory="+im+
@@ -52,9 +52,29 @@ class MirrorFirstTallyConnector:
)
def fetch_accounting_masters(self, company_name: str):
return self.mirror.master_snapshot(
masters = self.mirror.master_snapshot(
self._client(), company_name, str(self.context.get("tally_guid") or "")
)
# The proven v3.0.1 ODBC mirror intentionally contains the high-volume
# accounting datasets (ledgers, stock items, vouchers, movements, HSN and
# GST rates). Preserve the existing auxiliary-master feature by using the
# already-existing read-only XML collection only when that auxiliary table
# is not present in the proven mirror. No Tally write is performed here.
for key in (
"groups", "voucher_types", "stock_groups", "stock_categories",
"units", "cost_centres", "cost_categories",
):
if masters.get(key):
continue
try:
masters[key] = self.live.export_master_collection(company_name, key)
except Exception as exc:
self.logger.warning(
"Accounting Mirror auxiliary master fallback failed company=%s key=%s: %s",
company_name, key, exc,
)
masters.setdefault(key, [])
return masters
def fetch_accounting_transactions(self, company_name: str, date_from: str, date_to: str):
return self.mirror.transactions(