Prepare ERP source for Gitea deployment
This commit is contained in:
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user