from __future__ import annotations from datetime import date, datetime, timezone from pathlib import Path import re from urllib.parse import quote from fastapi import APIRouter, Form, Request from fastapi.responses import RedirectResponse from sqlalchemy import select from app.core.db.common import CommonSessionLocal from app.core.security.csrf import get_or_create_csrf_token, validate_csrf from app.core.security.session_auth import get_current_user from app.core.templating import templates from app.modules.clients.models import Client from app.modules.core.rbac.deps import get_user_permissions, get_user_roles from app.modules.core.rbac.permission_guard import require_permission from app.modules.documents.services import build_document_scope, get_active_storage_node_for_branch, client_folder_parts, sanitize_segment from app.modules.accounting.agent_bridge import request_agent_command from app.modules.registrations.models import ClientRegistration, RegistrationType router = APIRouter(prefix="/tools/tally", tags=["accounting-tally-ui"]) def _denied(): from app.core.http_responses import ui_access_denied return ui_access_denied() def _require_partner(request: Request, db, permission: str): user = get_current_user(request, db) if not user: return None, RedirectResponse(url="/login", status_code=303) roles = set(get_user_roles(db, user.id)) if "Partner" not in roles: return None, _denied() try: require_permission(db, user, permission) except Exception: return None, _denied() return user, None def _visible_clients(db, request: Request, user): scope = build_document_scope(request, db, user) stmt = select(Client).where(Client.tenant_id == scope.tenant_id, Client.partner_id == user.id) if scope.branch_id is not None: stmt = stmt.where(Client.branch_id == scope.branch_id) return db.execute(stmt.order_by(Client.client_name.asc(), Client.id.asc())).scalars().all(), scope def _find_visible_client(db, request: Request, user, client_id: int): clients, scope = _visible_clients(db, request, user) client = next((row for row in clients if int(row.id) == int(client_id)), None) return client, clients, scope def _financial_year_for_date(value: date | None = None) -> str: value = value or date.today() start_year = value.year if value.month >= 4 else value.year - 1 return f"{start_year}-{str(start_year + 1)[-2:]}" def _accounting_storage_payload(client, financial_year: str | None = None) -> dict: """Use the same FY + client-folder policy as existing Engagement storage.""" fy_value = str(financial_year or _financial_year_for_date()).strip() fy_folder = sanitize_segment(f"FY{fy_value}", "FY") letter, client_folder = client_folder_parts(client, int(client.id)) relative_dir = Path(fy_folder) / "Clients" / letter / client_folder / "Accounting" return { "client_id": int(client.id), "client_name": str(client.client_name or "").strip(), "client_code": str(getattr(client, "client_code", "") or "").strip(), "financial_year": fy_value, "accounting_relative_dir": relative_dir.as_posix(), } def _client_registrations(db, client, tenant_id: int): rows = db.execute( select(ClientRegistration, RegistrationType) .join(RegistrationType, RegistrationType.id == ClientRegistration.registration_type_id) .where( ClientRegistration.tenant_id == tenant_id, ClientRegistration.client_id == client.id, ) .order_by(RegistrationType.sort_order.asc(), ClientRegistration.id.asc()) ).all() return rows def _registration_payload(registration, registration_type) -> dict: return { "id": int(registration.id), "registration_type_id": int(registration.registration_type_id), "registration_type_code": str(getattr(registration_type, "code", "") or "").strip(), "registration_type_name": str(getattr(registration_type, "name", "") or "").strip(), "registration_number": str(getattr(registration, "registration_number", "") or "").strip(), "legal_name": str(getattr(registration, "legal_name", "") or "").strip(), "trade_name": str(getattr(registration, "trade_name", "") or "").strip(), "business_unit_id": getattr(registration, "business_unit_id", None), "client_branch_id": getattr(registration, "client_branch_id", None), } def _normalise_gstin(value: str) -> str: return re.sub(r"\s+", "", str(value or "").strip().upper()) def _erp_client_gstins(registrations) -> list[str]: values: list[str] = [] for registration, registration_type in registrations or []: if str(getattr(registration_type, "code", "") or "").strip().upper() != "GSTIN": continue number = _normalise_gstin(getattr(registration, "registration_number", "")) if number and number not in values: values.append(number) return values def _enrich_gstin_display(live_result: dict | None, registrations) -> None: """Attach ERP/Tally GSTIN display metadata without mutating stored mappings.""" if not isinstance(live_result, dict): return accounting = live_result.get("accounting") or {} tally = live_result.get("tally") or {} mappings = accounting.get("mappings") or [] companies = tally.get("companies") or [] client_gstins = _erp_client_gstins(registrations) company_by_guid = {str(row.get("guid") or "").strip(): row for row in companies if isinstance(row, dict)} for mapping in mappings: if not isinstance(mapping, dict): continue registration_type = str(mapping.get("registration_type_code") or "").strip().upper() registration_number = _normalise_gstin(mapping.get("registration_number")) if mapping.get("registration_id") and registration_type == "GSTIN" and registration_number: erp_gstins = [registration_number] elif not mapping.get("registration_id"): erp_gstins = list(client_gstins) else: erp_gstins = [] live_company = company_by_guid.get(str(mapping.get("tally_guid") or "").strip()) or {} tally_gstin = _normalise_gstin(live_company.get("gstin") or mapping.get("gstin")) mapping["erp_gstins"] = erp_gstins mapping["tally_gstin_display"] = tally_gstin if erp_gstins and tally_gstin: mapping["gstin_match_status"] = "matched" if tally_gstin in erp_gstins else "mismatch" elif erp_gstins and not tally_gstin: mapping["gstin_match_status"] = "tally_missing" elif tally_gstin and not erp_gstins: mapping["gstin_match_status"] = "erp_missing" else: mapping["gstin_match_status"] = "unavailable" def _node_online(node) -> bool: if not node or not node.last_seen_at_utc: return False seen = node.last_seen_at_utc if seen.tzinfo is None: seen = seen.replace(tzinfo=timezone.utc) return (datetime.now(timezone.utc) - seen).total_seconds() <= 180 def _render(request: Request, db, user, **context): base = { "request": request, "current_user": user, "current_user_roles": get_user_roles(db, user.id), "current_user_permissions": get_user_permissions(db, user.id), "csrf_token": get_or_create_csrf_token(request), } base.update(context) return templates.TemplateResponse( "modules/accounting/templates/accounting/tally.html", base, ) @router.get("") def tally_tool( request: Request, client_id: int | None = None, refresh: int = 0, initialized: int = 0, mapped: int = 0, unmapped: int = 0, synced: int = 0, transactions_synced: int = 0, error: str = "", ): db = CommonSessionLocal() try: user, response = _require_partner(request, db, "accounting.tally.view") if response: return response clients, scope = _visible_clients(db, request, user) selected_client = next((row for row in clients if client_id and int(row.id) == int(client_id)), None) registrations = _client_registrations(db, selected_client, scope.tenant_id) if selected_client else [] node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id) online = _node_online(node) live_result = None command_error = error or "" should_query_agent = bool(refresh or selected_client) if refresh: try: require_permission(db, user, "accounting.tally.connect") except Exception: return _denied() if should_query_agent and node and online: payload = {} if selected_client: payload = _accounting_storage_payload(selected_client) try: response_data = request_agent_command( node.node_code, "phase4_status", payload, timeout_seconds=20, ) if response_data.get("ok"): live_result = response_data.get("result") or {} _enrich_gstin_display(live_result, registrations) else: command_error = str(response_data.get("error") or "Local agent command failed.") except Exception as exc: command_error = str(exc) today = date.today() fy_start_year = today.year if today.month >= 4 else today.year - 1 transaction_date_from = date(fy_start_year, 4, 1).isoformat() transaction_date_to = today.isoformat() return _render( request, db, user, title="Tally Accounting", clients=clients, selected_client=selected_client, registrations=registrations, storage_node=node, agent_online=online, live_result=live_result, initialized=bool(initialized), mapped=bool(mapped), unmapped=bool(unmapped), synced=bool(synced), transactions_synced=bool(transactions_synced), transaction_date_from=transaction_date_from, transaction_date_to=transaction_date_to, command_error=command_error, ) finally: db.close() @router.post("/initialize") def initialize_accounting_storage( request: Request, client_id: int = Form(...), csrf_token: str = Form(...), ): validate_csrf(request, csrf_token) db = CommonSessionLocal() try: user, response = _require_partner(request, db, "accounting.act.initialize") if response: return response client, _clients, scope = _find_visible_client(db, request, user, client_id) if not client: return _denied() 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?client_id={client.id}&error={quote('ERP Local Agent is offline for the active branch.')}", status_code=303, ) try: result = request_agent_command( node.node_code, "accounting_initialize", { **_accounting_storage_payload(client), "tenant_id": int(scope.tenant_id), "requested_by_user_id": int(user.id), }, timeout_seconds=20, ) if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Accounting storage initialization failed.")) except Exception as exc: return RedirectResponse( url=f"/tools/tally?client_id={client.id}&error={quote(str(exc))}", status_code=303, ) return RedirectResponse( url=f"/tools/tally?client_id={client.id}&refresh=1&initialized=1", status_code=303, ) finally: db.close() @router.post("/map-company") def map_tally_company( request: Request, client_id: int = Form(...), tally_guid: str = Form(...), registration_id: str = Form(""), allow_gstin_mismatch: bool = Form(False), csrf_token: str = Form(...), ): validate_csrf(request, csrf_token) db = CommonSessionLocal() try: user, response = _require_partner(request, db, "accounting.tally.map_company") if response: return response client, _clients, scope = _find_visible_client(db, request, user, client_id) if not client: return _denied() 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?client_id={client.id}&error={quote('ERP Local Agent is offline for the active branch.')}", status_code=303, ) registration_payload = None if str(registration_id or "").strip(): try: rid = int(registration_id) except Exception: return RedirectResponse( url=f"/tools/tally?client_id={client.id}&error={quote('Invalid registration selection.')}", status_code=303, ) row = db.execute( select(ClientRegistration, RegistrationType) .join(RegistrationType, RegistrationType.id == ClientRegistration.registration_type_id) .where( ClientRegistration.id == rid, ClientRegistration.client_id == client.id, ClientRegistration.tenant_id == scope.tenant_id, ) ).first() if not row: return _denied() registration_payload = _registration_payload(row[0], row[1]) try: result = request_agent_command( node.node_code, "accounting_map_company", { **_accounting_storage_payload(client), "tenant_id": int(scope.tenant_id), "registration": registration_payload, "tally_guid": str(tally_guid or "").strip(), "allow_gstin_mismatch": bool(allow_gstin_mismatch), "mapped_by_user_id": int(user.id), }, timeout_seconds=20, ) if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Tally company mapping failed.")) except Exception as exc: return RedirectResponse( url=f"/tools/tally?client_id={client.id}&error={quote(str(exc))}", status_code=303, ) return RedirectResponse( url=f"/tools/tally?client_id={client.id}&refresh=1&mapped=1", status_code=303, ) finally: db.close() @router.post("/unmap-company") def unmap_tally_company( request: Request, client_id: int = Form(...), mapping_id: int = Form(...), csrf_token: str = Form(...), ): validate_csrf(request, csrf_token) db = CommonSessionLocal() try: user, response = _require_partner(request, db, "accounting.tally.map_company") if response: return response client, _clients, scope = _find_visible_client(db, request, user, client_id) if not client: return _denied() 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?client_id={client.id}&error={quote('ERP Local Agent is offline for the active branch.')}", status_code=303, ) try: result = request_agent_command( node.node_code, "accounting_unmap_company", { **_accounting_storage_payload(client), "mapping_id": int(mapping_id), "unmapped_by_user_id": int(user.id), }, timeout_seconds=20, ) if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Tally company unmapping failed.")) except Exception as exc: return RedirectResponse( url=f"/tools/tally?client_id={client.id}&error={quote(str(exc))}", status_code=303, ) return RedirectResponse( url=f"/tools/tally?client_id={client.id}&refresh=1&unmapped=1", status_code=303, ) finally: db.close() @router.post("/sync-masters") def sync_tally_masters( request: Request, client_id: int = Form(...), tally_guid: str = Form(...), csrf_token: str = Form(...), ): validate_csrf(request, csrf_token) db = CommonSessionLocal() try: user, response = _require_partner(request, db, "accounting.tally.sync_masters") if response: return response client, _clients, scope = _find_visible_client(db, request, user, client_id) if not client: return _denied() 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?client_id={client.id}&error={quote('ERP Local Agent is offline for the active branch.')}", status_code=303, ) try: result = request_agent_command( node.node_code, "accounting_sync_masters", { **_accounting_storage_payload(client), "tally_guid": str(tally_guid or "").strip(), "requested_by_user_id": int(user.id), }, timeout_seconds=120, ) if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Tally master synchronization failed.")) except Exception as exc: return RedirectResponse( url=f"/tools/tally?client_id={client.id}&error={quote(str(exc))}", status_code=303, ) return RedirectResponse( url=f"/tools/tally?client_id={client.id}&refresh=1&synced=1", status_code=303, ) finally: db.close() @router.post("/sync-transactions") def sync_tally_transactions( request: Request, client_id: int = Form(...), tally_guid: str = Form(...), date_from: str = Form(...), date_to: str = Form(...), csrf_token: str = Form(...), ): validate_csrf(request, csrf_token) db = CommonSessionLocal() try: user, response = _require_partner(request, db, "accounting.tally.sync_transactions") if response: return response client, _clients, scope = _find_visible_client(db, request, user, client_id) if not client: return _denied() try: start = date.fromisoformat(str(date_from or "")) end = date.fromisoformat(str(date_to or "")) if start > end: raise ValueError("From Date cannot be after To Date.") if (end - start).days > 370: raise ValueError("Transaction sync is limited to 371 days per run. Sync additional periods separately.") except Exception as exc: return RedirectResponse(url=f"/tools/tally?client_id={client.id}&error={quote(str(exc))}", status_code=303) 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?client_id={client.id}&error={quote('ERP Local Agent is offline for the active branch.')}", status_code=303) try: result = request_agent_command( node.node_code, "accounting_sync_transactions", { **_accounting_storage_payload(client, _financial_year_for_date(start)), "tally_guid": str(tally_guid or "").strip(), "date_from": start.isoformat(), "date_to": end.isoformat(), "requested_by_user_id": int(user.id), }, timeout_seconds=300, ) if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Tally transaction synchronization failed.")) except Exception as exc: return RedirectResponse(url=f"/tools/tally?client_id={client.id}&error={quote(str(exc))}", status_code=303) return RedirectResponse(url=f"/tools/tally?client_id={client.id}&refresh=1&transactions_synced=1", status_code=303) finally: db.close() @router.get("/depreciation") def depreciation_it_tool( request: Request, client_id: int | None = None, tally_guid: str = "", fy_start: str = "", fy_end: str = "", calculated: int = 0, approved: int = 0, posted: int = 0, run_id: int | None = None, error: str = "", ): db = CommonSessionLocal() try: user, response = _require_partner(request, db, "accounting.tally.depreciation_it") if response: return response clients, scope = _visible_clients(db, request, user) selected_client = next((row for row in clients if client_id and int(row.id) == int(client_id)), None) node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id) online = _node_online(node) today = date.today(); start_year = today.year if today.month >= 4 else today.year - 1 start_text = fy_start or date(start_year,4,1).isoformat(); end_text = fy_end or date(start_year+1,3,31).isoformat() live_result = None; preview = None; depreciation_run = None; command_error = error or "" if selected_client and node and online: try: status_response = request_agent_command(node.node_code,"phase6_status",_accounting_storage_payload(selected_client, _financial_year_for_date(date.fromisoformat(start_text))),timeout_seconds=20) if status_response.get("ok"): live_result=status_response.get("result") or {} else: command_error=str(status_response.get("error") or "Local Agent status failed.") chosen_guid=str(tally_guid or "").strip() if chosen_guid: preview_response=request_agent_command(node.node_code,"accounting_depreciation_preview",{**_accounting_storage_payload(selected_client, _financial_year_for_date(date.fromisoformat(start_text))),"tally_guid":chosen_guid,"fy_start":start_text,"fy_end":end_text},timeout_seconds=60) if preview_response.get("ok"): preview=(preview_response.get("result") or {}).get("preview") else: command_error=str(preview_response.get("error") or "Depreciation preview failed.") if run_id: run_response=request_agent_command(node.node_code,"accounting_get_it_depreciation_run",{**_accounting_storage_payload(selected_client, _financial_year_for_date(date.fromisoformat(start_text))),"run_id":int(run_id)},timeout_seconds=30) if run_response.get("ok"): depreciation_run=(run_response.get("result") or {}).get("depreciation") except Exception as exc: command_error=str(exc) base={"request":request,"current_user":user,"current_user_roles":get_user_roles(db,user.id),"current_user_permissions":get_user_permissions(db,user.id),"csrf_token":get_or_create_csrf_token(request)} base.update({"title":"Income-tax Depreciation","clients":clients,"selected_client":selected_client,"storage_node":node,"agent_online":online,"live_result":live_result,"preview":preview,"selected_tally_guid":str(tally_guid or ""),"fy_start":start_text,"fy_end":end_text,"calculated":bool(calculated),"approved":bool(approved),"posted":bool(posted),"depreciation_run":depreciation_run,"command_error":command_error}) return templates.TemplateResponse("modules/accounting/templates/accounting/depreciation_it.html",base) finally: db.close() @router.post("/depreciation/calculate") async def calculate_it_depreciation(request: Request): form = await request.form() csrf_token = str(form.get("csrf_token") or ""); validate_csrf(request, csrf_token) db = CommonSessionLocal() try: user, response = _require_partner(request, db, "accounting.tally.depreciation_it") if response: return response try: client_id=int(form.get("client_id")); start=date.fromisoformat(str(form.get("fy_start") or "")); end=date.fromisoformat(str(form.get("fy_end") or "")) except Exception as exc: return RedirectResponse(url=f"/tools/tally/depreciation?error={quote('Invalid client or financial-year dates.')}",status_code=303) client, _clients, scope = _find_visible_client(db, request, user, client_id) if not client: return _denied() tally_guid=str(form.get("tally_guid") or "").strip() assignments=[] for key,value in form.multi_items(): if not str(key).startswith("rate__"): continue ledger_key=str(key)[6:] try: rate=float(str(value or "0").strip() or 0) except Exception: rate=-1 assignments.append({"ledger_key":ledger_key,"rate_percent":rate}) 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}&tally_guid={quote(tally_guid)}&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)),"tally_guid":tally_guid,"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 ""),"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: return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client.id}&tally_guid={quote(tally_guid)}&fy_start={start.isoformat()}&fy_end={end.isoformat()}&error={quote(str(exc))}",status_code=303) return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client.id}&tally_guid={quote(tally_guid)}&fy_start={start.isoformat()}&fy_end={end.isoformat()}&calculated=1&run_id={rid}",status_code=303) finally: db.close() @router.post("/depreciation/approve") async def approve_it_depreciation(request: Request): form = await request.form() validate_csrf(request, str(form.get("csrf_token") or "")) db = CommonSessionLocal() try: user, response = _require_partner(request, db, "accounting.tally.writeback") if response: return response try: client_id = int(form.get("client_id")); run_id = int(form.get("run_id")) approval_fy_start = date.fromisoformat(str(form.get("fy_start") or "")) except Exception: return RedirectResponse(url="/tools/tally/depreciation?error=" + quote("Invalid depreciation draft or financial year."), status_code=303) client, _clients, scope = _find_visible_client(db, request, user, client_id) if not client: return _denied() if str(form.get("confirm_approval") or "") != "yes": return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client_id}&run_id={run_id}&error={quote('Confirm that you reviewed the depreciation draft before approval.')}", status_code=303) 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}&run_id={run_id}&error={quote('ERP Local Agent is offline for the active branch.')}", status_code=303) 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 "")}, timeout_seconds=30, ) if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Approval failed.")) dep = (result.get("result") or {}).get("depreciation") or {} guid = quote(str(dep.get("tally_guid") or "")) return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client_id}&tally_guid={guid}&fy_start={dep.get('fy_start','')}&fy_end={dep.get('fy_end','')}&run_id={run_id}&approved=1", status_code=303) except Exception as exc: return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client_id}&run_id={run_id}&error={quote(str(exc))}", status_code=303) finally: db.close() @router.post("/depreciation/post-to-tally") async def post_it_depreciation_to_tally(request: Request): form = await request.form() validate_csrf(request, str(form.get("csrf_token") or "")) db = CommonSessionLocal() try: user, response = _require_partner(request, db, "accounting.tally.writeback") if response: return response try: client_id = int(form.get("client_id")); run_id = int(form.get("run_id")) posting_fy_start = date.fromisoformat(str(form.get("fy_start") or "")) except Exception: return RedirectResponse(url="/tools/tally/depreciation?error=" + quote("Invalid depreciation draft or financial year."), status_code=303) client, _clients, scope = _find_visible_client(db, request, user, client_id) if not client: return _denied() if str(form.get("post_confirmation") or "").strip().upper() != "POST": return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client_id}&run_id={run_id}&error={quote('Type POST exactly to confirm Tally write-back.')}", status_code=303) if str(form.get("confirm_posting") or "") != "yes": return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client_id}&run_id={run_id}&error={quote('Confirm that you want to create the Journal voucher in TallyPrime.')}", status_code=303) 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}&run_id={run_id}&error={quote('ERP Local Agent is offline for the active branch.')}", status_code=303) 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)}, timeout_seconds=120, ) if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Tally write-back failed.")) dep = (result.get("result") or {}).get("depreciation") or {} guid = quote(str(dep.get("tally_guid") or "")) return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client_id}&tally_guid={guid}&fy_start={dep.get('fy_start','')}&fy_end={dep.get('fy_end','')}&run_id={run_id}&posted=1", status_code=303) except Exception as exc: return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client_id}&run_id={run_id}&error={quote(str(exc))}", status_code=303) finally: db.close()