Prepare ERP source for Gitea deployment

This commit is contained in:
A R R R Associates
2026-06-20 15:01:44 +05:30
commit 5c75eb6bd9
450 changed files with 67698 additions and 0 deletions
+244
View File
@@ -0,0 +1,244 @@
from __future__ import annotations
from collections import Counter
from datetime import date
from typing import Any
from sqlalchemy import select, func
from sqlalchemy.orm import Session, selectinload
from app.modules.documents.models import EngagementDocument, PermanentClientDocument
from app.modules.services.models import (
ClientServiceSubscription,
ClientServiceTaskInstance,
ServiceTaskComment,
)
OPEN_TASK_STATUSES = {"pending", "in_progress", "blocked", "ready_for_review", "rework"}
CLOSED_TASK_STATUSES = {"completed", "approved", "closed", "not_applicable"}
def _client_id(client_row: dict[str, Any]) -> int:
return int(client_row.get("id") or 0)
def _tenant_id(client_row: dict[str, Any]) -> int:
return int(client_row.get("tenant_id") or 0)
def list_client_engagements(db: Session, client_row: dict[str, Any], *, limit: int = 200, financial_year: str | None = None) -> list[ClientServiceSubscription]:
"""Return engagements/subscriptions visible to the logged-in client."""
query = (
select(ClientServiceSubscription)
.options(
selectinload(ClientServiceSubscription.catalogue),
selectinload(ClientServiceSubscription.assigned_partner),
selectinload(ClientServiceSubscription.assigned_manager),
selectinload(ClientServiceSubscription.assigned_staff),
)
.where(
ClientServiceSubscription.tenant_id == _tenant_id(client_row),
ClientServiceSubscription.client_id == _client_id(client_row),
ClientServiceSubscription.is_active.is_(True),
)
)
if financial_year:
query = query.where(ClientServiceSubscription.financial_year == financial_year.strip())
rows = db.execute(
query.order_by(
ClientServiceSubscription.current_due_date.asc().nulls_last(),
ClientServiceSubscription.updated_at_utc.desc(),
)
.limit(max(1, min(int(limit or 200), 500)))
).scalars().all()
return rows
def list_client_tasks_for_engagement(db: Session, client_row: dict[str, Any], engagement_id: int) -> list[ClientServiceTaskInstance]:
return db.execute(
select(ClientServiceTaskInstance)
.options(
selectinload(ClientServiceTaskInstance.catalogue),
selectinload(ClientServiceTaskInstance.assigned_to),
selectinload(ClientServiceTaskInstance.comments).selectinload(ServiceTaskComment.created_by),
)
.where(
ClientServiceTaskInstance.tenant_id == _tenant_id(client_row),
ClientServiceTaskInstance.client_id == _client_id(client_row),
ClientServiceTaskInstance.subscription_id == int(engagement_id),
ClientServiceTaskInstance.is_active.is_(True),
)
.order_by(ClientServiceTaskInstance.sequence_no.asc(), ClientServiceTaskInstance.id.asc())
).scalars().all()
def get_client_engagement(db: Session, client_row: dict[str, Any], engagement_id: int, *, financial_year: str | None = None) -> ClientServiceSubscription | None:
query = (
select(ClientServiceSubscription)
.options(
selectinload(ClientServiceSubscription.catalogue),
selectinload(ClientServiceSubscription.assigned_partner),
selectinload(ClientServiceSubscription.assigned_manager),
selectinload(ClientServiceSubscription.assigned_staff),
)
.where(
ClientServiceSubscription.id == int(engagement_id),
ClientServiceSubscription.tenant_id == _tenant_id(client_row),
ClientServiceSubscription.client_id == _client_id(client_row),
ClientServiceSubscription.is_active.is_(True),
)
)
if financial_year:
query = query.where(ClientServiceSubscription.financial_year == financial_year.strip())
return db.execute(query).scalar_one_or_none()
def get_client_task(db: Session, client_row: dict[str, Any], task_id: int, *, financial_year: str | None = None) -> ClientServiceTaskInstance | None:
query = (
select(ClientServiceTaskInstance)
.where(
ClientServiceTaskInstance.id == int(task_id),
ClientServiceTaskInstance.tenant_id == _tenant_id(client_row),
ClientServiceTaskInstance.client_id == _client_id(client_row),
ClientServiceTaskInstance.is_active.is_(True),
)
)
if financial_year:
query = query.where(ClientServiceTaskInstance.financial_year == financial_year.strip())
return db.execute(query).scalar_one_or_none()
def list_client_visible_comments(db: Session, client_row: dict[str, Any], *, limit: int = 100, financial_year: str | None = None) -> list[ServiceTaskComment]:
query = (
select(ServiceTaskComment)
.options(
selectinload(ServiceTaskComment.created_by),
selectinload(ServiceTaskComment.task).selectinload(ClientServiceTaskInstance.catalogue),
selectinload(ServiceTaskComment.subscription).selectinload(ClientServiceSubscription.catalogue),
)
.join(ClientServiceTaskInstance, ClientServiceTaskInstance.id == ServiceTaskComment.task_instance_id)
.where(
ServiceTaskComment.tenant_id == _tenant_id(client_row),
ServiceTaskComment.visibility == "client",
ServiceTaskComment.is_deleted.is_(False),
ClientServiceTaskInstance.client_id == _client_id(client_row),
ClientServiceTaskInstance.tenant_id == _tenant_id(client_row),
ClientServiceTaskInstance.is_active.is_(True),
)
)
if financial_year:
query = query.where(ClientServiceTaskInstance.financial_year == financial_year.strip())
return db.execute(
query.order_by(ServiceTaskComment.created_at_utc.desc(), ServiceTaskComment.id.desc())
.limit(max(1, min(int(limit or 100), 300)))
).scalars().all()
def create_client_reply(db: Session, *, client_row: dict[str, Any], task: ClientServiceTaskInstance, message: str, user) -> ServiceTaskComment:
clean_message = (message or "").strip()
if not clean_message:
raise ValueError("Reply message is required.")
if len(clean_message) > 4000:
raise ValueError("Reply message is too long. Please keep it within 4000 characters.")
comment = ServiceTaskComment(
tenant_id=task.tenant_id,
branch_id=task.branch_id,
subscription_id=task.subscription_id,
task_instance_id=task.id,
comment_type="client_clarification",
visibility="client",
message=clean_message,
created_by_user_id=getattr(user, "id", None),
)
db.add(comment)
db.flush()
return comment
def list_client_engagement_documents(db: Session, client_row: dict[str, Any], *, engagement_id: int | None = None, financial_year: str | None = None) -> list[EngagementDocument]:
stmt = (
select(EngagementDocument)
.options(selectinload(EngagementDocument.versions), selectinload(EngagementDocument.engagement).selectinload(ClientServiceSubscription.catalogue))
.where(
EngagementDocument.tenant_id == _tenant_id(client_row),
EngagementDocument.client_id == _client_id(client_row),
EngagementDocument.is_deleted.is_(False),
)
)
if engagement_id is not None:
stmt = stmt.where(EngagementDocument.engagement_id == int(engagement_id))
if financial_year:
stmt = stmt.where(EngagementDocument.financial_year == financial_year.strip())
return db.execute(stmt.order_by(EngagementDocument.updated_at_utc.desc())).unique().scalars().all()
def list_client_permanent_documents(db: Session, client_row: dict[str, Any]) -> list[PermanentClientDocument]:
return db.execute(
select(PermanentClientDocument)
.options(selectinload(PermanentClientDocument.versions))
.where(
PermanentClientDocument.tenant_id == _tenant_id(client_row),
PermanentClientDocument.client_id == _client_id(client_row),
PermanentClientDocument.is_deleted.is_(False),
)
.order_by(PermanentClientDocument.category.asc(), PermanentClientDocument.updated_at_utc.desc())
).unique().scalars().all()
def build_client_portal_summary(db: Session, client_row: dict[str, Any], *, financial_year: str | None = None) -> dict[str, Any]:
engagements = list_client_engagements(db, client_row, limit=500, financial_year=financial_year)
engagement_ids = [row.id for row in engagements]
today = date.today()
task_rows: list[ClientServiceTaskInstance] = []
if engagement_ids:
task_rows = db.execute(
select(ClientServiceTaskInstance).where(
ClientServiceTaskInstance.tenant_id == _tenant_id(client_row),
ClientServiceTaskInstance.client_id == _client_id(client_row),
ClientServiceTaskInstance.subscription_id.in_(engagement_ids),
ClientServiceTaskInstance.is_active.is_(True),
)
).scalars().all()
status_counter = Counter((task.status or "pending") for task in task_rows)
open_tasks = [task for task in task_rows if (task.status or "pending") in OPEN_TASK_STATUSES]
overdue_tasks = [
task for task in open_tasks
if task.internal_target_date is not None and task.internal_target_date < today
]
due_soon_engagements = [
row for row in engagements
if row.current_due_date is not None and row.current_due_date >= today
][:10]
pending_from_client = 0
with_firm = 0
completed = 0
clarification_required = 0
for row in engagements:
tasks_for_eng = [t for t in task_rows if t.subscription_id == row.id]
statuses = {(t.status or "pending") for t in tasks_for_eng}
if statuses & {"blocked", "client_pending", "clarification_required"}:
clarification_required += 1
elif tasks_for_eng and all((t.status or "pending") in CLOSED_TASK_STATUSES for t in tasks_for_eng):
completed += 1
elif statuses & {"pending"}:
pending_from_client += 1
else:
with_firm += 1
return {
"engagements": engagements,
"task_rows": task_rows,
"status_counter": status_counter,
"total_engagements": len(engagements),
"open_tasks": len(open_tasks),
"overdue_tasks": len(overdue_tasks),
"completed_tasks": status_counter.get("completed", 0) + status_counter.get("approved", 0) + status_counter.get("closed", 0),
"due_soon_engagements": due_soon_engagements,
"pending_from_client": pending_from_client,
"with_firm": with_firm,
"clarification_required": clarification_required,
"completed_engagements": completed,
}