Remove bank analyzer document service circular import

This commit is contained in:
A R R R Associates
2026-07-13 13:46:45 +05:30
parent b45538a496
commit 31f7c8fd3e
+60 -16
View File
@@ -11,8 +11,6 @@ from typing import Iterable
from fastapi import UploadFile from fastapi import UploadFile
from app.modules.documents.services import DEFAULT_STORAGE_ROOT
from .analyzer import analyze_files, export_excel from .analyzer import analyze_files, export_excel
ALLOWED_ROLES = {"Partner", "Manager", "Branch Manager", "Staff", "Employee", "Consultant"} ALLOWED_ROLES = {"Partner", "Manager", "Branch Manager", "Staff", "Employee", "Consultant"}
@@ -22,18 +20,34 @@ RETENTION_HOURS = int(os.getenv("BANK_ANALYZER_FAILED_RETENTION_HOURS", "24"))
_SAFE = re.compile(r"[^A-Za-z0-9._-]+") _SAFE = re.compile(r"[^A-Za-z0-9._-]+")
def _resolve_document_storage_root() -> Path:
"""Resolve the same automatic document root without importing documents.services.
Importing app.modules.documents.services here creates a circular import through
clients.__init__ -> clients.ui -> documents.services. This local resolver
intentionally mirrors the existing document module's storage-root rules while
keeping the bank analyzer independent of document models and UI modules.
"""
configured = (os.getenv("DOCUMENT_STORAGE_ROOT") or "").strip()
if configured:
configured_path = Path(configured).expanduser()
if configured_path.is_absolute():
return configured_path
return (Path.cwd() / configured_path).resolve()
if os.name == "nt":
anchor = Path.cwd().anchor or (os.environ.get("SystemDrive", "C:") + "\\")
return Path(anchor) / "AuditFirmERPDocuments" / "engagement_documents"
return (Path.cwd() / "documents" / "engagement_documents").resolve()
def _now() -> datetime: def _now() -> datetime:
return datetime.now(timezone.utc) return datetime.now(timezone.utc)
def _root() -> Path: def _root() -> Path:
"""Return the automatic ERP work-storage root. return _resolve_document_storage_root().parent / "Work"
The document module already resolves its storage location for the current
deployment. Bank-statement jobs use a sibling ``Work`` folder so no new
environment variable or separate path configuration is required.
"""
return DEFAULT_STORAGE_ROOT.parent / "Work"
def _segment(value: object, default: str = "NA") -> str: def _segment(value: object, default: str = "NA") -> str:
@@ -92,7 +106,7 @@ def _validate_pdf_header(data: bytes) -> None:
async def save_uploads(files: list[UploadFile], input_dir: Path) -> list[Path]: async def save_uploads(files: list[UploadFile], input_dir: Path) -> list[Path]:
usable = [f for f in files if f and (f.filename or "").strip()] usable = [file for file in files if file and (file.filename or "").strip()]
if not usable: if not usable:
raise ValueError("Please select at least one PDF bank statement.") raise ValueError("Please select at least one PDF bank statement.")
if len(usable) > MAX_FILES: if len(usable) > MAX_FILES:
@@ -122,10 +136,36 @@ async def save_uploads(files: list[UploadFile], input_dir: Path) -> list[Path]:
return saved return saved
def analyze_job(*, user, roles: Iterable[str], job_id: str, paths: list[Path], output_dir: Path, customer_override: str = "", account_override: str = "") -> dict: def analyze_job(
metas, all_df, unique_df = analyze_files(paths, customer_override.strip(), account_override.strip()) *,
user,
roles: Iterable[str],
job_id: str,
paths: list[Path],
output_dir: Path,
customer_override: str = "",
account_override: str = "",
bank_selection: str = "auto",
financial_year: str = "",
classification_enabled: bool = True,
) -> dict:
metas, all_df, unique_df = analyze_files(
paths,
customer_override.strip(),
account_override.strip(),
bank_hint=bank_selection,
classification_enabled=classification_enabled,
)
output = output_dir / "Bank_Statement_Analysis.xlsx" output = output_dir / "Bank_Statement_Analysis.xlsx"
export_excel(output, metas, all_df, unique_df) export_excel(
output,
metas,
all_df,
unique_df,
financial_year=financial_year.strip(),
selected_bank=bank_selection,
classification_enabled=classification_enabled,
)
summary = { summary = {
"job_id": job_id, "job_id": job_id,
"owner_user_id": int(user.id), "owner_user_id": int(user.id),
@@ -135,9 +175,13 @@ def analyze_job(*, user, roles: Iterable[str], job_id: str, paths: list[Path], o
"unique_transactions": int(len(unique_df)), "unique_transactions": int(len(unique_df)),
"exact_duplicate_rows": int(all_df.exact_duplicate.sum()) if not all_df.empty else 0, "exact_duplicate_rows": int(all_df.exact_duplicate.sum()) if not all_df.empty else 0,
"possible_duplicate_rows": int(all_df.possible_duplicate.sum()) if not all_df.empty else 0, "possible_duplicate_rows": int(all_df.possible_duplicate.sum()) if not all_df.empty else 0,
"banks": sorted({m.bank_name for m in metas}), "review_items": int(unique_df.review_note.fillna("").ne("").sum()) if not unique_df.empty else 0,
"customer_name": next((m.customer_name for m in metas if m.customer_name), ""), "categories": int(unique_df.category.nunique()) if not unique_df.empty else 0,
"account_number": next((m.account_number for m in metas if m.account_number), ""), "banks": sorted({meta.bank_name for meta in metas}),
"customer_name": next((meta.customer_name for meta in metas if meta.customer_name), ""),
"account_number": next((meta.account_number for meta in metas if meta.account_number), ""),
"financial_year": financial_year.strip(),
"classification_enabled": classification_enabled,
"output_file": output.name, "output_file": output.name,
} }
(output_dir.parent / "job.json").write_text(json.dumps(summary, indent=2), encoding="utf-8") (output_dir.parent / "job.json").write_text(json.dumps(summary, indent=2), encoding="utf-8")