Add Phase 5 historical Tally learning foundation
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user