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 @@