Validate FY mirrors and repair depreciation and TDS review

This commit is contained in:
A R R R Associates
2026-09-06 20:49:42 +05:30
parent 739973e370
commit eb83e5418d
12 changed files with 166 additions and 22 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ import io
from pathlib import Path
import zipfile
ERP_LOCAL_AGENT_VERSION = "1.25.2"
ERP_LOCAL_AGENT_VERSION = "1.25.3"
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)
@@ -1,2 +1,2 @@
__version__ = "1.25.2"
__version__ = "1.25.3"
AGENT_NAME = "ERP Local Agent"
@@ -3,6 +3,7 @@ from __future__ import annotations
from datetime import date, datetime, timedelta, timezone
import json
import os
import re
from pathlib import Path
import shutil
import sqlite3
@@ -3194,7 +3194,61 @@ class AgentCommandProcessor:
liability=round(max(0.0,expected-actual),2)
status="OK" if expected>0 and liability<=0.009 else ("TDS NOT DEDUCTED" if expected>0 and actual<=0.009 else ("SHORT DEDUCTION" if liability>0 else "BELOW / OUTSIDE THRESHOLD"))
transactions.append({**{k:v for k,v in x.items() if k!='rule'},"rule_id":r.get("id"),"rule_code":r.get("rule_code"),"rule_name":r.get("name"),"legacy_section":r.get("legacy_section"),"statutory_reference":r.get("statutory_reference"),"cumulative_amount":cumulative,"expected_tds":expected,"actual_tds":actual,"liability":liability,"status":status})
review = {"company_name":company_name,"company_guid":company_guid,"date_from":date_from,"date_to":date_to,"summary":{"vouchers_reviewed":len(vouchers),"candidate_transactions":len(transactions),"tds_not_deducted":sum(1 for x in transactions if x["status"]=="TDS NOT DEDUCTED"),"short_deduction":sum(1 for x in transactions if x["status"]=="SHORT DEDUCTION"),"purchase_transactions":sum(1 for x in transactions if x["rule_code"]=="PURCHASE_GOODS")},"transactions":transactions,"tds_ledgers":sorted(tds_ledger_names),"read_only":True,"sqlite_mirror":True}
category_map = {}
for item in transactions:
key = str(item.get("rule_code") or item.get("rule_name") or "UNCLASSIFIED")
row = category_map.setdefault(key, {
"rule_code": str(item.get("rule_code") or ""),
"category": str(item.get("rule_name") or "Unclassified"),
"legacy_section": str(item.get("legacy_section") or ""),
"statutory_reference": str(item.get("statutory_reference") or ""),
"transactions": 0,
"taxable_base": 0.0,
"expected_tds": 0.0,
"actual_tds": 0.0,
"liability": 0.0,
"tds_not_deducted": 0,
"short_deduction": 0,
"ok": 0,
"below_threshold": 0,
})
row["transactions"] += 1
row["taxable_base"] = round(row["taxable_base"] + float(item.get("taxable_base") or 0), 2)
row["expected_tds"] = round(row["expected_tds"] + float(item.get("expected_tds") or 0), 2)
row["actual_tds"] = round(row["actual_tds"] + float(item.get("actual_tds") or 0), 2)
row["liability"] = round(row["liability"] + float(item.get("liability") or 0), 2)
status = str(item.get("status") or "")
if status == "TDS NOT DEDUCTED":
row["tds_not_deducted"] += 1
elif status == "SHORT DEDUCTION":
row["short_deduction"] += 1
elif status == "OK":
row["ok"] += 1
else:
row["below_threshold"] += 1
category_summary = sorted(
category_map.values(),
key=lambda row: (-float(row.get("liability") or 0), str(row.get("category") or "").casefold()),
)
review = {
"company_name": company_name,
"company_guid": company_guid,
"date_from": date_from,
"date_to": date_to,
"summary": {
"vouchers_reviewed": len(vouchers),
"candidate_transactions": len(transactions),
"tds_not_deducted": sum(1 for x in transactions if x["status"]=="TDS NOT DEDUCTED"),
"short_deduction": sum(1 for x in transactions if x["status"]=="SHORT DEDUCTION"),
"purchase_transactions": sum(1 for x in transactions if x["rule_code"]=="PURCHASE_GOODS"),
},
"category_summary": category_summary,
"transactions": transactions,
"tds_ledgers": sorted(tds_ledger_names),
"read_only": True,
"sqlite_mirror": True,
}
analysis_run_id = self._save_analysis_run(
client_id,
analysis_type="TDS_COMPLIANCE",
@@ -3531,11 +3585,66 @@ class AgentCommandProcessor:
@staticmethod
def _mirror_financial_year_match(mirror: dict[str, Any], financial_year: str) -> dict[str, Any]:
"""Validate mirror contents against the FY requested by ERP.
A mirror file can exist in an FY folder even when its vouchers were
extracted while Tally was on another active period. Such a file must
not be shown as Ready for Opening Balance / depreciation analysis.
"""
fy = str(financial_year or "").strip()
match = re.fullmatch(r"(\d{4})-(\d{2})", fy)
if not match:
return {"requested_financial_year": fy, "ready_for_financial_year": bool(mirror.get("ready"))}
start_year = int(match.group(1))
fy_from = _dt_date(start_year, 4, 1)
fy_to = _dt_date(start_year + 1, 3, 31)
period = mirror.get("voucher_period") or {}
from_text = str(period.get("from_date") or "").strip()
to_text = str(period.get("to_date") or "").strip()
try:
mirror_from = _dt_date.fromisoformat(from_text)
mirror_to = _dt_date.fromisoformat(to_text)
except Exception:
return {
"requested_financial_year": fy,
"ready_for_financial_year": False,
"financial_year_reason": "Mirror voucher period is unavailable.",
"financial_year_from": fy_from.isoformat(),
"financial_year_to": fy_to.isoformat(),
}
# Require the mirror's actual voucher period to fall inside the requested
# Indian FY. This deliberately rejects a copied/misfiled FY2025-26
# extraction sitting in the FY2026-27 directory.
ready = bool(mirror.get("ready")) and mirror_from >= fy_from and mirror_to <= fy_to
return {
"requested_financial_year": fy,
"ready_for_financial_year": ready,
"financial_year_reason": "" if ready else (
f"Mirror contains {mirror_from.isoformat()} to {mirror_to.isoformat()}, "
f"not FY {fy}."
),
"financial_year_from": fy_from.isoformat(),
"financial_year_to": fy_to.isoformat(),
}
def _accounting_mirror_status(self, payload: dict[str, Any]) -> dict[str, Any]:
client_id = int(payload.get("client_id") or 0)
if client_id <= 0:
raise ValueError("client_id is required.")
return self.tally.mirror.status(client_id)
result = self.tally.mirror.status(client_id)
mirror = result.get("mirror") or {}
fy_check = self._mirror_financial_year_match(mirror, str(payload.get("financial_year") or ""))
mirror.update(fy_check)
# Keep file-level readiness for diagnostics, but expose the effective
# readiness used by ERP analysis pages separately.
result["mirror"] = mirror
result["ready_for_financial_year"] = bool(fy_check.get("ready_for_financial_year"))
return result
def _accounting_mirror_sync(self, payload: dict[str, Any]) -> dict[str, Any]:
client_id = int(payload.get("client_id") or 0)