Link Accounting Mirror metadata and Tally paths to Client FY
This commit is contained in:
@@ -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="")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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">
|
||||
{% 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.
|
||||
<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 %}
|
||||
<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 %}
|
||||
</div>
|
||||
<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.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(
|
||||
|
||||
Reference in New Issue
Block a user