Add Phase 5 historical Tally learning foundation

This commit is contained in:
A R R R Associates
2026-08-22 13:01:17 +05:30
parent 45fedd0d96
commit a9a5effcd0
15 changed files with 546 additions and 6 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ import io
from pathlib import Path
import zipfile
ERP_LOCAL_AGENT_VERSION = "1.11.0"
ERP_LOCAL_AGENT_VERSION = "1.12.0"
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)
@@ -53,7 +53,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"
"Phase 3 master synchronization is preserved. Phase 4 adds read-only date-scoped Tally voucher/transaction synchronization.\n"
"Existing master/transaction sync and depreciation are preserved. Phase 5 adds neutral historical-ledger evidence read capability for server-side learning.\n"
"Dashboard: http://127.0.0.1:8788\n"
)
_write_zip_bytes(dst, "README_ERP_LOCAL_AGENT.txt", text.encode("utf-8"))
@@ -1,2 +1 @@
__version__ = "1.11.0"
AGENT_NAME = "ERP Local Agent"
__version__ = "1.12.0"
@@ -1077,6 +1077,54 @@ class LocalAccountingStore:
).fetchall()
return [dict(row) for row in rows]
def historical_learning_evidence(self, client_id: int, *, tally_guid: str, date_from: str = "", date_to: str = "", voucher_scope: str = "purchase") -> dict:
"""Return neutral aggregated historical ledger facts from the local .act store.
No accounting classification is performed locally. The ERP server maps
these facts to its canonical taxonomy.
"""
if not self.exists(client_id):
raise ValueError("Accounting storage is not initialized for this client.")
guid = str(tally_guid or "").strip()
if not guid:
raise ValueError("Tally company GUID is required.")
mapping = self.get_active_mapping_by_guid(client_id, guid)
params: list = [guid]
where = ["v.tally_guid=?", "TRIM(COALESCE(v.party_ledger_name,''))<>''", "LOWER(TRIM(COALESCE(v.is_cancelled,''))) NOT IN ('yes','true','1')", "LOWER(TRIM(e.ledger_name))<>LOWER(TRIM(v.party_ledger_name))"]
if str(voucher_scope or "purchase").strip().lower() == "purchase":
where.append("LOWER(v.voucher_type_name) LIKE '%purchase%'")
if date_from:
where.append("v.voucher_date>=?"); params.append(str(date_from))
if date_to:
where.append("v.voucher_date<=?"); params.append(str(date_to))
sql = f"""
SELECT v.party_ledger_name AS party_ledger_name,
COALESCE(pl.parent,'') AS party_parent_group,
e.ledger_name AS counter_ledger_name,
COALESCE(cl.parent,'') AS counter_parent_group,
v.voucher_type_name AS voucher_type_name,
COUNT(DISTINCT v.id) AS voucher_count,
COALESCE(SUM(ABS(e.amount)),0) AS absolute_amount_total,
MIN(NULLIF(v.voucher_date,'')) AS first_voucher_date,
MAX(NULLIF(v.voucher_date,'')) AS last_voucher_date,
MIN(NULLIF(v.narration,'')) AS sample_narration
FROM tally_vouchers v
JOIN tally_voucher_ledger_entries e ON e.voucher_id=v.id
LEFT JOIN tally_ledgers pl ON pl.tally_guid=v.tally_guid AND LOWER(TRIM(pl.name))=LOWER(TRIM(v.party_ledger_name))
LEFT JOIN tally_ledgers cl ON cl.tally_guid=v.tally_guid AND LOWER(TRIM(cl.name))=LOWER(TRIM(e.ledger_name))
WHERE {' AND '.join(where)}
GROUP BY v.party_ledger_name, COALESCE(pl.parent,''), e.ledger_name, COALESCE(cl.parent,''), v.voucher_type_name
ORDER BY voucher_count DESC, v.party_ledger_name COLLATE NOCASE, e.ledger_name COLLATE NOCASE
LIMIT 5000
"""
with self.connect(client_id) as db:
rows = [dict(r) for r in db.execute(sql, tuple(params)).fetchall()]
return {
"client_id": int(client_id), "tally_guid": guid, "company_name": str(mapping.get("company_name") or ""),
"date_from": str(date_from or ""), "date_to": str(date_to or ""), "voucher_scope": str(voucher_scope or "purchase"),
"evidence": rows, "evidence_count": len(rows),
}
def snapshot(self, client_id: int) -> dict:
path = self.db_path(client_id)
if not path.is_file():
@@ -36,6 +36,8 @@ class AgentCommandProcessor:
result = self._sync_masters(payload)
elif action == "accounting_sync_transactions":
result = self._sync_transactions(payload)
elif action == "accounting_historical_evidence":
result = self._historical_evidence(payload)
elif action == "accounting_depreciation_preview":
result = self._depreciation_preview(payload)
elif action == "accounting_calculate_it_depreciation":
@@ -67,6 +69,7 @@ class AgentCommandProcessor:
"tally_capability": True, "accounting_act_capability": True,
"tally_mapping_capability": True, "tally_master_sync_capability": True,
"tally_transaction_sync_capability": True,
"historical_learning_read_capability": True,
"it_depreciation_capability": True,
"tally_writeback_capability": True,
}
@@ -201,6 +204,16 @@ class AgentCommandProcessor:
self.logger.info("Tally transaction sync completed client_id=%s company=%s period=%s..%s rows=%s", client_id, company_name, date_from, date_to, sync.get("rows_processed"))
return {"synced": True, "sync": sync, "accounting": self.store.snapshot(client_id), "tally": tally_status, "agent": self._agent_info()}
def _historical_evidence(self, payload: dict[str, Any]) -> dict[str, Any]:
client_id = int(payload.get("client_id"))
return self.store.historical_learning_evidence(
client_id,
tally_guid=str(payload.get("tally_guid") or ""),
date_from=str(payload.get("date_from") or ""),
date_to=str(payload.get("date_to") or ""),
voucher_scope=str(payload.get("voucher_scope") or "purchase"),
)
def _depreciation_preview(self, payload: dict[str, Any]) -> dict[str, Any]:
client_id = int(payload.get("client_id"))
tally_guid = str(payload.get("tally_guid") or "").strip()
@@ -49,7 +49,7 @@ class StorageAgent:
"free_bytes": free,
"agent_version": __version__,
"agent_name": "ERP Local Agent",
"capabilities": ["storage", "tally", "accounting_act", "tally_master_sync", "tally_transaction_sync", "local_dashboard", "manual_updates"],
"capabilities": ["storage", "tally", "accounting_act", "tally_master_sync", "tally_transaction_sync", "local_dashboard", "manual_updates", "historical_learning_read"],
"agent_time_utc": datetime.now(timezone.utc).isoformat(),
}
self.client.heartbeat(payload)
@@ -190,7 +190,7 @@ class StorageAgentTunnel:
def _capabilities(self) -> list[str]:
return [
"storage", "local_dashboard", "manual_updates", "accounting_act", "agent.jobs.v2",
"storage", "local_dashboard", "manual_updates", "historical_learning_read", "accounting_act", "agent.jobs.v2",
"tally.status", "tally.company_identity", "tally.read_groups",
"tally.read_ledgers", "tally.read_stock_items", "tally.read_vouchers",
"tally.read_trial_balance", "tally.master_sync", "tally.transaction_sync",