Link Accounting Mirror metadata and Tally paths to Client FY
This commit is contained in:
@@ -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")
|
||||||
@@ -34,6 +34,9 @@ class AccountingMirrorRegistry(CommonBase):
|
|||||||
financial_year: Mapped[str] = mapped_column(String(7), nullable=False, index=True)
|
financial_year: Mapped[str] = mapped_column(String(7), nullable=False, index=True)
|
||||||
accounting_relative_dir: Mapped[str] = mapped_column(String(1000), nullable=False)
|
accounting_relative_dir: Mapped[str] = mapped_column(String(1000), nullable=False)
|
||||||
mirror_file_name: Mapped[str] = mapped_column(String(255), 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_name: Mapped[str] = mapped_column(String(255), nullable=False, default="")
|
||||||
company_guid: Mapped[str] = mapped_column(String(160), nullable=False, default="")
|
company_guid: Mapped[str] = mapped_column(String(160), nullable=False, default="")
|
||||||
|
|||||||
@@ -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)
|
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]:
|
def list_accounting_mirror_versions(db, tenant_id: int, client_id: int, financial_year: str) -> list[AccountingMirrorRegistry]:
|
||||||
rows = db.execute(
|
rows = db.execute(
|
||||||
select(AccountingMirrorRegistry).where(
|
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.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.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
|
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_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.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
|
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_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.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)
|
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()
|
current.updated_at_utc = _mirror_utcnow()
|
||||||
db.flush()
|
db.flush()
|
||||||
return current
|
return current
|
||||||
|
|||||||
@@ -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.settings import get_settings
|
||||||
from app.core.templating import templates
|
from app.core.templating import templates
|
||||||
from app.modules.accounting.agent_bridge import request_agent_command
|
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.core.rbac.deps import get_user_permissions, get_user_roles
|
||||||
from app.modules.credential_vault.crypto import decrypt_value
|
from app.modules.credential_vault.crypto import decrypt_value
|
||||||
from app.modules.credential_vault.models import CredentialVaultEntry
|
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")
|
fy_folder = sanitize_segment(f"FY{fy}", "FY")
|
||||||
letter, client_folder = client_folder_parts(client, int(client.id))
|
letter, client_folder = client_folder_parts(client, int(client.id))
|
||||||
root = Path(fy_folder) / "Clients" / letter / client_folder
|
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):
|
def _redirect(client_id: int, **params):
|
||||||
|
|||||||
@@ -293,8 +293,16 @@
|
|||||||
<div id="mirrorRegistryStatus" class="mt-3 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-700">
|
<div id="mirrorRegistryStatus" class="mt-3 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-700">
|
||||||
{% if selected_mirror_registration %}
|
{% if selected_mirror_registration %}
|
||||||
<strong>Mirror available:</strong> 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.
|
<strong>Mirror available:</strong> 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.
|
||||||
|
<div class="mt-3 grid gap-2 lg:grid-cols-2">
|
||||||
|
{% if selected_mirror_registration.mirror_local_path %}
|
||||||
|
<div class="rounded-lg border border-slate-200 bg-white p-2.5"><div class="text-[11px] font-semibold uppercase text-slate-500">Registered Mirror Path</div><div class="mt-1 break-all font-mono text-[11px] text-slate-700">{{ selected_mirror_registration.mirror_local_path }}</div></div>
|
||||||
|
{% endif %}
|
||||||
|
{% if selected_mirror_registration.tally_data_local_path %}
|
||||||
|
<div class="rounded-lg border border-slate-200 bg-white p-2.5"><div class="text-[11px] font-semibold uppercase text-slate-500">Tally Working Data Path</div><div class="mt-1 break-all font-mono text-[11px] text-slate-700">{{ selected_mirror_registration.tally_data_local_path }}</div></div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<strong>No registered mirror for FY {{ selected_financial_year }}.</strong> Creating the mirror will register it to this client and financial year for Accounting/Audit tools.
|
<strong>No registered mirror for FY {{ selected_financial_year }}.</strong> 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<form method="post" action="/tools/tally/full-export" class="mt-5 grid gap-4 lg:grid-cols-5 lg:items-end" id="mirrorExportForm">
|
<form method="post" action="/tools/tally/full-export" class="mt-5 grid gap-4 lg:grid-cols-5 lg:items-end" id="mirrorExportForm">
|
||||||
|
|||||||
@@ -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.agent_bridge import request_agent_command
|
||||||
from app.modules.accounting.accounting_mirror_service import (
|
from app.modules.accounting.accounting_mirror_service import (
|
||||||
get_registered_mirror,
|
get_registered_mirror,
|
||||||
|
get_current_accounting_mirror,
|
||||||
|
registered_mirror_payload,
|
||||||
list_registered_mirrors,
|
list_registered_mirrors,
|
||||||
list_accounting_mirror_versions,
|
list_accounting_mirror_versions,
|
||||||
next_accounting_mirror_version,
|
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:
|
def _accounting_storage_payload(client, financial_year: str | None = None, *, db=None, tenant_id: int | None = None) -> dict:
|
||||||
"""Use the same FY + client-folder policy as existing Engagement storage."""
|
"""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_value = str(financial_year or _financial_year_for_date()).strip()
|
||||||
fy_folder = sanitize_segment(f"FY{fy_value}", "FY")
|
fy_folder = sanitize_segment(f"FY{fy_value}", "FY")
|
||||||
letter, client_folder = client_folder_parts(client, int(client.id))
|
letter, client_folder = client_folder_parts(client, int(client.id))
|
||||||
relative_dir = Path(fy_folder) / "Clients" / letter / client_folder / "Accounting"
|
relative_dir = Path(fy_folder) / "Clients" / letter / client_folder / "Accounting"
|
||||||
return {
|
fallback = {
|
||||||
"client_id": int(client.id),
|
"client_id": int(client.id),
|
||||||
"client_name": str(client.client_name or "").strip(),
|
"client_name": str(client.client_name or "").strip(),
|
||||||
"client_code": str(getattr(client, "client_code", "") or "").strip(),
|
"client_code": str(getattr(client, "client_code", "") or "").strip(),
|
||||||
"financial_year": fy_value,
|
"financial_year": fy_value,
|
||||||
"accounting_relative_dir": relative_dir.as_posix(),
|
"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):
|
def _client_registrations(db, client, tenant_id: int):
|
||||||
rows = db.execute(
|
rows = db.execute(
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ from typing import Any, Iterable
|
|||||||
|
|
||||||
from sqlalchemy import select
|
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.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.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:
|
def _join_local_path(root: str, relative: str) -> str:
|
||||||
root = (root or "").strip().rstrip("\\/")
|
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:
|
if not root:
|
||||||
return ""
|
return ""
|
||||||
if re.match(r"^[A-Za-z]:", root) or "\\" in root:
|
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 ""
|
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.
|
||||||
|
|
||||||
@@ -274,13 +206,12 @@ 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 = ""
|
||||||
|
registered_mirror = None
|
||||||
tenant_id = int(getattr(engagement, "tenant_id", 0) or 0)
|
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)
|
||||||
relative_dir = Path(fy_folder) / "Clients" / letter / client_folder / "Accounting"
|
fallback_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()
|
|
||||||
node = get_active_storage_node_for_branch(
|
node = get_active_storage_node_for_branch(
|
||||||
db,
|
db,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
@@ -289,37 +220,20 @@ def build_engagement_resource_context(db, engagement, tasks: Iterable[Any], docu
|
|||||||
if node:
|
if node:
|
||||||
storage_root_path = str(getattr(node, "storage_root_path", "") or "").strip()
|
storage_root_path = str(getattr(node, "storage_root_path", "") or "").strip()
|
||||||
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)
|
|
||||||
|
|
||||||
# Client/FY accounting resources are shared across engagements for the same
|
registered_mirror = get_current_accounting_mirror(db, tenant_id, client_id, fy)
|
||||||
# client. This lets Tax Audit, Statutory Audit, ITR, etc. reuse the same Tally
|
if registered_mirror:
|
||||||
# dataset and registered mirror instead of requiring duplicate uploads.
|
accounting_relative_dir = str(registered_mirror.accounting_relative_dir or fallback_dir.as_posix()).strip()
|
||||||
client_tally_documents: list[Any] = []
|
mirror_name = str(registered_mirror.mirror_file_name or f"client_{client_id:08d}.act").strip()
|
||||||
if tenant_id and client_id and tally_year_options:
|
accounting_relative_path = (Path(accounting_relative_dir) / mirror_name).as_posix()
|
||||||
client_tally_documents = db.execute(
|
accounting_local_path = str(getattr(registered_mirror, "mirror_local_path", "") or "").strip()
|
||||||
select(EngagementDocument).where(
|
if not accounting_local_path and storage_root_path:
|
||||||
EngagementDocument.tenant_id == tenant_id,
|
accounting_local_path = _join_local_path(storage_root_path, accounting_relative_path)
|
||||||
EngagementDocument.client_id == client_id,
|
else:
|
||||||
EngagementDocument.is_deleted.is_(False),
|
accounting_relative_dir = fallback_dir.as_posix()
|
||||||
EngagementDocument.description.like(f"{_TALLY_MARKER_PREFIX}%"),
|
accounting_relative_path = (fallback_dir / f"client_{client_id:08d}.act").as_posix()
|
||||||
EngagementDocument.financial_year.in_(tally_year_options),
|
if storage_root_path:
|
||||||
).order_by(EngagementDocument.updated_at_utc.desc(), EngagementDocument.id.desc())
|
accounting_local_path = _join_local_path(storage_root_path, accounting_relative_path)
|
||||||
).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)
|
||||||
@@ -348,23 +262,70 @@ 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": 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)
|
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 {
|
return {
|
||||||
"accounting_relative_dir": accounting_relative_dir,
|
"accounting_relative_dir": accounting_relative_dir,
|
||||||
"accounting_relative_path": accounting_relative_path,
|
"accounting_relative_path": accounting_relative_path,
|
||||||
"accounting_local_path": accounting_local_path,
|
"accounting_local_path": accounting_local_path,
|
||||||
"storage_root_path": storage_root_path,
|
"storage_root_path": storage_root_path,
|
||||||
"storage_node_name": storage_node_name,
|
"storage_node_name": storage_node_name,
|
||||||
|
"registered_mirror": registered_mirror_info,
|
||||||
"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,61 +15,40 @@
|
|||||||
<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">Client / FY Accounting Data</div>
|
<div class="text-xs font-semibold uppercase tracking-wide text-indigo-600">Accounting Intelligence</div>
|
||||||
<div class="mt-1 font-semibold text-indigo-950">Tally data and Accounting Mirror</div>
|
<div class="mt-1 font-semibold text-indigo-950">Client accounting .act path</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>
|
{% if resources.registered_mirror %}
|
||||||
|
<div class="mt-1 text-xs text-indigo-700">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 %}</div>
|
||||||
|
{% endif %}
|
||||||
</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 %}
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
</div>
|
{% if resources.registered_mirror %}<span class="rounded-full bg-emerald-100 px-2.5 py-1 text-xs font-semibold text-emerald-700">Mirror Available</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 class="mt-4 space-y-3">
|
|
||||||
{% for year_row in resources.accounting_years %}
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="mt-3 grid gap-3 xl:grid-cols-2">
|
|
||||||
<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 %}
|
|
||||||
<p class="mt-2 text-xs text-amber-700">No usable Tally storage path is registered for this client/FY yet.</p>
|
|
||||||
{% endif %}
|
|
||||||
</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>
|
</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>
|
||||||
|
{% if resources.accounting_local_path %}
|
||||||
|
<div class="mt-3 flex gap-2">
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
{% 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>
|
||||||
|
<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>
|
||||||
|
{% else %}
|
||||||
|
<p class="mt-3 text-sm text-slate-600">Accounting path is unavailable for this engagement.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if resources.registered_mirror and resources.registered_mirror.tally_data_local_path %}
|
||||||
|
<div class="mt-4 border-t border-indigo-200 pt-3">
|
||||||
|
<div class="text-xs font-semibold uppercase tracking-wide text-indigo-600">Tally Working Data Path</div>
|
||||||
|
<div class="mt-2 flex gap-2">
|
||||||
|
<input id="engagement-tally-working-path" readonly value="{{ resources.registered_mirror.tally_data_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">
|
||||||
|
<button type="button" data-copy-target="engagement-tally-working-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>
|
||||||
|
<p class="mt-2 text-xs text-indigo-700">This path is stored with the Client + FY mirror metadata and is reused by the engagement workspace.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user