diff --git a/alembic/versions/20260920_accounting_mirror_paths.py b/alembic/versions/20260920_accounting_mirror_paths.py new file mode 100644 index 0000000..308ff37 --- /dev/null +++ b/alembic/versions/20260920_accounting_mirror_paths.py @@ -0,0 +1,46 @@ +"""Store Accounting Mirror and Tally working paths in Client/FY mirror metadata. + +Revision ID: 20260920_accounting_mirror_paths +Revises: 20260918_accounting_mirror_ver +""" +from alembic import op +import sqlalchemy as sa + +revision = "20260920_accounting_mirror_paths" +down_revision = "20260918_accounting_mirror_ver" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + bind = op.get_bind() + inspector = sa.inspect(bind) + columns = {c["name"] for c in inspector.get_columns("accounting_mirror_registry")} + + if "mirror_local_path" not in columns: + op.add_column( + "accounting_mirror_registry", + sa.Column("mirror_local_path", sa.String(length=2000), nullable=False, server_default=""), + ) + if "tally_data_relative_path" not in columns: + op.add_column( + "accounting_mirror_registry", + sa.Column("tally_data_relative_path", sa.String(length=1000), nullable=False, server_default=""), + ) + if "tally_data_local_path" not in columns: + op.add_column( + "accounting_mirror_registry", + sa.Column("tally_data_local_path", sa.String(length=2000), nullable=False, server_default=""), + ) + + +def downgrade() -> None: + bind = op.get_bind() + inspector = sa.inspect(bind) + columns = {c["name"] for c in inspector.get_columns("accounting_mirror_registry")} + if "tally_data_local_path" in columns: + op.drop_column("accounting_mirror_registry", "tally_data_local_path") + if "tally_data_relative_path" in columns: + op.drop_column("accounting_mirror_registry", "tally_data_relative_path") + if "mirror_local_path" in columns: + op.drop_column("accounting_mirror_registry", "mirror_local_path") diff --git a/app/modules/accounting/accounting_mirror_models.py b/app/modules/accounting/accounting_mirror_models.py index 0b83c15..21df934 100644 --- a/app/modules/accounting/accounting_mirror_models.py +++ b/app/modules/accounting/accounting_mirror_models.py @@ -34,6 +34,9 @@ class AccountingMirrorRegistry(CommonBase): financial_year: Mapped[str] = mapped_column(String(7), nullable=False, index=True) accounting_relative_dir: Mapped[str] = mapped_column(String(1000), nullable=False) mirror_file_name: Mapped[str] = mapped_column(String(255), nullable=False) + mirror_local_path: Mapped[str] = mapped_column(String(2000), nullable=False, default="") + tally_data_relative_path: Mapped[str] = mapped_column(String(1000), nullable=False, default="") + tally_data_local_path: Mapped[str] = mapped_column(String(2000), nullable=False, default="") company_name: Mapped[str] = mapped_column(String(255), nullable=False, default="") company_guid: Mapped[str] = mapped_column(String(160), nullable=False, default="") diff --git a/app/modules/accounting/accounting_mirror_service.py b/app/modules/accounting/accounting_mirror_service.py index fd6b7ea..b09d70d 100644 --- a/app/modules/accounting/accounting_mirror_service.py +++ b/app/modules/accounting/accounting_mirror_service.py @@ -790,6 +790,73 @@ def get_registered_mirror(db, tenant_id: int, client_id: int, financial_year: st return get_current_accounting_mirror(db, tenant_id, client_id, financial_year) +def registered_mirror_payload( + db, + *, + tenant_id: int, + client_id: int, + financial_year: str, + fallback: dict[str, Any] | None = None, +) -> dict[str, Any]: + """Return the standard Local-Agent payload using the current registered mirror. + + Existing callers may pass the old deterministic payload as ``fallback``. When a + current Client/FY mirror is registered, its stored directory/file metadata wins. + This keeps all Accounting tools on one source of truth without breaking clients + that have not yet been backfilled into the registry. + """ + payload = dict(fallback or {}) + payload.setdefault("client_id", int(client_id)) + payload["financial_year"] = str(financial_year or payload.get("financial_year") or "").strip() + row = get_current_accounting_mirror(db, int(tenant_id), int(client_id), payload["financial_year"]) + if not row: + payload["mirror_registered"] = False + return payload + payload.update({ + "accounting_relative_dir": str(row.accounting_relative_dir or "").strip(), + "mirror_file_name": str(row.mirror_file_name or "").strip(), + "mirror_local_path": str(getattr(row, "mirror_local_path", "") or "").strip(), + "tally_data_relative_path": str(getattr(row, "tally_data_relative_path", "") or "").strip(), + "tally_data_local_path": str(getattr(row, "tally_data_local_path", "") or "").strip(), + "mirror_registry_id": int(row.id), + "mirror_version_no": int(row.version_no or 1), + "mirror_registered": True, + }) + return payload + + +def update_accounting_mirror_paths( + db, + *, + tenant_id: int, + client_id: int, + financial_year: str, + mirror_local_path: str | None = None, + tally_data_relative_path: str | None = None, + tally_data_local_path: str | None = None, +) -> AccountingMirrorRegistry | None: + """Idempotently enrich the current Client/FY mirror with workstation paths.""" + row = get_current_accounting_mirror(db, tenant_id, client_id, financial_year) + if not row: + return None + changed = False + for attr, value in ( + ("mirror_local_path", mirror_local_path), + ("tally_data_relative_path", tally_data_relative_path), + ("tally_data_local_path", tally_data_local_path), + ): + if value is None: + continue + value = str(value or "").strip() + if value and str(getattr(row, attr, "") or "").strip() != value: + setattr(row, attr, value) + changed = True + if changed: + row.updated_at_utc = _mirror_utcnow() + db.flush() + return row + + def list_accounting_mirror_versions(db, tenant_id: int, client_id: int, financial_year: str) -> list[AccountingMirrorRegistry]: rows = db.execute( select(AccountingMirrorRegistry).where( @@ -903,6 +970,15 @@ 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() + 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() + tally_local = str(job.get("tally_data_local_path") or mirror.get("tally_data_local_path") or "").strip() + if tally_relative: + row.tally_data_relative_path = tally_relative + if tally_local: + row.tally_data_local_path = tally_local row.company_name = str(job.get("company_name") or company.get("company_name") or row.company_name or "").strip() row.company_guid = str(job.get("tally_guid") or company.get("company_guid") or row.company_guid or "").strip() row.voucher_from_date = _parse_mirror_date(voucher_period.get("from_date")) or row.voucher_from_date @@ -964,6 +1040,9 @@ 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() + if discovered_path: + current.mirror_local_path = discovered_path current.updated_at_utc = _mirror_utcnow() db.flush() return current diff --git a/app/modules/accounting/gst_reconciliation_ui.py b/app/modules/accounting/gst_reconciliation_ui.py index 9995ddd..46119f7 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 _find_visible_client, _require_partner, _visible_clients, _node_online +from app.modules.accounting.ui import _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 @@ -260,7 +260,9 @@ def _storage_payload(client, fy: str, gstin: str) -> tuple[str, 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 - return (root / "Accounting").as_posix(), (root / "GST" / sanitize_segment(f"GSTIN_{gstin}", "GSTIN")).as_posix() + 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() def _redirect(client_id: int, **params): diff --git a/app/modules/accounting/templates/accounting/tally.html b/app/modules/accounting/templates/accounting/tally.html index 4593b23..7447e42 100644 --- a/app/modules/accounting/templates/accounting/tally.html +++ b/app/modules/accounting/templates/accounting/tally.html @@ -293,8 +293,16 @@
{% if selected_mirror_registration %} Mirror available: FY {{ selected_mirror_registration.financial_year }} · Version {{ selected_mirror_registration.version_no }}{% if selected_mirror_registration.company_name %} · {{ selected_mirror_registration.company_name }}{% endif %}{% if selected_mirror_registration.last_synced_at_utc %} · last synced {{ selected_mirror_registration.last_synced_at_utc.strftime('%d-%m-%Y %H:%M') }}{% endif %}. Creating this FY again creates a new version and keeps the earlier version. +
+ {% if selected_mirror_registration.mirror_local_path %} +
Registered Mirror Path
{{ selected_mirror_registration.mirror_local_path }}
+ {% endif %} + {% if selected_mirror_registration.tally_data_local_path %} +
Tally Working Data Path
{{ selected_mirror_registration.tally_data_local_path }}
+ {% endif %} +
{% else %} - No registered mirror for FY {{ selected_financial_year }}. Creating the mirror will register it to this client and financial year for Accounting/Audit tools. + No registered mirror for FY {{ selected_financial_year }}. Creating the mirror will register it to this client and financial year for Accounting/Audit tools. Existing canonical mirrors discovered by the Local Agent are adopted automatically; you do not need to recreate them. {% endif %}
diff --git a/app/modules/accounting/ui.py b/app/modules/accounting/ui.py index eebb494..d542537 100644 --- a/app/modules/accounting/ui.py +++ b/app/modules/accounting/ui.py @@ -24,6 +24,8 @@ from app.modules.documents.services import build_document_scope, get_active_stor from app.modules.accounting.agent_bridge import request_agent_command from app.modules.accounting.accounting_mirror_service import ( get_registered_mirror, + get_current_accounting_mirror, + registered_mirror_payload, list_registered_mirrors, list_accounting_mirror_versions, next_accounting_mirror_version, @@ -106,20 +108,87 @@ def _financial_year_options(reference_fy: str | None = None, years_back: int = 7 ] -def _accounting_storage_payload(client, financial_year: str | None = None) -> dict: - """Use the same FY + client-folder policy as existing Engagement storage.""" +def _accounting_storage_payload(client, financial_year: str | None = None, *, db=None, tenant_id: int | None = None) -> dict: + """Resolve the Accounting Mirror path for one Client/FY. + + The deterministic FY/client path remains the backward-compatible fallback. If + a current mirror metadata row exists, its registered path/version becomes the + authoritative payload for every Accounting tool importing this helper. This + keeps old tools working while removing repeated mirror/path selection. + """ 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 { + fallback = { "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(), + "mirror_registered": False, } + own_db = None + try: + session = db + if session is None: + own_db = CommonSessionLocal() + session = own_db + resolved_tenant_id = int(tenant_id or getattr(client, "tenant_id", 0) or 0) + if not resolved_tenant_id: + return fallback + resolved = registered_mirror_payload( + session, + tenant_id=resolved_tenant_id, + client_id=int(client.id), + financial_year=fy_value, + fallback=fallback, + ) + if resolved.get("mirror_registered") and str(resolved.get("mirror_local_path") or "").strip(): + return resolved + + # Backfill mirrors created before the registry existed, and enrich older + # registry rows that pre-date mirror_local_path metadata. This is deliberately + # read-only on the workstation: the agent only lists canonical .act files; + # sync_discovered_mirrors creates missing metadata rows in PostgreSQL. + try: + branch_id = getattr(client, "branch_id", None) + node = get_active_storage_node_for_branch(session, resolved_tenant_id, branch_id) + if node and _node_online(node): + response = request_agent_command( + node.node_code, + "accounting_mirror_list", + {"client_id": int(client.id)}, + timeout_seconds=8, + ) + if response.get("ok"): + sync_discovered_mirrors( + session, + tenant_id=resolved_tenant_id, + client_id=int(client.id), + storage_node_id=int(node.id), + discovered=(response.get("result") or {}).get("mirrors") or [], + requested_by_user_id=None, + ) + resolved = registered_mirror_payload( + session, + tenant_id=resolved_tenant_id, + client_id=int(client.id), + financial_year=fy_value, + fallback=fallback, + ) + except Exception: + pass + return resolved + except Exception: + # Registry lookup must never break an existing Accounting tool; legacy + # deterministic path resolution remains available until backfill occurs. + return fallback + finally: + if own_db is not None: + own_db.close() + def _client_registrations(db, client, tenant_id: int): rows = db.execute( diff --git a/app/modules/services/engagement_resources.py b/app/modules/services/engagement_resources.py index 001fd21..c34d11e 100644 --- a/app/modules/services/engagement_resources.py +++ b/app/modules/services/engagement_resources.py @@ -6,10 +6,10 @@ from typing import Any, Iterable from sqlalchemy import select -from app.modules.documents.models import BranchStorageNode, DocumentStorageJob, EngagementDocument +from app.modules.documents.models import DocumentStorageJob from app.modules.documents.services import client_folder_parts, get_active_storage_node_for_branch, sanitize_segment +from app.modules.accounting.accounting_mirror_service import get_current_accounting_mirror, update_accounting_mirror_paths from app.modules.services.models import FirmServiceTaskTemplate, ServiceTaskCategory -from app.modules.accounting.accounting_mirror_models import AccountingMirrorRegistry @@ -36,6 +36,13 @@ def _tally_document_year(doc: Any) -> str: def _join_local_path(root: str, relative: str) -> str: root = (root or "").strip().rstrip("\\/") + relative = (relative or "").strip() + if not relative: + return "" + # Local Agent may already return an absolute final path (especially for + # extracted Tally ZIP/TAR datasets). Never prefix the Storage Root twice. + if re.match(r"^[A-Za-z]:[\\/]", relative) or relative.startswith("/") or relative.startswith("\\\\"): + return relative if not root: return "" if re.match(r"^[A-Za-z]:", root) or "\\" in root: @@ -62,81 +69,6 @@ def _latest_local_final_path(db, version: Any) -> str: 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]]: """Return the service's real task-category master mapped to this engagement's tasks. @@ -274,13 +206,12 @@ def build_engagement_resource_context(db, engagement, tasks: Iterable[Any], docu accounting_local_path = "" storage_root_path = "" storage_node_name = "" + registered_mirror = None tenant_id = int(getattr(engagement, "tenant_id", 0) or 0) if client and client_id and fy: fy_folder = sanitize_segment(f"FY{fy}", "FY") letter, client_folder = client_folder_parts(client, client_id) - relative_dir = Path(fy_folder) / "Clients" / letter / client_folder / "Accounting" - accounting_relative_dir = relative_dir.as_posix() - accounting_relative_path = (relative_dir / f"client_{client_id:08d}.act").as_posix() + fallback_dir = Path(fy_folder) / "Clients" / letter / client_folder / "Accounting" node = get_active_storage_node_for_branch( db, tenant_id, @@ -289,37 +220,20 @@ def build_engagement_resource_context(db, engagement, tasks: Iterable[Any], docu if node: storage_root_path = str(getattr(node, "storage_root_path", "") or "").strip() storage_node_name = str(getattr(node, "node_name", "") or "").strip() - 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), - }) + registered_mirror = get_current_accounting_mirror(db, tenant_id, client_id, fy) + if registered_mirror: + accounting_relative_dir = str(registered_mirror.accounting_relative_dir or fallback_dir.as_posix()).strip() + mirror_name = str(registered_mirror.mirror_file_name or f"client_{client_id:08d}.act").strip() + accounting_relative_path = (Path(accounting_relative_dir) / mirror_name).as_posix() + accounting_local_path = str(getattr(registered_mirror, "mirror_local_path", "") or "").strip() + if not accounting_local_path and storage_root_path: + accounting_local_path = _join_local_path(storage_root_path, accounting_relative_path) + else: + accounting_relative_dir = fallback_dir.as_posix() + accounting_relative_path = (fallback_dir / f"client_{client_id:08d}.act").as_posix() + if storage_root_path: + accounting_local_path = _join_local_path(storage_root_path, accounting_relative_path) 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) @@ -348,23 +262,70 @@ def build_engagement_resource_context(db, engagement, tasks: Iterable[Any], docu } document_options.append(row) 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({ **row, "financial_year": tally_year, - "local_path": tally_local_path, + "local_path": _join_local_path(storage_root_path, displayed_relative_path) if displayed_relative_path else "", }) tally_data_files.sort(key=lambda row: row.get("financial_year", ""), reverse=True) + # Backfill Tally working paths into the existing Client/FY mirror metadata. + # This is idempotent and lets the Engagement page and all later Accounting + # tools obtain the same path without asking the user to select/upload it again. + metadata_changed = False + for tally in tally_data_files: + tally_fy = str(tally.get("financial_year") or "").strip() + if not tally_fy: + continue + local_path = str(tally.get("local_path") or "").strip() + relative_path = str(tally.get("displayed_relative_path") or tally.get("latest_relative_path") or "").strip() + row = get_current_accounting_mirror(db, tenant_id, client_id, tally_fy) if tenant_id and client_id else None + if not row: + continue + before = ( + str(getattr(row, "tally_data_relative_path", "") or ""), + str(getattr(row, "tally_data_local_path", "") or ""), + ) + update_accounting_mirror_paths( + db, + tenant_id=tenant_id, + client_id=client_id, + financial_year=tally_fy, + tally_data_relative_path=relative_path, + tally_data_local_path=local_path, + ) + after = ( + str(getattr(row, "tally_data_relative_path", "") or ""), + str(getattr(row, "tally_data_local_path", "") or ""), + ) + metadata_changed = metadata_changed or before != after + if metadata_changed: + db.commit() + registered_mirror = get_current_accounting_mirror(db, tenant_id, client_id, fy) if tenant_id and client_id and fy else registered_mirror + + registered_mirror_info = None + if registered_mirror: + registered_mirror_info = { + "id": int(registered_mirror.id), + "financial_year": str(registered_mirror.financial_year or ""), + "version_no": int(registered_mirror.version_no or 1), + "status": str(registered_mirror.status or "active"), + "company_name": str(registered_mirror.company_name or ""), + "mirror_local_path": str(getattr(registered_mirror, "mirror_local_path", "") or accounting_local_path or ""), + "tally_data_relative_path": str(getattr(registered_mirror, "tally_data_relative_path", "") or ""), + "tally_data_local_path": str(getattr(registered_mirror, "tally_data_local_path", "") or ""), + "last_synced_at_utc": getattr(registered_mirror, "last_synced_at_utc", None), + } + return { "accounting_relative_dir": accounting_relative_dir, "accounting_relative_path": accounting_relative_path, "accounting_local_path": accounting_local_path, "storage_root_path": storage_root_path, "storage_node_name": storage_node_name, + "registered_mirror": registered_mirror_info, "tally_year_options": tally_year_options, - "accounting_years": accounting_years, "tally_data_files": tally_data_files, "task_categories": categories, "default_upload_task_id": default_upload_task_id, diff --git a/app/modules/services/templates/services/engagements/_engagement_resources.html b/app/modules/services/templates/services/engagements/_engagement_resources.html index 07b159c..17f8d44 100644 --- a/app/modules/services/templates/services/engagements/_engagement_resources.html +++ b/app/modules/services/templates/services/engagements/_engagement_resources.html @@ -15,61 +15,40 @@
-
Client / FY Accounting Data
-
Tally data and Accounting Mirror
-

These resources are linked by client and financial year, so the same data can be reused across this client's engagements.

+
Accounting Intelligence
+
Client accounting .act path
+ {% if resources.registered_mirror %} +
Registered mirror · FY {{ resources.registered_mirror.financial_year }} · v{{ resources.registered_mirror.version_no }}{% if resources.registered_mirror.company_name %} · {{ resources.registered_mirror.company_name }}{% endif %}
+ {% endif %}
- {% if resources.storage_node_name %}{{ resources.storage_node_name }}{% endif %} -
- -
- {% for year_row in resources.accounting_years %} -
-
-
FY {{ year_row.financial_year }}{% if year_row.is_engagement_year %} · engagement year{% else %} · previous year{% endif %}
-
- Tally {{ 'Available' if year_row.tally and year_row.tally.local_path else 'Not available' }} - Mirror {{ 'Linked' if year_row.mirror and year_row.mirror.available else 'Not linked' }} -
-
- -
-
-
Tally Data Storage Path
- {% if year_row.tally and year_row.tally.local_path %} -
- - -
-

Paste this storage/extracted-data path into TallyPrime when working with this FY.

- {% if year_row.tally.latest_filename %}
{{ year_row.tally.latest_filename }} · v{{ year_row.tally.version_no }}
{% endif %} - {% else %} -

No usable Tally storage path is registered for this client/FY yet.

- {% endif %} -
- -
-
Accounting Mirror (.act)
- {% if year_row.mirror and year_row.mirror.available %} - {% if year_row.mirror.local_path %} -
- - -
- {% elif year_row.mirror.relative_path %} -
{{ year_row.mirror.relative_path }}
- {% endif %} -
{% 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 %}
- {% else %} -

No active Accounting Mirror is linked to this client/FY.

- {% endif %} -
-
+
+ {% if resources.registered_mirror %}Mirror Available{% endif %} + {% if resources.storage_node_name %}{{ resources.storage_node_name }}{% endif %}
- {% else %} -
Financial-year accounting resources are unavailable for this engagement.
- {% endfor %}
+ {% if resources.accounting_local_path %} +
+ + +
+

This is the existing .act accounting/intelligence database path used by the Local Agent. Uploaded Tally data paths are shown separately on the right.

+ {% elif resources.accounting_relative_path %} +
{{ resources.accounting_relative_path }}
+

The relative accounting path is known, but this branch does not currently expose a configured local storage root.

+ {% else %} +

Accounting path is unavailable for this engagement.

+ {% endif %} + + {% if resources.registered_mirror and resources.registered_mirror.tally_data_local_path %} +
+
Tally Working Data Path
+
+ + +
+

This path is stored with the Client + FY mirror metadata and is reused by the engagement workspace.

+
+ {% endif %}