|
|
|
@@ -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)
|
|
|
|
|