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:
"""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]):
@@ -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()
+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.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:
+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.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),
@@ -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.")
+5 -4
View File
@@ -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}",
+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.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 ""))
+39 -6
View File
@@ -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."))