Validate FY mirrors and repair depreciation and TDS review
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user