Unify accounting analysis on Client FY mirror registry

This commit is contained in:
A R R R Associates
2026-09-20 16:36:14 +05:30
parent 24b13e5d6e
commit c63fc2773c
11 changed files with 103 additions and 45 deletions
@@ -21,9 +21,10 @@ from app.modules.accounting.accounting_mirror_service import (
class AccountingAnalyticsDataSource: class AccountingAnalyticsDataSource:
"""Common read-only data source for accounting/audit analytical procedures. """Common read-only data source for accounting/audit analytical procedures.
This class is additive. Existing tools are not silently redirected in v1.23.0. Read-only accounting/audit analysis is mirror-first. UI analysis workflows
New or migrated tools can use the mirror immediately while current Opening resolve their Client/FY through AccountingMirrorRegistry before constructing
Balance, Cash Payment, Depreciation and posting workflows continue unchanged. this source or invoking Local-Agent mirror commands. Live Tally remains a
bootstrap/write channel only; analytical reads use the registered .act mirror.
""" """
def __init__(self, *, node_code: str, accounting_payload: dict[str, Any]): def __init__(self, *, node_code: str, accounting_payload: dict[str, Any]):
@@ -936,7 +936,7 @@ def register_accounting_mirror_version(
is_active=True, is_active=True,
status="active", status="active",
accounting_relative_dir=str(accounting_relative_dir or "").strip(), accounting_relative_dir=str(accounting_relative_dir or "").strip(),
mirror_file_name=Path(str(job.get("accounting_db_path") or f"client_{int(client_id):08d}.act")).name, mirror_file_name=Path(str(job.get("mirror_db_path") or mirror.get("path") or job.get("accounting_db_path") or f"client_{int(client_id):08d}_mirror.act")).name,
created_by_user_id=requested_by_user_id, created_by_user_id=requested_by_user_id,
supersedes_mirror_id=(int(current.id) if current else None), supersedes_mirror_id=(int(current.id) if current else None),
) )
@@ -969,8 +969,8 @@ def register_accounting_mirror_version(
row.storage_node_id = int(storage_node_id) if storage_node_id else None row.storage_node_id = int(storage_node_id) if storage_node_id else None
row.accounting_relative_dir = str(accounting_relative_dir or row.accounting_relative_dir or "").strip() row.accounting_relative_dir = str(accounting_relative_dir or row.accounting_relative_dir or "").strip()
row.mirror_file_name = Path(str(job.get("accounting_db_path") or row.mirror_file_name or f"client_{int(client_id):08d}.act")).name row.mirror_file_name = Path(str(job.get("mirror_db_path") or mirror.get("path") or job.get("accounting_db_path") or row.mirror_file_name or f"client_{int(client_id):08d}_mirror.act")).name
mirror_local_path = str(job.get("accounting_db_path") or mirror.get("path") or "").strip() mirror_local_path = str(job.get("mirror_db_path") or mirror.get("path") or job.get("accounting_db_path") or "").strip()
if mirror_local_path: if mirror_local_path:
row.mirror_local_path = mirror_local_path row.mirror_local_path = mirror_local_path
tally_relative = str(job.get("tally_data_relative_path") or mirror.get("tally_data_relative_path") or "").strip() tally_relative = str(job.get("tally_data_relative_path") or mirror.get("tally_data_relative_path") or "").strip()
@@ -1040,7 +1040,7 @@ def upsert_registered_mirror(
current.company_name = str(((mirror or {}).get("company") or {}).get("company_name") or current.company_name or "").strip() current.company_name = str(((mirror or {}).get("company") or {}).get("company_name") or current.company_name or "").strip()
current.company_guid = str(((mirror or {}).get("company") or {}).get("company_guid") or current.company_guid or "").strip() current.company_guid = str(((mirror or {}).get("company") or {}).get("company_guid") or current.company_guid or "").strip()
current.file_size_bytes = int((mirror or {}).get("size_bytes") or current.file_size_bytes or 0) current.file_size_bytes = int((mirror or {}).get("size_bytes") or current.file_size_bytes or 0)
discovered_path = str((job or {}).get("accounting_db_path") or (mirror or {}).get("path") or "").strip() discovered_path = str((job or {}).get("mirror_db_path") or (mirror or {}).get("path") or (job or {}).get("accounting_db_path") or "").strip()
if discovered_path: if discovered_path:
current.mirror_local_path = discovered_path current.mirror_local_path = discovered_path
current.updated_at_utc = _mirror_utcnow() current.updated_at_utc = _mirror_utcnow()
+9 -6
View File
@@ -18,6 +18,7 @@ from app.modules.accounting.agent_bridge import request_agent_command
from app.modules.accounting.cash_payment_models import CashPaymentRuleSetting from app.modules.accounting.cash_payment_models import CashPaymentRuleSetting
from app.modules.accounting.ui import ( from app.modules.accounting.ui import (
_accounting_storage_payload, _accounting_storage_payload,
_analysis_accounting_storage_payload,
_denied, _denied,
_find_visible_client, _find_visible_client,
_financial_year_for_date, _financial_year_for_date,
@@ -263,9 +264,11 @@ def cash_payment_review(
"SQLite Accounting Mirror, so the storage agent must be online." "SQLite Accounting Mirror, so the storage agent must be online."
) )
else: else:
storage_payload = _accounting_storage_payload( storage_payload = _analysis_accounting_storage_payload(
selected_client, selected_client,
review_fy, review_fy,
db=db,
tenant_id=scope.tenant_id,
) )
try: try:
status_response = request_agent_command( status_response = request_agent_command(
@@ -387,7 +390,7 @@ async def cash_payment_ledgers(request: Request):
try: try:
fy = _financial_year_for_date(date.fromisoformat(date_from)) fy = _financial_year_for_date(date.fromisoformat(date_from))
payload = { payload = {
**_accounting_storage_payload(selected_client, fy), **_analysis_accounting_storage_payload(selected_client, fy, db=db, tenant_id=scope.tenant_id),
"tally_guid": tally_guid, "tally_guid": tally_guid,
"company_name": company_name, "company_name": company_name,
"ledger_scope": ledger_scope, "ledger_scope": ledger_scope,
@@ -470,7 +473,7 @@ def cash_payment_ledgers_progress(
}) })
fy = _financial_year_for_date(date.fromisoformat(date_from)) fy = _financial_year_for_date(date.fromisoformat(date_from))
payload = { payload = {
**_accounting_storage_payload(selected_client, fy), **_analysis_accounting_storage_payload(selected_client, fy, db=db, tenant_id=scope.tenant_id),
"job_id": str(job_id), "job_id": str(job_id),
"tally_guid": str(tally_guid or ""), "tally_guid": str(tally_guid or ""),
"company_name": str(company_name or ""), "company_name": str(company_name or ""),
@@ -576,7 +579,7 @@ async def start_cash_payment_cache(request: Request):
node.node_code, node.node_code,
"accounting_cash_payment_cache_start", "accounting_cash_payment_cache_start",
{ {
**_accounting_storage_payload(selected_client, _financial_year_for_date(parsed_from)), **_analysis_accounting_storage_payload(selected_client, _financial_year_for_date(parsed_from), db=db, tenant_id=scope.tenant_id),
"tally_guid": tally_guid, "tally_guid": tally_guid,
"company_name": company_name, "company_name": company_name,
"date_from": date_from, "date_from": date_from,
@@ -632,7 +635,7 @@ async def cancel_cash_payment_cache(request: Request):
node.node_code, node.node_code,
"accounting_cash_payment_cache_cancel", "accounting_cash_payment_cache_cancel",
{ {
**_accounting_storage_payload(selected_client, _financial_year_for_date(parsed_from)), **_analysis_accounting_storage_payload(selected_client, _financial_year_for_date(parsed_from), db=db, tenant_id=scope.tenant_id),
"job_id": job_id, "job_id": job_id,
}, },
timeout_seconds=15, timeout_seconds=15,
@@ -665,7 +668,7 @@ def cash_payment_cache_progress(request: Request, client_id: int, job_id: str, d
result = request_agent_command( result = request_agent_command(
node.node_code, node.node_code,
"accounting_cash_payment_cache_status", "accounting_cash_payment_cache_status",
{**_accounting_storage_payload(selected_client, fy), "job_id": str(job_id)}, {**_analysis_accounting_storage_payload(selected_client, fy, db=db, tenant_id=scope.tenant_id), "job_id": str(job_id)},
timeout_seconds=20, timeout_seconds=20,
) )
except Exception as exc: except Exception as exc:
+3 -2
View File
@@ -21,6 +21,7 @@ from app.modules.accounting.accounting_mirror_service import (
from app.modules.accounting.agent_bridge import request_agent_command from app.modules.accounting.agent_bridge import request_agent_command
from app.modules.accounting.ui import ( from app.modules.accounting.ui import (
_accounting_storage_payload, _accounting_storage_payload,
_analysis_accounting_storage_payload,
_active_financial_year, _active_financial_year,
_financial_year_bounds, _financial_year_bounds,
_find_visible_client, _find_visible_client,
@@ -111,9 +112,9 @@ def _load_analysis(request: Request, db, user, client_id: int, financial_year: s
return None, clients, scope, f"No registered Accounting Mirror is available for follow-up FY {follow_up_fy}." return None, clients, scope, f"No registered Accounting Mirror is available for follow-up FY {follow_up_fy}."
follow_up_start = follow_start.isoformat() follow_up_start = follow_start.isoformat()
follow_up_end = follow_end.isoformat() follow_up_end = follow_end.isoformat()
follow_up_payload = _accounting_storage_payload(client, follow_up_fy) follow_up_payload = _analysis_accounting_storage_payload(client, follow_up_fy, db=db, tenant_id=scope.tenant_id)
payload = _accounting_storage_payload(client, financial_year) payload = _analysis_accounting_storage_payload(client, financial_year, db=db, tenant_id=scope.tenant_id)
try: try:
result = sundry_creditors_aging( result = sundry_creditors_aging(
node_code=str(node.node_code), node_code=str(node.node_code),
@@ -21,7 +21,7 @@ from app.core.security.csrf import get_or_create_csrf_token, validate_csrf
from app.core.settings import get_settings from app.core.settings import get_settings
from app.core.templating import templates from app.core.templating import templates
from app.modules.accounting.agent_bridge import request_agent_command from app.modules.accounting.agent_bridge import request_agent_command
from app.modules.accounting.ui import _accounting_storage_payload, _find_visible_client, _require_partner, _visible_clients, _node_online from app.modules.accounting.ui import _accounting_storage_payload, _analysis_accounting_storage_payload, _find_visible_client, _require_partner, _visible_clients, _node_online
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
from app.modules.credential_vault.crypto import decrypt_value from app.modules.credential_vault.crypto import decrypt_value
from app.modules.credential_vault.models import CredentialVaultEntry from app.modules.credential_vault.models import CredentialVaultEntry
@@ -256,13 +256,18 @@ def _vault_entries(db, user, request, tenant_id: int, client_id: int, registrati
return sorted(eligible, key=rank) return sorted(eligible, key=rank)
def _storage_payload(client, fy: str, gstin: str) -> tuple[str, str]: def _storage_payload(client, fy: str, gstin: str, *, db=None, tenant_id: int | None = None, require_registered: bool = False) -> tuple[dict, str]:
fy_folder = sanitize_segment(f"FY{fy}", "FY") fy_folder = sanitize_segment(f"FY{fy}", "FY")
letter, client_folder = client_folder_parts(client, int(client.id)) letter, client_folder = client_folder_parts(client, int(client.id))
root = Path(fy_folder) / "Clients" / letter / client_folder root = Path(fy_folder) / "Clients" / letter / client_folder
accounting = _accounting_storage_payload(client, fy) if require_registered:
accounting_dir = str(accounting.get("accounting_relative_dir") or (root / "Accounting").as_posix()).strip() accounting = _analysis_accounting_storage_payload(
return accounting_dir, (root / "GST" / sanitize_segment(f"GSTIN_{gstin}", "GSTIN")).as_posix() client, fy, db=db, tenant_id=tenant_id
)
else:
accounting = _accounting_storage_payload(client, fy, db=db, tenant_id=tenant_id)
accounting.setdefault("accounting_relative_dir", (root / "Accounting").as_posix())
return accounting, (root / "GST" / sanitize_segment(f"GSTIN_{gstin}", "GSTIN")).as_posix()
def _redirect(client_id: int, **params): def _redirect(client_id: int, **params):
@@ -490,7 +495,8 @@ def start_download(
# Ensure FY is consistent in single-month mode. # Ensure FY is consistent in single-month mode.
if download_mode == "single": if download_mode == "single":
financial_year=_fy_for_period(periods[0]) financial_year=_fy_for_period(periods[0])
accounting_dir,gst_dir=_storage_payload(client,financial_year,gstin) accounting_payload,gst_dir=_storage_payload(client,financial_year,gstin,db=db,tenant_id=scope.tenant_id)
accounting_dir=str(accounting_payload.get("accounting_relative_dir") or "")
jti=uuid.uuid4().hex jti=uuid.uuid4().hex
token=_encode_operator_token({ token=_encode_operator_token({
"jti":jti,"user_id":int(user.id),"tenant_id":int(scope.tenant_id),"branch_id":int(scope.branch_id), "jti":jti,"user_id":int(user.id),"tenant_id":int(scope.tenant_id),"branch_id":int(scope.branch_id),
@@ -610,7 +616,8 @@ async def import_downloaded_returns(
allowed_periods = set(_selected_periods(download_mode, financial_year, period)) allowed_periods = set(_selected_periods(download_mode, financial_year, period))
if download_mode == "single": if download_mode == "single":
financial_year = _fy_for_period(next(iter(allowed_periods))) financial_year = _fy_for_period(next(iter(allowed_periods)))
accounting_dir, gst_dir = _storage_payload(client, financial_year, gstin) accounting_payload, gst_dir = _storage_payload(client, financial_year, gstin, db=db, tenant_id=scope.tenant_id)
accounting_dir = str(accounting_payload.get("accounting_relative_dir") or "")
groups = { groups = {
"GSTR1": gstr1_files or [], "GSTR2B": gstr2b_files or [], "GSTR1": gstr1_files or [], "GSTR2B": gstr2b_files or [],
"GSTR3B": gstr3b_files or [], "GSTR2A": gstr2a_files or [], "GSTR3B": gstr3b_files or [], "GSTR2A": gstr2a_files or [],
@@ -719,13 +726,14 @@ def analyze(request: Request, client_id: int=Form(...), registration_id: int=For
reg,_=pair; gstin=_norm_gstin(reg.registration_number) reg,_=pair; gstin=_norm_gstin(reg.registration_number)
periods=_periods_for_fy(financial_year) if analyze_mode=="full_fy" else [re.sub(r"\D","",period or "")] periods=_periods_for_fy(financial_year) if analyze_mode=="full_fy" else [re.sub(r"\D","",period or "")]
if any(len(p)!=6 for p in periods): raise ValueError("Enter a valid MMYYYY period.") if any(len(p)!=6 for p in periods): raise ValueError("Enter a valid MMYYYY period.")
accounting_dir,gst_dir=_storage_payload(client,financial_year,gstin) accounting_payload,gst_dir=_storage_payload(client,financial_year,gstin,db=db,tenant_id=scope.tenant_id,require_registered=True)
accounting_dir=str(accounting_payload.get("accounting_relative_dir") or "")
node=get_active_storage_node_for_branch(db,scope.tenant_id,scope.branch_id) node=get_active_storage_node_for_branch(db,scope.tenant_id,scope.branch_id)
if not node or not _node_online(node): return _redirect(client_id,registration_id=registration_id,period=period,financial_year=financial_year,error="Local Storage Agent is offline.") if not node or not _node_online(node): return _redirect(client_id,registration_id=registration_id,period=period,financial_year=financial_year,error="Local Storage Agent is offline.")
results=[] results=[]
for p in periods: for p in periods:
date_from,date_to=_period_bounds(p) date_from,date_to=_period_bounds(p)
res=request_agent_command(node.node_code,"gst_reconciliation_analyze",{"client_id":client.id,"gstin":gstin,"financial_year":financial_year,"period":p,"gst_relative_dir":gst_dir,"accounting_relative_dir":accounting_dir,"date_from":date_from,"date_to":date_to},timeout_seconds=30) res=request_agent_command(node.node_code,"gst_reconciliation_analyze",{**accounting_payload,"client_id":client.id,"gstin":gstin,"financial_year":financial_year,"period":p,"gst_relative_dir":gst_dir,"accounting_relative_dir":accounting_dir,"date_from":date_from,"date_to":date_to},timeout_seconds=30)
if res.get("ok"): if res.get("ok"):
results.append((res.get("result") or {}).get("analysis") or {}) results.append((res.get("result") or {}).get("analysis") or {})
if not results: raise RuntimeError("No stored GST periods could be reconciled.") if not results: raise RuntimeError("No stored GST periods could be reconciled.")
+5 -4
View File
@@ -25,6 +25,7 @@ from app.modules.accounting.opening_balance_service import (
) )
from app.modules.accounting.ui import ( from app.modules.accounting.ui import (
_accounting_storage_payload, _accounting_storage_payload,
_analysis_accounting_storage_payload,
_financial_year_for_date, _financial_year_for_date,
_find_visible_client, _find_visible_client,
_node_online, _node_online,
@@ -187,7 +188,7 @@ def page(
response = request_agent_command( response = request_agent_command(
node.node_code, node.node_code,
"accounting_mirror_status", "accounting_mirror_status",
_accounting_storage_payload(selected, fy_value), _analysis_accounting_storage_payload(selected, fy_value, db=db, tenant_id=scope.tenant_id),
timeout_seconds=20, timeout_seconds=20,
) )
status = (response.get("result") or {}) if response.get("ok") else {} status = (response.get("result") or {}) if response.get("ok") else {}
@@ -570,8 +571,8 @@ def compare(
if not node or not _node_online(node): if not node or not _node_online(node):
raise ValueError("ERP Local Agent is offline for the active branch.") raise ValueError("ERP Local Agent is offline for the active branch.")
previous_payload = _accounting_storage_payload(client, previous_fy) previous_payload = _analysis_accounting_storage_payload(client, previous_fy, db=db, tenant_id=scope.tenant_id)
current_payload = _accounting_storage_payload(client, current_fy) current_payload = _analysis_accounting_storage_payload(client, current_fy, db=db, tenant_id=scope.tenant_id)
response = request_agent_command( response = request_agent_command(
node.node_code, node.node_code,
"accounting_opening_balance_mirror_snapshot", "accounting_opening_balance_mirror_snapshot",
@@ -768,7 +769,7 @@ async def apply_corrections(
node.node_code, node.node_code,
"accounting_opening_balance_apply", "accounting_opening_balance_apply",
{ {
**_accounting_storage_payload(client, selected_financial_year or None), **_analysis_accounting_storage_payload(client, selected_financial_year or None, db=db, tenant_id=scope.tenant_id),
"current_company_name": run.current_company_name, "current_company_name": run.current_company_name,
"current_company_guid": run.current_company_guid, "current_company_guid": run.current_company_guid,
"source_key": f"OPENING-RUN-{run.id}", "source_key": f"OPENING-RUN-{run.id}",
+4 -4
View File
@@ -14,7 +14,7 @@ from app.core.security.csrf import get_or_create_csrf_token, validate_csrf
from app.core.templating import templates from app.core.templating import templates
from app.modules.accounting.agent_bridge import request_agent_command from app.modules.accounting.agent_bridge import request_agent_command
from app.modules.accounting.tds_models import AccountingTDSPostingInstruction, AccountingTDSRule from app.modules.accounting.tds_models import AccountingTDSPostingInstruction, AccountingTDSRule
from app.modules.accounting.ui import _accounting_storage_payload, _financial_year_for_date, _node_online, _require_partner, _visible_clients from app.modules.accounting.ui import _accounting_storage_payload, _analysis_accounting_storage_payload, _financial_year_for_date, _node_online, _require_partner, _visible_clients
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
from app.modules.documents.services import get_active_storage_node_for_branch from app.modules.documents.services import get_active_storage_node_for_branch
@@ -70,7 +70,7 @@ def _workspace(request, db, user, client_id, tally_guid, date_from, date_to, run
start=date_from or date(sy,4,1).isoformat(); end=date_to or today.isoformat() start=date_from or date(sy,4,1).isoformat(); end=date_to or today.isoformat()
live=None; review=None; error=""; history=[] live=None; review=None; error=""; history=[]
if selected and node and online: if selected and node and online:
storage = _accounting_storage_payload(selected,_financial_year_for_date(date.fromisoformat(start))) storage = _analysis_accounting_storage_payload(selected,_financial_year_for_date(date.fromisoformat(start)), db=db, tenant_id=scope.tenant_id)
st=request_agent_command(node.node_code,"accounting_mirror_status",storage,timeout_seconds=20) st=request_agent_command(node.node_code,"accounting_mirror_status",storage,timeout_seconds=20)
if st.get("ok"): live=st.get("result") or {} if st.get("ok"): live=st.get("result") or {}
else: error=str(st.get("error") or "Accounting Mirror status failed.") else: error=str(st.get("error") or "Accounting Mirror status failed.")
@@ -204,7 +204,7 @@ async def post_liability(request:Request,instruction_id:int):
node=get_active_storage_node_for_branch(db,scope.tenant_id,scope.branch_id) node=get_active_storage_node_for_branch(db,scope.tenant_id,scope.branch_id)
if not node or not _node_online(node): raise ValueError("Local Agent is offline.") if not node or not _node_online(node): raise ValueError("Local Agent is offline.")
voucher_date=str(form.get("voucher_date") or (row.source_voucher_date.isoformat() if row.source_voucher_date else date.today().isoformat())) voucher_date=str(form.get("voucher_date") or (row.source_voucher_date.isoformat() if row.source_voucher_date else date.today().isoformat()))
result=request_agent_command(node.node_code,"accounting_post_tds_liability",{**_accounting_storage_payload(client,_financial_year_for_date(date.fromisoformat(voucher_date))),"tally_guid":str(form.get("tally_guid") or row.company_guid or ""),"voucher_date":voucher_date,"party_ledger":row.party_ledger,"tds_ledger":str(form.get("tds_ledger") or row.tds_ledger),"amount":row.liability_amount,"reference":row.erp_reference,"narration":row.narration or f"TDS liability {row.erp_reference}","posted_by_user_id":int(user.id)},timeout_seconds=120) result=request_agent_command(node.node_code,"accounting_post_tds_liability",{**_analysis_accounting_storage_payload(client,_financial_year_for_date(date.fromisoformat(voucher_date)), db=db, tenant_id=_tenant_id(request,user)),"tally_guid":str(form.get("tally_guid") or row.company_guid or ""),"voucher_date":voucher_date,"party_ledger":row.party_ledger,"tds_ledger":str(form.get("tds_ledger") or row.tds_ledger),"amount":row.liability_amount,"reference":row.erp_reference,"narration":row.narration or f"TDS liability {row.erp_reference}","posted_by_user_id":int(user.id)},timeout_seconds=120)
if not result.get("ok"): raise ValueError(str(result.get("error") or "Tally posting failed.")) if not result.get("ok"): raise ValueError(str(result.get("error") or "Tally posting failed."))
rr=result.get("result") or {}; row.status="posted"; row.tds_ledger=str(form.get("tds_ledger") or row.tds_ledger); row.posted_by_user_id=user.id; row.posted_at_utc=datetime.utcnow(); row.tally_voucher_id=str(rr.get("last_voucher_id") or rr.get("voucher_id") or "") or None; db.commit() rr=result.get("result") or {}; row.status="posted"; row.tds_ledger=str(form.get("tds_ledger") or row.tds_ledger); row.posted_by_user_id=user.id; row.posted_at_utc=datetime.utcnow(); row.tally_voucher_id=str(rr.get("last_voucher_id") or rr.get("voucher_id") or "") or None; db.commit()
return RedirectResponse(f"/tools/accounting/tds/liability?client_id={row.client_id}&saved=1",status_code=303) return RedirectResponse(f"/tools/accounting/tds/liability?client_id={row.client_id}&saved=1",status_code=303)
@@ -247,7 +247,7 @@ async def reconciliation_run(request:Request):
node=get_active_storage_node_for_branch(db,scope.tenant_id,scope.branch_id) node=get_active_storage_node_for_branch(db,scope.tenant_id,scope.branch_id)
if not node or not _node_online(node): raise ValueError("Local Agent is offline.") if not node or not _node_online(node): raise ValueError("Local Agent is offline.")
dfrom=str(form.get("date_from") or ""); dto=str(form.get("date_to") or ""); guid=str(form.get("tally_guid") or "") dfrom=str(form.get("date_from") or ""); dto=str(form.get("date_to") or ""); guid=str(form.get("tally_guid") or "")
rr=request_agent_command(node.node_code,"accounting_tds_compliance",{**_accounting_storage_payload(selected,_financial_year_for_date(date.fromisoformat(dfrom))),"date_from":dfrom,"date_to":dto,"rules":_rule_payload(_rules(db,_tenant_id(request,user)))},timeout_seconds=120) rr=request_agent_command(node.node_code,"accounting_tds_compliance",{**_analysis_accounting_storage_payload(selected,_financial_year_for_date(date.fromisoformat(dfrom)), db=db, tenant_id=_tenant_id(request,user)),"date_from":dfrom,"date_to":dto,"rules":_rule_payload(_rules(db,_tenant_id(request,user)))},timeout_seconds=120)
if not rr.get("ok"): raise ValueError(str(rr.get("error") or "Tally review failed.")) if not rr.get("ok"): raise ValueError(str(rr.get("error") or "Tally review failed."))
review=(rr.get("result") or {}).get("tds_review") or {}; tally_total=round(sum(float(x.get("actual_tds") or 0) for x in review.get("transactions",[])),2); expected_total=round(sum(float(x.get("expected_tds") or 0) for x in review.get("transactions",[])),2) review=(rr.get("result") or {}).get("tds_review") or {}; tally_total=round(sum(float(x.get("actual_tds") or 0) for x in review.get("transactions",[])),2); expected_total=round(sum(float(x.get("expected_tds") or 0) for x in review.get("transactions",[])),2)
returns=_parse_external(str(form.get("return_data") or "")); challans=_parse_external(str(form.get("challan_data") or "")); traces=_parse_external(str(form.get("traces_data") or "")) returns=_parse_external(str(form.get("return_data") or "")); challans=_parse_external(str(form.get("challan_data") or "")); traces=_parse_external(str(form.get("traces_data") or ""))
+39 -6
View File
@@ -190,6 +190,39 @@ def _accounting_storage_payload(client, financial_year: str | None = None, *, db
own_db.close() own_db.close()
def _analysis_accounting_storage_payload(
client,
financial_year: str | None = None,
*,
db=None,
tenant_id: int | None = None,
) -> dict:
"""Return the authoritative registered Client/FY mirror payload for analysis jobs.
Analytical procedures must never silently fall back to a deterministic folder.
The fallback remains available to bootstrap/sync/write workflows through
``_accounting_storage_payload``. Requiring a registry row here guarantees that
every analysis is tied to the same Client/FY/version selected by
``AccountingMirrorRegistry``.
"""
payload = _accounting_storage_payload(
client,
financial_year,
db=db,
tenant_id=tenant_id,
)
if not bool(payload.get("mirror_registered")):
fy_value = str(payload.get("financial_year") or financial_year or "").strip()
raise ValueError(
f"No registered Accounting Mirror is available for FY {fy_value}. "
"Create or register the Client/FY mirror from Tally Accounting first."
)
if not str(payload.get("accounting_relative_dir") or "").strip():
raise ValueError("The registered Accounting Mirror has no storage path.")
payload["analysis_source"] = "accounting_mirror_registry"
return payload
def _client_registrations(db, client, tenant_id: int): def _client_registrations(db, client, tenant_id: int):
rows = db.execute( rows = db.execute(
select(ClientRegistration, RegistrationType) select(ClientRegistration, RegistrationType)
@@ -535,7 +568,7 @@ def download_analysis_report(
node.node_code, node.node_code,
"accounting_analysis_get", "accounting_analysis_get",
{ {
**_accounting_storage_payload(client, financial_year), **_analysis_accounting_storage_payload(client, financial_year, db=db, tenant_id=scope.tenant_id),
"run_id": int(analysis_run_id), "run_id": int(analysis_run_id),
}, },
timeout_seconds=30, timeout_seconds=30,
@@ -1109,7 +1142,7 @@ def depreciation_it_tool(
if selected_client and node and online: if selected_client and node and online:
selected_fy=_financial_year_for_date(date.fromisoformat(start_text)) selected_fy=_financial_year_for_date(date.fromisoformat(start_text))
storage_payload=_accounting_storage_payload(selected_client,selected_fy) storage_payload=_analysis_accounting_storage_payload(selected_client,selected_fy,db=db,tenant_id=scope.tenant_id)
try: try:
status_response=request_agent_command( status_response=request_agent_command(
node.node_code,"accounting_mirror_status",storage_payload,timeout_seconds=20 node.node_code,"accounting_mirror_status",storage_payload,timeout_seconds=20
@@ -1124,7 +1157,7 @@ def depreciation_it_tool(
prior_start=date(prior_start_year,4,1) prior_start=date(prior_start_year,4,1)
prior_end=date(prior_start_year+1,3,31) prior_end=date(prior_start_year+1,3,31)
prior_fy=_financial_year_for_date(prior_start) prior_fy=_financial_year_for_date(prior_start)
prior_payload=_accounting_storage_payload(selected_client,prior_fy) prior_payload=_analysis_accounting_storage_payload(selected_client,prior_fy,db=db,tenant_id=scope.tenant_id)
prior_response=request_agent_command( prior_response=request_agent_command(
node.node_code,"accounting_mirror_status",prior_payload,timeout_seconds=20 node.node_code,"accounting_mirror_status",prior_payload,timeout_seconds=20
) )
@@ -1222,7 +1255,7 @@ async def calculate_it_depreciation(request: Request):
node=get_active_storage_node_for_branch(db,scope.tenant_id,scope.branch_id) node=get_active_storage_node_for_branch(db,scope.tenant_id,scope.branch_id)
if not node or not _node_online(node): return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client.id}&fy_start={start.isoformat()}&fy_end={end.isoformat()}&error={quote('ERP Local Agent is offline for the active branch.')}",status_code=303) if not node or not _node_online(node): return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client.id}&fy_start={start.isoformat()}&fy_end={end.isoformat()}&error={quote('ERP Local Agent is offline for the active branch.')}",status_code=303)
try: try:
result=request_agent_command(node.node_code,"accounting_calculate_it_depreciation",{**_accounting_storage_payload(client, _financial_year_for_date(start)),"fy_start":start.isoformat(),"fy_end":end.isoformat(),"assignments":assignments,"depreciation_expense_ledger":str(form.get("depreciation_expense_ledger") or ""),"depreciation_reserve_ledger":str(form.get("depreciation_reserve_ledger") or ""),"financial_year":_financial_year_for_date(start),"requested_by_user_id":int(user.id)},timeout_seconds=120) result=request_agent_command(node.node_code,"accounting_calculate_it_depreciation",{**_analysis_accounting_storage_payload(client, _financial_year_for_date(start), db=db, tenant_id=scope.tenant_id),"fy_start":start.isoformat(),"fy_end":end.isoformat(),"assignments":assignments,"depreciation_expense_ledger":str(form.get("depreciation_expense_ledger") or ""),"depreciation_reserve_ledger":str(form.get("depreciation_reserve_ledger") or ""),"financial_year":_financial_year_for_date(start),"requested_by_user_id":int(user.id)},timeout_seconds=120)
if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Income-tax depreciation calculation failed.")) if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Income-tax depreciation calculation failed."))
dep=(result.get("result") or {}).get("depreciation") or {}; rid=int(dep.get("run_id")) dep=(result.get("result") or {}).get("depreciation") or {}; rid=int(dep.get("run_id"))
except Exception as exc: except Exception as exc:
@@ -1255,7 +1288,7 @@ async def approve_it_depreciation(request: Request):
try: try:
result = request_agent_command( result = request_agent_command(
node.node_code, "accounting_approve_it_depreciation", node.node_code, "accounting_approve_it_depreciation",
{**_accounting_storage_payload(client, _financial_year_for_date(approval_fy_start)), "run_id": run_id, "approved_by_user_id": int(user.id), "approval_note": str(form.get("approval_note") or "")}, {**_analysis_accounting_storage_payload(client, _financial_year_for_date(approval_fy_start), db=db, tenant_id=scope.tenant_id), "run_id": run_id, "approved_by_user_id": int(user.id), "approval_note": str(form.get("approval_note") or "")},
timeout_seconds=30, timeout_seconds=30,
) )
if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Approval failed.")) if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Approval failed."))
@@ -1293,7 +1326,7 @@ async def post_it_depreciation_to_tally(request: Request):
try: try:
result = request_agent_command( result = request_agent_command(
node.node_code, "accounting_post_it_depreciation", node.node_code, "accounting_post_it_depreciation",
{**_accounting_storage_payload(client, _financial_year_for_date(posting_fy_start)), "run_id": run_id, "posted_by_user_id": int(user.id)}, {**_analysis_accounting_storage_payload(client, _financial_year_for_date(posting_fy_start), db=db, tenant_id=scope.tenant_id), "run_id": run_id, "posted_by_user_id": int(user.id)},
timeout_seconds=120, timeout_seconds=120,
) )
if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Tally write-back failed.")) if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Tally write-back failed."))
+1 -1
View File
@@ -4,7 +4,7 @@ import io
from pathlib import Path from pathlib import Path
import zipfile import zipfile
ERP_LOCAL_AGENT_VERSION = "1.26.19" ERP_LOCAL_AGENT_VERSION = "1.26.20"
ERP_LOCAL_AGENT_NAME = "ERP Local Agent" ERP_LOCAL_AGENT_NAME = "ERP Local Agent"
RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime" RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime"
_DETERMINISTIC_ZIP_TIMESTAMP = (2026, 1, 1, 0, 0, 0) _DETERMINISTIC_ZIP_TIMESTAMP = (2026, 1, 1, 0, 0, 0)
@@ -1,2 +1,2 @@
__version__ = "1.26.19" __version__ = "1.26.20"
AGENT_NAME = "ERP Local Agent" AGENT_NAME = "ERP Local Agent"
@@ -679,7 +679,10 @@ class AgentCommandProcessor:
raise ValueError("Client GST and Accounting storage paths are required.") raise ValueError("Client GST and Accounting storage paths are required.")
root = Path(self.config.storage_root) root = Path(self.config.storage_root)
base_dir = root / Path(gst_relative_dir) / period base_dir = root / Path(gst_relative_dir) / period
mirror_db = root / Path(accounting_relative_dir) / f"client_{client_id:08d}_mirror.act" mirror_file_name = Path(str(payload.get("mirror_file_name") or f"client_{client_id:08d}_mirror.act")).name
if not mirror_file_name.lower().endswith(".act"):
raise ValueError("Registered Accounting Mirror filename is invalid.")
mirror_db = root / Path(accounting_relative_dir) / mirror_file_name
if not base_dir.exists(): if not base_dir.exists():
raise ValueError("GST data has not yet been downloaded for this client/period.") raise ValueError("GST data has not yet been downloaded for this client/period.")
if not mirror_db.exists(): if not mirror_db.exists():
@@ -4219,10 +4222,17 @@ class AgentCommandProcessor:
client_id = int(payload.get("client_id") or 0) client_id = int(payload.get("client_id") or 0)
if client_id <= 0: if client_id <= 0:
raise ValueError("client_id is required.") raise ValueError("client_id is required.")
filename = f"client_{client_id:08d}.act" mirror_filename = f"client_{client_id:08d}_mirror.act"
accounting_filename = f"client_{client_id:08d}.act"
items: list[dict[str, Any]] = [] items: list[dict[str, Any]] = []
pattern = f"FY*/Clients/*/*/Accounting/{filename}" pattern = f"FY*/Clients/*/*/Accounting/{mirror_filename}"
for path in sorted(self.store.storage_root.glob(pattern)): paths = sorted(self.store.storage_root.glob(pattern))
# Legacy fallback: older workstations may have only the client accounting
# database until the next full mirror refresh. Discovery still reports it,
# but new/full exports register the actual *_mirror.act analytical source.
if not paths:
paths = sorted(self.store.storage_root.glob(f"FY*/Clients/*/*/Accounting/{accounting_filename}"))
for path in paths:
if not path.is_file(): if not path.is_file():
continue continue
try: try:
@@ -4254,7 +4264,8 @@ class AgentCommandProcessor:
"client_id": client_id, "client_id": client_id,
"financial_year": fy, "financial_year": fy,
"accounting_relative_dir": relative, "accounting_relative_dir": relative,
"accounting_db_path": str(path), "mirror_db_path": str(path),
"accounting_db_path": str(path.parent / accounting_filename),
"company_name": str(company.get("company_name") or "").strip(), "company_name": str(company.get("company_name") or "").strip(),
"tally_guid": str(company.get("company_guid") or "").strip(), "tally_guid": str(company.get("company_guid") or "").strip(),
"mirror": { "mirror": {