diff --git a/app/modules/accounting/accounting_analytics_source.py b/app/modules/accounting/accounting_analytics_source.py index 1e19b8a..b63f2a8 100644 --- a/app/modules/accounting/accounting_analytics_source.py +++ b/app/modules/accounting/accounting_analytics_source.py @@ -21,9 +21,10 @@ from app.modules.accounting.accounting_mirror_service import ( class AccountingAnalyticsDataSource: """Common read-only data source for accounting/audit analytical procedures. - This class is additive. Existing tools are not silently redirected in v1.23.0. - New or migrated tools can use the mirror immediately while current Opening - Balance, Cash Payment, Depreciation and posting workflows continue unchanged. + Read-only accounting/audit analysis is mirror-first. UI analysis workflows + resolve their Client/FY through AccountingMirrorRegistry before constructing + 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]): diff --git a/app/modules/accounting/accounting_mirror_service.py b/app/modules/accounting/accounting_mirror_service.py index b09d70d..b20afd3 100644 --- a/app/modules/accounting/accounting_mirror_service.py +++ b/app/modules/accounting/accounting_mirror_service.py @@ -936,7 +936,7 @@ def register_accounting_mirror_version( is_active=True, status="active", 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, 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.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 - mirror_local_path = str(job.get("accounting_db_path") or mirror.get("path") or "").strip() + 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("mirror_db_path") or mirror.get("path") or job.get("accounting_db_path") or "").strip() if 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() @@ -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_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) - 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: current.mirror_local_path = discovered_path current.updated_at_utc = _mirror_utcnow() diff --git a/app/modules/accounting/cash_payment_ui.py b/app/modules/accounting/cash_payment_ui.py index 361ed06..c0e1be2 100644 --- a/app/modules/accounting/cash_payment_ui.py +++ b/app/modules/accounting/cash_payment_ui.py @@ -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.ui import ( _accounting_storage_payload, + _analysis_accounting_storage_payload, _denied, _find_visible_client, _financial_year_for_date, @@ -263,9 +264,11 @@ def cash_payment_review( "SQLite Accounting Mirror, so the storage agent must be online." ) else: - storage_payload = _accounting_storage_payload( + storage_payload = _analysis_accounting_storage_payload( selected_client, review_fy, + db=db, + tenant_id=scope.tenant_id, ) try: status_response = request_agent_command( @@ -387,7 +390,7 @@ async def cash_payment_ledgers(request: Request): try: fy = _financial_year_for_date(date.fromisoformat(date_from)) 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, "company_name": company_name, "ledger_scope": ledger_scope, @@ -470,7 +473,7 @@ def cash_payment_ledgers_progress( }) fy = _financial_year_for_date(date.fromisoformat(date_from)) 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), "tally_guid": str(tally_guid or ""), "company_name": str(company_name or ""), @@ -576,7 +579,7 @@ async def start_cash_payment_cache(request: Request): node.node_code, "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, "company_name": company_name, "date_from": date_from, @@ -632,7 +635,7 @@ async def cancel_cash_payment_cache(request: Request): node.node_code, "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, }, 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( node.node_code, "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, ) except Exception as exc: diff --git a/app/modules/accounting/creditors_aging_ui.py b/app/modules/accounting/creditors_aging_ui.py index ecbfa31..797c18f 100644 --- a/app/modules/accounting/creditors_aging_ui.py +++ b/app/modules/accounting/creditors_aging_ui.py @@ -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.ui import ( _accounting_storage_payload, + _analysis_accounting_storage_payload, _active_financial_year, _financial_year_bounds, _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}." follow_up_start = follow_start.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: result = sundry_creditors_aging( node_code=str(node.node_code), diff --git a/app/modules/accounting/gst_reconciliation_ui.py b/app/modules/accounting/gst_reconciliation_ui.py index 46119f7..8287096 100644 --- a/app/modules/accounting/gst_reconciliation_ui.py +++ b/app/modules/accounting/gst_reconciliation_ui.py @@ -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.templating import templates 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.credential_vault.crypto import decrypt_value 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) -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") letter, client_folder = client_folder_parts(client, int(client.id)) root = Path(fy_folder) / "Clients" / letter / client_folder - accounting = _accounting_storage_payload(client, fy) - accounting_dir = str(accounting.get("accounting_relative_dir") or (root / "Accounting").as_posix()).strip() - return accounting_dir, (root / "GST" / sanitize_segment(f"GSTIN_{gstin}", "GSTIN")).as_posix() + if require_registered: + accounting = _analysis_accounting_storage_payload( + 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): @@ -490,7 +495,8 @@ def start_download( # Ensure FY is consistent in single-month mode. if download_mode == "single": 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 token=_encode_operator_token({ "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)) if download_mode == "single": 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 = { "GSTR1": gstr1_files or [], "GSTR2B": gstr2b_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) 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.") - 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) 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=[] for p in periods: 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"): results.append((res.get("result") or {}).get("analysis") or {}) if not results: raise RuntimeError("No stored GST periods could be reconciled.") diff --git a/app/modules/accounting/opening_balance_ui.py b/app/modules/accounting/opening_balance_ui.py index 0379475..544b2e0 100644 --- a/app/modules/accounting/opening_balance_ui.py +++ b/app/modules/accounting/opening_balance_ui.py @@ -25,6 +25,7 @@ from app.modules.accounting.opening_balance_service import ( ) from app.modules.accounting.ui import ( _accounting_storage_payload, + _analysis_accounting_storage_payload, _financial_year_for_date, _find_visible_client, _node_online, @@ -187,7 +188,7 @@ def page( response = request_agent_command( node.node_code, "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, ) status = (response.get("result") or {}) if response.get("ok") else {} @@ -570,8 +571,8 @@ def compare( if not node or not _node_online(node): raise ValueError("ERP Local Agent is offline for the active branch.") - previous_payload = _accounting_storage_payload(client, previous_fy) - current_payload = _accounting_storage_payload(client, current_fy) + previous_payload = _analysis_accounting_storage_payload(client, previous_fy, db=db, tenant_id=scope.tenant_id) + current_payload = _analysis_accounting_storage_payload(client, current_fy, db=db, tenant_id=scope.tenant_id) response = request_agent_command( node.node_code, "accounting_opening_balance_mirror_snapshot", @@ -768,7 +769,7 @@ async def apply_corrections( node.node_code, "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_guid": run.current_company_guid, "source_key": f"OPENING-RUN-{run.id}", diff --git a/app/modules/accounting/tds_ui.py b/app/modules/accounting/tds_ui.py index 2f645bc..a633daf 100644 --- a/app/modules/accounting/tds_ui.py +++ b/app/modules/accounting/tds_ui.py @@ -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.modules.accounting.agent_bridge import request_agent_command 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.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() live=None; review=None; error=""; history=[] 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) if st.get("ok"): live=st.get("result") or {} 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) 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())) - 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.")) 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) @@ -247,7 +247,7 @@ async def reconciliation_run(request:Request): 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.") 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.")) 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 "")) diff --git a/app/modules/accounting/ui.py b/app/modules/accounting/ui.py index d542537..d737788 100644 --- a/app/modules/accounting/ui.py +++ b/app/modules/accounting/ui.py @@ -190,6 +190,39 @@ def _accounting_storage_payload(client, financial_year: str | None = None, *, db 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): rows = db.execute( select(ClientRegistration, RegistrationType) @@ -535,7 +568,7 @@ def download_analysis_report( node.node_code, "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), }, timeout_seconds=30, @@ -1109,7 +1142,7 @@ def depreciation_it_tool( if selected_client and node and online: 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: status_response=request_agent_command( 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_end=date(prior_start_year+1,3,31) 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( 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) 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: - 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.")) dep=(result.get("result") or {}).get("depreciation") or {}; rid=int(dep.get("run_id")) except Exception as exc: @@ -1255,7 +1288,7 @@ async def approve_it_depreciation(request: Request): try: result = request_agent_command( 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, ) 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: result = request_agent_command( 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, ) if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Tally write-back failed.")) diff --git a/app/modules/documents/agent_package.py b/app/modules/documents/agent_package.py index e8a8861..6a15b66 100644 --- a/app/modules/documents/agent_package.py +++ b/app/modules/documents/agent_package.py @@ -4,7 +4,7 @@ import io from pathlib import Path import zipfile -ERP_LOCAL_AGENT_VERSION = "1.26.19" +ERP_LOCAL_AGENT_VERSION = "1.26.20" 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) diff --git a/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py b/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py index 9239fde..108ff62 100644 --- a/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py +++ b/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py @@ -1,2 +1,2 @@ -__version__ = "1.26.19" +__version__ = "1.26.20" AGENT_NAME = "ERP Local Agent" diff --git a/app/modules/documents/local_agent_runtime/erp_local_agent/commands.py b/app/modules/documents/local_agent_runtime/erp_local_agent/commands.py index 5fd1a19..c2df1c7 100644 --- a/app/modules/documents/local_agent_runtime/erp_local_agent/commands.py +++ b/app/modules/documents/local_agent_runtime/erp_local_agent/commands.py @@ -679,7 +679,10 @@ class AgentCommandProcessor: raise ValueError("Client GST and Accounting storage paths are required.") root = Path(self.config.storage_root) 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(): raise ValueError("GST data has not yet been downloaded for this client/period.") if not mirror_db.exists(): @@ -4219,10 +4222,17 @@ class AgentCommandProcessor: client_id = int(payload.get("client_id") or 0) if client_id <= 0: 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]] = [] - pattern = f"FY*/Clients/*/*/Accounting/{filename}" - for path in sorted(self.store.storage_root.glob(pattern)): + pattern = f"FY*/Clients/*/*/Accounting/{mirror_filename}" + 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(): continue try: @@ -4254,7 +4264,8 @@ class AgentCommandProcessor: "client_id": client_id, "financial_year": fy, "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(), "tally_guid": str(company.get("company_guid") or "").strip(), "mirror": {