Prepare ERP source for Gitea deployment
This commit is contained in:
@@ -0,0 +1,609 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime, timezone
|
||||
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.orm import Session, selectinload
|
||||
|
||||
from app.modules.clients.models import Client
|
||||
from app.modules.core.iam.models import User
|
||||
from app.modules.services.models import (
|
||||
ClientServiceSubscription,
|
||||
ClientServiceTaskInstance,
|
||||
ServiceTaskComment,
|
||||
FirmServiceTaskTemplate,
|
||||
ServiceCatalogue,
|
||||
)
|
||||
|
||||
TASK_STATUSES = [
|
||||
("pending", "Pending"),
|
||||
("in_progress", "In Progress"),
|
||||
("completed", "Completed"),
|
||||
("blocked", "Blocked"),
|
||||
("not_applicable", "Not Applicable"),
|
||||
("cancelled", "Cancelled"),
|
||||
]
|
||||
|
||||
OPEN_TASK_STATUSES = {"pending", "in_progress", "blocked"}
|
||||
CLOSED_TASK_STATUSES = {"completed", "not_applicable", "cancelled"}
|
||||
|
||||
|
||||
TASK_COMMENT_TYPES = [
|
||||
("internal_note", "Internal Note"),
|
||||
("client_clarification", "Client Clarification"),
|
||||
("consultant_clarification", "Consultant Clarification"),
|
||||
("partner_review_note", "Partner Review Note"),
|
||||
]
|
||||
|
||||
TASK_COMMENT_VISIBILITIES = [
|
||||
("internal", "Internal"),
|
||||
("client", "Client"),
|
||||
("consultant", "Consultant"),
|
||||
]
|
||||
|
||||
TASK_PRIORITIES = [
|
||||
("low", "Low"),
|
||||
("normal", "Normal"),
|
||||
("high", "High"),
|
||||
("urgent", "Urgent"),
|
||||
]
|
||||
|
||||
|
||||
def _normalise_status(status: str | None) -> str:
|
||||
allowed = {code for code, _label in TASK_STATUSES}
|
||||
value = (status or "pending").strip().lower()
|
||||
return value if value in allowed else "pending"
|
||||
|
||||
|
||||
def _normalise_priority(priority: str | None) -> str:
|
||||
allowed = {code for code, _label in TASK_PRIORITIES}
|
||||
value = (priority or "normal").strip().lower()
|
||||
return value if value in allowed else "normal"
|
||||
|
||||
|
||||
def _normalise_comment_type(comment_type: str | None) -> str:
|
||||
allowed = {code for code, _label in TASK_COMMENT_TYPES}
|
||||
value = (comment_type or "internal_note").strip().lower()
|
||||
return value if value in allowed else "internal_note"
|
||||
|
||||
|
||||
def _normalise_visibility(visibility: str | None) -> str:
|
||||
allowed = {code for code, _label in TASK_COMMENT_VISIBILITIES}
|
||||
value = (visibility or "internal").strip().lower()
|
||||
return value if value in allowed else "internal"
|
||||
|
||||
|
||||
def parse_date_value(value: str | None) -> date | None:
|
||||
text = (value or "").strip()
|
||||
if not text:
|
||||
return None
|
||||
return date.fromisoformat(text)
|
||||
|
||||
|
||||
def _default_assignee_for_template(subscription: ClientServiceSubscription, template: FirmServiceTaskTemplate) -> int | None:
|
||||
role = (template.default_role_name or "").strip().lower()
|
||||
if "partner" in role:
|
||||
return subscription.assigned_partner_user_id
|
||||
if "manager" in role:
|
||||
return subscription.assigned_manager_user_id
|
||||
if "staff" in role or "employee" in role:
|
||||
return subscription.assigned_staff_user_id
|
||||
return subscription.assigned_staff_user_id or subscription.assigned_manager_user_id or subscription.assigned_partner_user_id
|
||||
|
||||
|
||||
def get_subscription_for_execution(db: Session, *, tenant_id: int, subscription_id: int) -> ClientServiceSubscription | None:
|
||||
return db.execute(
|
||||
select(ClientServiceSubscription)
|
||||
.options(
|
||||
selectinload(ClientServiceSubscription.client),
|
||||
selectinload(ClientServiceSubscription.catalogue),
|
||||
selectinload(ClientServiceSubscription.assigned_partner),
|
||||
selectinload(ClientServiceSubscription.assigned_manager),
|
||||
selectinload(ClientServiceSubscription.assigned_staff),
|
||||
)
|
||||
.where(
|
||||
ClientServiceSubscription.id == subscription_id,
|
||||
ClientServiceSubscription.tenant_id == tenant_id,
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
|
||||
|
||||
def list_subscription_execution_payload(db: Session, *, tenant_id: int, branch_id: int | None = None, financial_year: str | None = None, q: str = ""):
|
||||
query = (
|
||||
select(ClientServiceSubscription)
|
||||
.options(
|
||||
selectinload(ClientServiceSubscription.client),
|
||||
selectinload(ClientServiceSubscription.catalogue),
|
||||
selectinload(ClientServiceSubscription.assigned_partner),
|
||||
selectinload(ClientServiceSubscription.assigned_manager),
|
||||
selectinload(ClientServiceSubscription.assigned_staff),
|
||||
)
|
||||
.where(
|
||||
ClientServiceSubscription.tenant_id == tenant_id,
|
||||
ClientServiceSubscription.is_active.is_(True),
|
||||
ClientServiceSubscription.status == "active",
|
||||
ClientServiceSubscription.is_locked.is_(False),
|
||||
)
|
||||
)
|
||||
if financial_year:
|
||||
query = query.where(ClientServiceSubscription.financial_year == financial_year.strip())
|
||||
if branch_id:
|
||||
query = query.where(ClientServiceSubscription.branch_id == branch_id)
|
||||
if q.strip():
|
||||
term = f"%{q.strip()}%"
|
||||
query = (
|
||||
query.join(Client, Client.id == ClientServiceSubscription.client_id)
|
||||
.join(ServiceCatalogue, ServiceCatalogue.id == ClientServiceSubscription.service_catalogue_id)
|
||||
.where(
|
||||
or_(
|
||||
Client.client_name.ilike(term),
|
||||
Client.client_code.ilike(term),
|
||||
ServiceCatalogue.service_code.ilike(term),
|
||||
ServiceCatalogue.service_name.ilike(term),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
rows = db.execute(query.order_by(ClientServiceSubscription.id.desc())).scalars().all()
|
||||
payload = []
|
||||
for sub in rows:
|
||||
total = db.execute(
|
||||
select(func.count(ClientServiceTaskInstance.id)).where(
|
||||
ClientServiceTaskInstance.subscription_id == sub.id,
|
||||
ClientServiceTaskInstance.is_active.is_(True),
|
||||
)
|
||||
).scalar_one()
|
||||
completed = db.execute(
|
||||
select(func.count(ClientServiceTaskInstance.id)).where(
|
||||
ClientServiceTaskInstance.subscription_id == sub.id,
|
||||
ClientServiceTaskInstance.is_active.is_(True),
|
||||
ClientServiceTaskInstance.status == "completed",
|
||||
)
|
||||
).scalar_one()
|
||||
open_tasks = db.execute(
|
||||
select(func.count(ClientServiceTaskInstance.id)).where(
|
||||
ClientServiceTaskInstance.subscription_id == sub.id,
|
||||
ClientServiceTaskInstance.is_active.is_(True),
|
||||
ClientServiceTaskInstance.status.in_(list(OPEN_TASK_STATUSES)),
|
||||
)
|
||||
).scalar_one()
|
||||
payload.append({"subscription": sub, "total_tasks": total, "completed_tasks": completed, "open_tasks": open_tasks})
|
||||
return payload
|
||||
|
||||
|
||||
def _default_internal_target_date(subscription: ClientServiceSubscription, template: FirmServiceTaskTemplate) -> date | None:
|
||||
# Phase 4B keeps task target dates internal. Existing task templates do not yet have
|
||||
# an offset field, so new generated tasks start blank and can be assigned through the tracker.
|
||||
return None
|
||||
|
||||
|
||||
def generate_tasks_for_subscription(db: Session, *, subscription: ClientServiceSubscription, user_id: int) -> int:
|
||||
templates = db.execute(
|
||||
select(FirmServiceTaskTemplate)
|
||||
.where(
|
||||
FirmServiceTaskTemplate.tenant_id == subscription.tenant_id,
|
||||
FirmServiceTaskTemplate.service_catalogue_id == subscription.service_catalogue_id,
|
||||
FirmServiceTaskTemplate.is_active.is_(True),
|
||||
)
|
||||
.order_by(FirmServiceTaskTemplate.sequence_no.asc(), FirmServiceTaskTemplate.id.asc())
|
||||
).scalars().all()
|
||||
|
||||
created = 0
|
||||
for template in templates:
|
||||
existing = db.execute(
|
||||
select(ClientServiceTaskInstance.id).where(
|
||||
ClientServiceTaskInstance.subscription_id == subscription.id,
|
||||
ClientServiceTaskInstance.firm_task_template_id == template.id,
|
||||
ClientServiceTaskInstance.financial_year == subscription.financial_year,
|
||||
)
|
||||
).first()
|
||||
if existing:
|
||||
continue
|
||||
|
||||
db.add(
|
||||
ClientServiceTaskInstance(
|
||||
tenant_id=subscription.tenant_id,
|
||||
branch_id=subscription.branch_id,
|
||||
subscription_id=subscription.id,
|
||||
client_id=subscription.client_id,
|
||||
service_catalogue_id=subscription.service_catalogue_id,
|
||||
firm_task_template_id=template.id,
|
||||
financial_year=subscription.financial_year,
|
||||
assessment_year=subscription.assessment_year,
|
||||
task_name=template.task_name,
|
||||
description=template.description,
|
||||
sequence_no=template.sequence_no,
|
||||
default_role_name=template.default_role_name,
|
||||
assigned_to_user_id=_default_assignee_for_template(subscription, template),
|
||||
internal_target_date=_default_internal_target_date(subscription, template),
|
||||
status="pending",
|
||||
priority="normal",
|
||||
is_active=True,
|
||||
created_by_user_id=user_id,
|
||||
updated_by_user_id=user_id,
|
||||
)
|
||||
)
|
||||
created += 1
|
||||
return created
|
||||
|
||||
|
||||
def _decorate_task_for_tracker(task: ClientServiceTaskInstance, *, today: date) -> ClientServiceTaskInstance:
|
||||
target_date = getattr(task, "internal_target_date", None)
|
||||
subscription = getattr(task, "subscription", None)
|
||||
engagement_due_date = getattr(subscription, "current_due_date", None) if subscription else None
|
||||
task.is_task_overdue = bool(target_date and target_date < today and task.status not in CLOSED_TASK_STATUSES)
|
||||
task.is_due_today = bool(target_date and target_date == today and task.status not in CLOSED_TASK_STATUSES)
|
||||
task.is_engagement_due_overdue = bool(
|
||||
engagement_due_date and engagement_due_date < today and task.status not in CLOSED_TASK_STATUSES
|
||||
)
|
||||
task.tracker_status_label = dict(TASK_STATUSES).get(task.status, task.status)
|
||||
task.priority_label = dict(TASK_PRIORITIES).get(task.priority, task.priority)
|
||||
return task
|
||||
|
||||
|
||||
|
||||
|
||||
def _apply_partner_visibility_filter(query, partner_user_id: int | None):
|
||||
if not partner_user_id:
|
||||
return query
|
||||
return query.where(
|
||||
or_(
|
||||
ClientServiceTaskInstance.subscription.has(
|
||||
ClientServiceSubscription.assigned_partner_user_id == partner_user_id
|
||||
),
|
||||
ClientServiceTaskInstance.client.has(Client.partner_id == partner_user_id),
|
||||
)
|
||||
)
|
||||
|
||||
def list_tasks_payload(
|
||||
db: Session,
|
||||
*,
|
||||
tenant_id: int,
|
||||
branch_id: int | None = None,
|
||||
assigned_to_user_id: int | None = None,
|
||||
partner_user_id: int | None = None,
|
||||
status: str = "",
|
||||
q: str = "",
|
||||
include_inactive: bool = False,
|
||||
financial_year: str | None = None,
|
||||
):
|
||||
today = date.today()
|
||||
special_filter = (status or "").strip().lower()
|
||||
query = (
|
||||
select(ClientServiceTaskInstance)
|
||||
.options(
|
||||
selectinload(ClientServiceTaskInstance.client),
|
||||
selectinload(ClientServiceTaskInstance.catalogue),
|
||||
selectinload(ClientServiceTaskInstance.assigned_to),
|
||||
selectinload(ClientServiceTaskInstance.subscription),
|
||||
)
|
||||
.where(ClientServiceTaskInstance.tenant_id == tenant_id)
|
||||
)
|
||||
if financial_year:
|
||||
query = query.where(ClientServiceTaskInstance.financial_year == financial_year.strip())
|
||||
if branch_id:
|
||||
query = query.where(ClientServiceTaskInstance.branch_id == branch_id)
|
||||
if assigned_to_user_id:
|
||||
query = query.where(ClientServiceTaskInstance.assigned_to_user_id == assigned_to_user_id)
|
||||
query = _apply_partner_visibility_filter(query, partner_user_id)
|
||||
if special_filter and special_filter not in {"overdue", "due_today", "unassigned"}:
|
||||
query = query.where(ClientServiceTaskInstance.status == special_filter)
|
||||
if special_filter == "overdue":
|
||||
query = query.where(
|
||||
ClientServiceTaskInstance.internal_target_date.is_not(None),
|
||||
ClientServiceTaskInstance.internal_target_date < today,
|
||||
ClientServiceTaskInstance.status.notin_(list(CLOSED_TASK_STATUSES)),
|
||||
)
|
||||
elif special_filter == "due_today":
|
||||
query = query.where(
|
||||
ClientServiceTaskInstance.internal_target_date == today,
|
||||
ClientServiceTaskInstance.status.notin_(list(CLOSED_TASK_STATUSES)),
|
||||
)
|
||||
elif special_filter == "unassigned":
|
||||
query = query.where(ClientServiceTaskInstance.assigned_to_user_id.is_(None))
|
||||
if not include_inactive:
|
||||
query = query.where(ClientServiceTaskInstance.is_active.is_(True))
|
||||
if q.strip():
|
||||
term = f"%{q.strip()}%"
|
||||
query = (
|
||||
query.join(Client, Client.id == ClientServiceTaskInstance.client_id)
|
||||
.join(ServiceCatalogue, ServiceCatalogue.id == ClientServiceTaskInstance.service_catalogue_id)
|
||||
.where(
|
||||
or_(
|
||||
ClientServiceTaskInstance.task_name.ilike(term),
|
||||
Client.client_name.ilike(term),
|
||||
Client.client_code.ilike(term),
|
||||
ServiceCatalogue.service_code.ilike(term),
|
||||
ServiceCatalogue.service_name.ilike(term),
|
||||
)
|
||||
)
|
||||
)
|
||||
rows = db.execute(
|
||||
query.order_by(
|
||||
ClientServiceTaskInstance.internal_target_date.is_(None),
|
||||
ClientServiceTaskInstance.internal_target_date.asc(),
|
||||
ClientServiceTaskInstance.status.asc(),
|
||||
ClientServiceTaskInstance.sequence_no.asc(),
|
||||
ClientServiceTaskInstance.id.desc(),
|
||||
)
|
||||
).scalars().all()
|
||||
return [_decorate_task_for_tracker(task, today=today) for task in rows]
|
||||
|
||||
|
||||
def get_task(
|
||||
db: Session,
|
||||
*,
|
||||
tenant_id: int,
|
||||
task_id: int,
|
||||
branch_id: int | None = None,
|
||||
assigned_to_user_id: int | None = None,
|
||||
partner_user_id: int | None = None,
|
||||
financial_year: str | None = None,
|
||||
) -> ClientServiceTaskInstance | None:
|
||||
query = (
|
||||
select(ClientServiceTaskInstance)
|
||||
.options(
|
||||
selectinload(ClientServiceTaskInstance.client),
|
||||
selectinload(ClientServiceTaskInstance.catalogue),
|
||||
selectinload(ClientServiceTaskInstance.assigned_to),
|
||||
selectinload(ClientServiceTaskInstance.subscription),
|
||||
)
|
||||
.where(
|
||||
ClientServiceTaskInstance.id == task_id,
|
||||
ClientServiceTaskInstance.tenant_id == tenant_id,
|
||||
)
|
||||
)
|
||||
if branch_id:
|
||||
query = query.where(ClientServiceTaskInstance.branch_id == branch_id)
|
||||
if financial_year:
|
||||
query = query.where(ClientServiceTaskInstance.financial_year == financial_year.strip())
|
||||
if assigned_to_user_id:
|
||||
query = query.where(ClientServiceTaskInstance.assigned_to_user_id == assigned_to_user_id)
|
||||
query = _apply_partner_visibility_filter(query, partner_user_id)
|
||||
task = db.execute(query).scalar_one_or_none()
|
||||
return _decorate_task_for_tracker(task, today=date.today()) if task else None
|
||||
|
||||
|
||||
def list_assignees_for_execution(db: Session, *, tenant_id: int, branch_id: int | None = None):
|
||||
query = select(User).where(User.tenant_id == tenant_id, User.is_active.is_(True))
|
||||
if branch_id:
|
||||
query = query.where((User.branch_id == branch_id) | (User.branch_id.is_(None)))
|
||||
return db.execute(query.order_by(User.full_name.asc(), User.email.asc())).scalars().all()
|
||||
|
||||
|
||||
def apply_task_update(
|
||||
task: ClientServiceTaskInstance,
|
||||
*,
|
||||
status: str,
|
||||
priority: str,
|
||||
assigned_to_user_id: int | None,
|
||||
internal_target_date: date | None,
|
||||
remarks: str,
|
||||
is_active: bool,
|
||||
user_id: int,
|
||||
):
|
||||
if getattr(task, "is_locked", False) or getattr(getattr(task, "subscription", None), "is_locked", False):
|
||||
return
|
||||
previous_status = task.status
|
||||
task.status = _normalise_status(status)
|
||||
task.priority = _normalise_priority(priority)
|
||||
task.assigned_to_user_id = assigned_to_user_id
|
||||
task.internal_target_date = internal_target_date
|
||||
task.remarks = remarks.strip() or None
|
||||
task.is_active = is_active
|
||||
task.updated_by_user_id = user_id
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
if previous_status != "in_progress" and task.status == "in_progress" and not task.started_at_utc:
|
||||
task.started_at_utc = now
|
||||
if task.status == "completed" and not task.completed_at_utc:
|
||||
task.completed_at_utc = now
|
||||
if task.status != "completed":
|
||||
task.completed_at_utc = None
|
||||
|
||||
|
||||
def apply_bulk_task_update(
|
||||
tasks: list[ClientServiceTaskInstance],
|
||||
*,
|
||||
status: str | None,
|
||||
assigned_to_user_id: int | None,
|
||||
update_assignee: bool,
|
||||
internal_target_date: date | None,
|
||||
update_internal_target_date: bool,
|
||||
user_id: int,
|
||||
) -> tuple[int, int]:
|
||||
updated = 0
|
||||
skipped = 0
|
||||
for task in tasks:
|
||||
if getattr(task, "is_locked", False) or getattr(getattr(task, "subscription", None), "is_locked", False):
|
||||
skipped += 1
|
||||
continue
|
||||
previous_status = task.status
|
||||
if status:
|
||||
task.status = _normalise_status(status)
|
||||
if update_assignee:
|
||||
task.assigned_to_user_id = assigned_to_user_id
|
||||
if update_internal_target_date:
|
||||
task.internal_target_date = internal_target_date
|
||||
task.updated_by_user_id = user_id
|
||||
now = datetime.now(timezone.utc)
|
||||
if previous_status != "in_progress" and task.status == "in_progress" and not task.started_at_utc:
|
||||
task.started_at_utc = now
|
||||
if task.status == "completed" and not task.completed_at_utc:
|
||||
task.completed_at_utc = now
|
||||
if task.status != "completed":
|
||||
task.completed_at_utc = None
|
||||
updated += 1
|
||||
return updated, skipped
|
||||
|
||||
|
||||
def get_tasks_for_bulk_update(
|
||||
db: Session,
|
||||
*,
|
||||
tenant_id: int,
|
||||
task_ids: list[int],
|
||||
branch_id: int | None = None,
|
||||
assigned_to_user_id: int | None = None,
|
||||
partner_user_id: int | None = None,
|
||||
financial_year: str | None = None,
|
||||
) -> list[ClientServiceTaskInstance]:
|
||||
if not task_ids:
|
||||
return []
|
||||
query = (
|
||||
select(ClientServiceTaskInstance)
|
||||
.options(selectinload(ClientServiceTaskInstance.subscription))
|
||||
.where(
|
||||
ClientServiceTaskInstance.tenant_id == tenant_id,
|
||||
ClientServiceTaskInstance.id.in_(task_ids),
|
||||
ClientServiceTaskInstance.is_active.is_(True),
|
||||
)
|
||||
)
|
||||
if branch_id:
|
||||
query = query.where(ClientServiceTaskInstance.branch_id == branch_id)
|
||||
if financial_year:
|
||||
query = query.where(ClientServiceTaskInstance.financial_year == financial_year.strip())
|
||||
if assigned_to_user_id:
|
||||
query = query.where(ClientServiceTaskInstance.assigned_to_user_id == assigned_to_user_id)
|
||||
query = _apply_partner_visibility_filter(query, partner_user_id)
|
||||
return db.execute(query).scalars().all()
|
||||
|
||||
|
||||
|
||||
def list_task_comments(db: Session, *, tenant_id: int, task_id: int) -> list[ServiceTaskComment]:
|
||||
return db.execute(
|
||||
select(ServiceTaskComment)
|
||||
.options(selectinload(ServiceTaskComment.created_by))
|
||||
.where(
|
||||
ServiceTaskComment.tenant_id == tenant_id,
|
||||
ServiceTaskComment.task_instance_id == task_id,
|
||||
ServiceTaskComment.is_deleted.is_(False),
|
||||
)
|
||||
.order_by(ServiceTaskComment.created_at_utc.desc(), ServiceTaskComment.id.desc())
|
||||
).scalars().all()
|
||||
|
||||
|
||||
def add_task_comment(
|
||||
db: Session,
|
||||
*,
|
||||
task: ClientServiceTaskInstance,
|
||||
comment_type: str,
|
||||
visibility: str,
|
||||
message: str,
|
||||
user_id: int,
|
||||
) -> ServiceTaskComment | None:
|
||||
if getattr(task, "is_locked", False) or getattr(getattr(task, "subscription", None), "is_locked", False):
|
||||
return None
|
||||
clean_message = (message or "").strip()
|
||||
if not clean_message:
|
||||
return None
|
||||
row = ServiceTaskComment(
|
||||
tenant_id=task.tenant_id,
|
||||
branch_id=task.branch_id,
|
||||
subscription_id=task.subscription_id,
|
||||
task_instance_id=task.id,
|
||||
comment_type=_normalise_comment_type(comment_type),
|
||||
visibility=_normalise_visibility(visibility),
|
||||
message=clean_message,
|
||||
created_by_user_id=user_id,
|
||||
)
|
||||
db.add(row)
|
||||
task.updated_by_user_id = user_id
|
||||
return row
|
||||
|
||||
|
||||
|
||||
def list_client_visible_task_comments(
|
||||
db: Session,
|
||||
*,
|
||||
tenant_id: int,
|
||||
client_id: int,
|
||||
limit: int = 20,
|
||||
) -> list[ServiceTaskComment]:
|
||||
"""Return client-visible task communication for one client dashboard.
|
||||
|
||||
This is intentionally read-only and scoped by tenant + client. Internal and
|
||||
consultant-only notes are never returned to the client portal.
|
||||
"""
|
||||
safe_limit = max(1, min(int(limit or 20), 100))
|
||||
rows = db.execute(
|
||||
select(ServiceTaskComment)
|
||||
.options(
|
||||
selectinload(ServiceTaskComment.created_by),
|
||||
selectinload(ServiceTaskComment.task).selectinload(ClientServiceTaskInstance.catalogue),
|
||||
selectinload(ServiceTaskComment.task).selectinload(ClientServiceTaskInstance.subscription),
|
||||
)
|
||||
.join(ClientServiceTaskInstance, ClientServiceTaskInstance.id == ServiceTaskComment.task_instance_id)
|
||||
.where(
|
||||
ServiceTaskComment.tenant_id == tenant_id,
|
||||
ServiceTaskComment.visibility == "client",
|
||||
ServiceTaskComment.is_deleted.is_(False),
|
||||
ClientServiceTaskInstance.client_id == client_id,
|
||||
ClientServiceTaskInstance.tenant_id == tenant_id,
|
||||
ClientServiceTaskInstance.is_active.is_(True),
|
||||
)
|
||||
.order_by(ServiceTaskComment.created_at_utc.desc(), ServiceTaskComment.id.desc())
|
||||
.limit(safe_limit)
|
||||
).scalars().all()
|
||||
|
||||
type_labels = dict(TASK_COMMENT_TYPES)
|
||||
visibility_labels = dict(TASK_COMMENT_VISIBILITIES)
|
||||
for row in rows:
|
||||
row.comment_type_label = type_labels.get(row.comment_type, row.comment_type)
|
||||
row.visibility_label = visibility_labels.get(row.visibility, row.visibility)
|
||||
return rows
|
||||
|
||||
|
||||
def dashboard_stats(
|
||||
db: Session,
|
||||
*,
|
||||
tenant_id: int,
|
||||
branch_id: int | None = None,
|
||||
assigned_to_user_id: int | None = None,
|
||||
partner_user_id: int | None = None,
|
||||
financial_year: str | None = None,
|
||||
):
|
||||
today = date.today()
|
||||
base = select(ClientServiceTaskInstance).where(
|
||||
ClientServiceTaskInstance.tenant_id == tenant_id,
|
||||
ClientServiceTaskInstance.is_active.is_(True),
|
||||
)
|
||||
if branch_id:
|
||||
base = base.where(ClientServiceTaskInstance.branch_id == branch_id)
|
||||
if financial_year:
|
||||
base = base.where(ClientServiceTaskInstance.financial_year == financial_year.strip())
|
||||
if assigned_to_user_id:
|
||||
base = base.where(ClientServiceTaskInstance.assigned_to_user_id == assigned_to_user_id)
|
||||
base = _apply_partner_visibility_filter(base, partner_user_id)
|
||||
|
||||
subq = base.subquery()
|
||||
total = db.execute(select(func.count()).select_from(subq)).scalar_one()
|
||||
pending = db.execute(select(func.count()).select_from(subq).where(subq.c.status == "pending")).scalar_one()
|
||||
progress = db.execute(select(func.count()).select_from(subq).where(subq.c.status == "in_progress")).scalar_one()
|
||||
blocked = db.execute(select(func.count()).select_from(subq).where(subq.c.status == "blocked")).scalar_one()
|
||||
completed = db.execute(select(func.count()).select_from(subq).where(subq.c.status == "completed")).scalar_one()
|
||||
not_applicable = db.execute(select(func.count()).select_from(subq).where(subq.c.status == "not_applicable")).scalar_one()
|
||||
unassigned = db.execute(select(func.count()).select_from(subq).where(subq.c.assigned_to_user_id.is_(None))).scalar_one()
|
||||
overdue = db.execute(
|
||||
select(func.count()).select_from(subq).where(
|
||||
subq.c.internal_target_date.is_not(None),
|
||||
subq.c.internal_target_date < today,
|
||||
subq.c.status.notin_(list(CLOSED_TASK_STATUSES)),
|
||||
)
|
||||
).scalar_one()
|
||||
due_today = db.execute(
|
||||
select(func.count()).select_from(subq).where(
|
||||
subq.c.internal_target_date == today,
|
||||
subq.c.status.notin_(list(CLOSED_TASK_STATUSES)),
|
||||
)
|
||||
).scalar_one()
|
||||
return {
|
||||
"total": total,
|
||||
"pending": pending,
|
||||
"in_progress": progress,
|
||||
"blocked": blocked,
|
||||
"completed": completed,
|
||||
"not_applicable": not_applicable,
|
||||
"unassigned": unassigned,
|
||||
"overdue": overdue,
|
||||
"due_today": due_today,
|
||||
}
|
||||
Reference in New Issue
Block a user