Show Tally data and mirror paths on engagement workspace

This commit is contained in:
A R R R Associates
2026-09-19 17:36:20 +05:30
parent 53732a7270
commit 827231a135
2 changed files with 163 additions and 16 deletions
+112 -3
View File
@@ -6,9 +6,10 @@ from typing import Any, Iterable
from sqlalchemy import select from sqlalchemy import select
from app.modules.documents.models import DocumentStorageJob from app.modules.documents.models import BranchStorageNode, DocumentStorageJob, EngagementDocument
from app.modules.documents.services import client_folder_parts, get_active_storage_node_for_branch, sanitize_segment from app.modules.documents.services import client_folder_parts, get_active_storage_node_for_branch, sanitize_segment
from app.modules.services.models import FirmServiceTaskTemplate, ServiceTaskCategory from app.modules.services.models import FirmServiceTaskTemplate, ServiceTaskCategory
from app.modules.accounting.accounting_mirror_models import AccountingMirrorRegistry
@@ -61,6 +62,81 @@ def _latest_local_final_path(db, version: Any) -> str:
return str(getattr(job, "local_final_path", "") or "").strip() if job else "" return str(getattr(job, "local_final_path", "") or "").strip() if job else ""
def _current_mirror_for_year(db, *, tenant_id: int, client_id: int, financial_year: str):
if not tenant_id or not client_id or not financial_year:
return None
return db.execute(
select(AccountingMirrorRegistry).where(
AccountingMirrorRegistry.tenant_id == int(tenant_id),
AccountingMirrorRegistry.client_id == int(client_id),
AccountingMirrorRegistry.financial_year == str(financial_year).strip(),
AccountingMirrorRegistry.is_current.is_(True),
AccountingMirrorRegistry.is_active.is_(True),
AccountingMirrorRegistry.status == "active",
).order_by(AccountingMirrorRegistry.version_no.desc(), AccountingMirrorRegistry.id.desc()).limit(1)
).scalar_one_or_none()
def _mirror_row(db, *, tenant_id: int, client_id: int, financial_year: str) -> dict[str, Any]:
mirror = _current_mirror_for_year(
db, tenant_id=tenant_id, client_id=client_id, financial_year=financial_year
)
if not mirror:
return {
"available": False,
"financial_year": financial_year,
"local_path": "",
"relative_path": "",
"version_no": 0,
"company_name": "",
"last_synced_at_utc": None,
}
relative_path = (Path(str(mirror.accounting_relative_dir or "")) / str(mirror.mirror_file_name or "")).as_posix()
local_path = ""
node = db.get(BranchStorageNode, int(mirror.storage_node_id)) if getattr(mirror, "storage_node_id", None) else None
if node:
root = str(getattr(node, "storage_root_path", "") or "").strip()
local_path = _join_local_path(root, relative_path) if root else ""
return {
"available": True,
"financial_year": financial_year,
"local_path": local_path,
"relative_path": relative_path,
"version_no": int(getattr(mirror, "version_no", 0) or 0),
"company_name": str(getattr(mirror, "company_name", "") or ""),
"last_synced_at_utc": getattr(mirror, "last_synced_at_utc", None),
"storage_node_name": str(getattr(node, "node_name", "") or "") if node else "",
}
def _tally_row_from_document(db, doc: Any, storage_root_path: str) -> dict[str, Any] | None:
tally_year = _tally_document_year(doc)
if not tally_year:
return None
versions = list(getattr(doc, "versions", None) or [])
latest = versions[0] if versions else None
latest_relative_path = str(getattr(latest, "local_relative_path", "") or "") if latest else ""
local_final_path = _latest_local_final_path(db, latest) if latest else ""
# local_final_path is already the workstation/storage-node acknowledged final
# destination. For extracted ZIP/TAR Tally uploads it is the directory that
# can be pasted into TallyPrime, so never prepend storage_root_path to it.
local_path = local_final_path or (_join_local_path(storage_root_path, latest_relative_path) if latest_relative_path else "")
return {
"id": int(getattr(doc, "id", 0) or 0),
"engagement_id": int(getattr(doc, "engagement_id", 0) or 0),
"title": str(getattr(doc, "title", "") or "Tally Data"),
"financial_year": tally_year,
"version_no": int(getattr(doc, "current_version_no", 0) or 0),
"latest_filename": str(getattr(latest, "original_filename", "") or "") if latest else "",
"latest_relative_path": latest_relative_path,
"local_final_path": local_final_path,
"local_path": local_path,
"storage_status": str(getattr(latest, "storage_status", "") or "") if latest else "",
}
def _task_category_options(db, engagement, tasks: list[Any]) -> list[dict[str, Any]]: def _task_category_options(db, engagement, tasks: list[Any]) -> list[dict[str, Any]]:
"""Return the service's real task-category master mapped to this engagement's tasks. """Return the service's real task-category master mapped to this engagement's tasks.
@@ -198,6 +274,7 @@ def build_engagement_resource_context(db, engagement, tasks: Iterable[Any], docu
accounting_local_path = "" accounting_local_path = ""
storage_root_path = "" storage_root_path = ""
storage_node_name = "" storage_node_name = ""
tenant_id = int(getattr(engagement, "tenant_id", 0) or 0)
if client and client_id and fy: if client and client_id and fy:
fy_folder = sanitize_segment(f"FY{fy}", "FY") fy_folder = sanitize_segment(f"FY{fy}", "FY")
letter, client_folder = client_folder_parts(client, client_id) letter, client_folder = client_folder_parts(client, client_id)
@@ -206,7 +283,7 @@ def build_engagement_resource_context(db, engagement, tasks: Iterable[Any], docu
accounting_relative_path = (relative_dir / f"client_{client_id:08d}.act").as_posix() accounting_relative_path = (relative_dir / f"client_{client_id:08d}.act").as_posix()
node = get_active_storage_node_for_branch( node = get_active_storage_node_for_branch(
db, db,
int(getattr(engagement, "tenant_id", 0) or 0), tenant_id,
getattr(engagement, "branch_id", None), getattr(engagement, "branch_id", None),
) )
if node: if node:
@@ -214,6 +291,36 @@ def build_engagement_resource_context(db, engagement, tasks: Iterable[Any], docu
storage_node_name = str(getattr(node, "node_name", "") or "").strip() storage_node_name = str(getattr(node, "node_name", "") or "").strip()
accounting_local_path = _join_local_path(storage_root_path, accounting_relative_path) accounting_local_path = _join_local_path(storage_root_path, accounting_relative_path)
# Client/FY accounting resources are shared across engagements for the same
# client. This lets Tax Audit, Statutory Audit, ITR, etc. reuse the same Tally
# dataset and registered mirror instead of requiring duplicate uploads.
client_tally_documents: list[Any] = []
if tenant_id and client_id and tally_year_options:
client_tally_documents = db.execute(
select(EngagementDocument).where(
EngagementDocument.tenant_id == tenant_id,
EngagementDocument.client_id == client_id,
EngagementDocument.is_deleted.is_(False),
EngagementDocument.description.like(f"{_TALLY_MARKER_PREFIX}%"),
EngagementDocument.financial_year.in_(tally_year_options),
).order_by(EngagementDocument.updated_at_utc.desc(), EngagementDocument.id.desc())
).scalars().unique().all()
client_tally_by_fy: dict[str, dict[str, Any]] = {}
for tally_doc in client_tally_documents:
row = _tally_row_from_document(db, tally_doc, storage_root_path)
if row and row["financial_year"] not in client_tally_by_fy:
client_tally_by_fy[row["financial_year"]] = row
accounting_years: list[dict[str, Any]] = []
for year in tally_year_options:
accounting_years.append({
"financial_year": year,
"is_engagement_year": year == fy,
"tally": client_tally_by_fy.get(year),
"mirror": _mirror_row(db, tenant_id=tenant_id, client_id=client_id, financial_year=year),
})
categories = _task_category_options(db, engagement, tasks) categories = _task_category_options(db, engagement, tasks)
default_upload_task_id = next((row["task_id"] for row in categories if row.get("task_id")), None) default_upload_task_id = next((row["task_id"] for row in categories if row.get("task_id")), None)
@@ -241,10 +348,11 @@ def build_engagement_resource_context(db, engagement, tasks: Iterable[Any], docu
} }
document_options.append(row) document_options.append(row)
if tally_year: if tally_year:
tally_local_path = local_final_path or (_join_local_path(storage_root_path, latest_relative_path) if latest_relative_path else "")
tally_data_files.append({ tally_data_files.append({
**row, **row,
"financial_year": tally_year, "financial_year": tally_year,
"local_path": _join_local_path(storage_root_path, displayed_relative_path) if displayed_relative_path else "", "local_path": tally_local_path,
}) })
tally_data_files.sort(key=lambda row: row.get("financial_year", ""), reverse=True) tally_data_files.sort(key=lambda row: row.get("financial_year", ""), reverse=True)
@@ -256,6 +364,7 @@ def build_engagement_resource_context(db, engagement, tasks: Iterable[Any], docu
"storage_root_path": storage_root_path, "storage_root_path": storage_root_path,
"storage_node_name": storage_node_name, "storage_node_name": storage_node_name,
"tally_year_options": tally_year_options, "tally_year_options": tally_year_options,
"accounting_years": accounting_years,
"tally_data_files": tally_data_files, "tally_data_files": tally_data_files,
"task_categories": categories, "task_categories": categories,
"default_upload_task_id": default_upload_task_id, "default_upload_task_id": default_upload_task_id,
@@ -15,25 +15,63 @@
<div class="rounded-2xl border border-indigo-200 bg-indigo-50 p-4"> <div class="rounded-2xl border border-indigo-200 bg-indigo-50 p-4">
<div class="flex flex-wrap items-start justify-between gap-3"> <div class="flex flex-wrap items-start justify-between gap-3">
<div> <div>
<div class="text-xs font-semibold uppercase tracking-wide text-indigo-600">Accounting Intelligence</div> <div class="text-xs font-semibold uppercase tracking-wide text-indigo-600">Client / FY Accounting Data</div>
<div class="mt-1 font-semibold text-indigo-950">Client accounting .act path</div> <div class="mt-1 font-semibold text-indigo-950">Tally data and Accounting Mirror</div>
<p class="mt-1 text-xs text-indigo-700">These resources are linked by client and financial year, so the same data can be reused across this client's engagements.</p>
</div> </div>
{% if resources.storage_node_name %}<span class="rounded-full bg-white px-2.5 py-1 text-xs font-semibold text-indigo-700">{{ resources.storage_node_name }}</span>{% endif %} {% if resources.storage_node_name %}<span class="rounded-full bg-white px-2.5 py-1 text-xs font-semibold text-indigo-700">{{ resources.storage_node_name }}</span>{% endif %}
</div> </div>
{% if resources.accounting_local_path %}
<div class="mt-3 flex gap-2"> <div class="mt-4 space-y-3">
<input id="engagement-accounting-path" readonly value="{{ resources.accounting_local_path }}" class="min-w-0 flex-1 rounded-xl border border-indigo-200 bg-white px-3 py-2 font-mono text-xs text-slate-700"> {% for year_row in resources.accounting_years %}
<button type="button" data-copy-target="engagement-accounting-path" class="rounded-xl bg-indigo-600 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-700">Copy Path</button> <div class="rounded-xl border border-indigo-200 bg-white p-3">
<div class="flex flex-wrap items-center justify-between gap-2">
<div class="text-sm font-semibold text-slate-900">FY {{ year_row.financial_year }}{% if year_row.is_engagement_year %} <span class="text-xs font-medium text-indigo-600">· engagement year</span>{% else %} <span class="text-xs font-medium text-slate-500">· previous year</span>{% endif %}</div>
<div class="flex flex-wrap gap-1.5">
<span class="rounded-full px-2 py-1 text-[11px] font-semibold {{ 'bg-emerald-100 text-emerald-700' if year_row.tally and year_row.tally.local_path else 'bg-amber-100 text-amber-700' }}">Tally {{ 'Available' if year_row.tally and year_row.tally.local_path else 'Not available' }}</span>
<span class="rounded-full px-2 py-1 text-[11px] font-semibold {{ 'bg-emerald-100 text-emerald-700' if year_row.mirror and year_row.mirror.available else 'bg-slate-100 text-slate-600' }}">Mirror {{ 'Linked' if year_row.mirror and year_row.mirror.available else 'Not linked' }}</span>
</div> </div>
<p class="mt-2 text-xs text-indigo-700">This is the existing .act accounting/intelligence database path used by the Local Agent. Uploaded Tally data paths are shown separately on the right.</p> </div>
{% elif resources.accounting_relative_path %}
<div class="mt-3 rounded-xl border border-indigo-200 bg-white px-3 py-2 font-mono text-xs text-slate-700">{{ resources.accounting_relative_path }}</div> <div class="mt-3 grid gap-3 xl:grid-cols-2">
<p class="mt-2 text-xs text-amber-700">The relative accounting path is known, but this branch does not currently expose a configured local storage root.</p> <div class="rounded-lg border border-emerald-200 bg-emerald-50 p-2.5">
<div class="text-xs font-semibold uppercase tracking-wide text-emerald-700">Tally Data Storage Path</div>
{% if year_row.tally and year_row.tally.local_path %}
<div class="mt-2 flex gap-2">
<input id="client-tally-path-{{ loop.index }}" readonly value="{{ year_row.tally.local_path }}" class="min-w-0 flex-1 rounded-lg border border-emerald-200 bg-white px-2.5 py-2 font-mono text-[11px] text-slate-700">
<button type="button" data-copy-target="client-tally-path-{{ loop.index }}" class="rounded-lg bg-emerald-600 px-2.5 py-2 text-xs font-semibold text-white hover:bg-emerald-700">Copy Path</button>
</div>
<p class="mt-1 text-[11px] text-emerald-700">Paste this storage/extracted-data path into TallyPrime when working with this FY.</p>
{% if year_row.tally.latest_filename %}<div class="mt-1 text-[11px] text-slate-500">{{ year_row.tally.latest_filename }} · v{{ year_row.tally.version_no }}</div>{% endif %}
{% else %} {% else %}
<p class="mt-3 text-sm text-slate-600">Accounting path is unavailable for this engagement.</p> <p class="mt-2 text-xs text-amber-700">No usable Tally storage path is registered for this client/FY yet.</p>
{% endif %} {% endif %}
</div> </div>
<div class="rounded-lg border border-indigo-200 bg-indigo-50 p-2.5">
<div class="text-xs font-semibold uppercase tracking-wide text-indigo-700">Accounting Mirror (.act)</div>
{% if year_row.mirror and year_row.mirror.available %}
{% if year_row.mirror.local_path %}
<div class="mt-2 flex gap-2">
<input id="client-mirror-path-{{ loop.index }}" readonly value="{{ year_row.mirror.local_path }}" class="min-w-0 flex-1 rounded-lg border border-indigo-200 bg-white px-2.5 py-2 font-mono text-[11px] text-slate-700">
<button type="button" data-copy-target="client-mirror-path-{{ loop.index }}" class="rounded-lg bg-indigo-600 px-2.5 py-2 text-xs font-semibold text-white hover:bg-indigo-700">Copy Path</button>
</div>
{% elif year_row.mirror.relative_path %}
<div class="mt-2 break-all rounded-lg border border-indigo-200 bg-white px-2.5 py-2 font-mono text-[11px] text-slate-700">{{ year_row.mirror.relative_path }}</div>
{% endif %}
<div class="mt-1 text-[11px] text-slate-500">{% if year_row.mirror.company_name %}{{ year_row.mirror.company_name }} · {% endif %}v{{ year_row.mirror.version_no }}{% if year_row.mirror.last_synced_at_utc %} · synced {{ year_row.mirror.last_synced_at_utc }}{% endif %}</div>
{% else %}
<p class="mt-2 text-xs text-slate-600">No active Accounting Mirror is linked to this client/FY.</p>
{% endif %}
</div>
</div>
</div>
{% else %}
<div class="rounded-xl border border-dashed border-indigo-200 bg-white p-4 text-sm text-slate-500">Financial-year accounting resources are unavailable for this engagement.</div>
{% endfor %}
</div>
</div>
<div> <div>
<div class="mb-2 flex items-center justify-between gap-3"> <div class="mb-2 flex items-center justify-between gap-3">
<div class="text-sm font-semibold text-slate-900">Current Engagement Files</div> <div class="text-sm font-semibold text-slate-900">Current Engagement Files</div>