255 lines
9.1 KiB
Python
255 lines
9.1 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
from sqlalchemy import func, or_, select
|
|
from sqlalchemy.orm import Session, selectinload
|
|
|
|
from app.modules.core.tenancy.models import Branch
|
|
from app.modules.services.models import (
|
|
FirmServiceSelection,
|
|
FirmServiceTaskTemplate,
|
|
ServiceCatalogue,
|
|
ServiceCategory,
|
|
ServiceDefaultTaskTemplate,
|
|
ServiceDueDateRule,
|
|
)
|
|
|
|
RECURRENCE_CHOICES = [
|
|
("one_time", "One Time"),
|
|
("monthly", "Monthly"),
|
|
("quarterly", "Quarterly"),
|
|
("yearly", "Yearly"),
|
|
("event_based", "Event Based"),
|
|
("custom", "Custom"),
|
|
]
|
|
|
|
ENGAGEMENT_TYPE_CHOICES = [
|
|
("assurance", "Assurance"),
|
|
("non_assurance", "Non-Assurance"),
|
|
]
|
|
|
|
VALID_ENGAGEMENT_TYPES = {value for value, _label in ENGAGEMENT_TYPE_CHOICES}
|
|
|
|
|
|
def normalize_engagement_type(value: str | None) -> str:
|
|
value = (value or "").strip().lower().replace("-", "_").replace(" ", "_")
|
|
if value in {"assurance", "audit", "aud", "certification", "certificate", "attestation"}:
|
|
return "assurance"
|
|
if value in {"non_assurance", "nonassurance", "non_audit", "nonaudit", "non", "compliance", "consulting", "consultancy"}:
|
|
return "non_assurance"
|
|
return "non_assurance"
|
|
|
|
|
|
def engagement_type_label(value: str | None) -> str:
|
|
normalized = normalize_engagement_type(value)
|
|
return "Assurance" if normalized == "assurance" else "Non-Assurance"
|
|
|
|
|
|
def normalize_code(value: str) -> str:
|
|
value = (value or "").strip().upper()
|
|
value = re.sub(r"[^A-Z0-9]+", "-", value)
|
|
value = re.sub(r"-+", "-", value).strip("-")
|
|
return value
|
|
|
|
|
|
def list_categories(db: Session, *, q: str = ""):
|
|
query = select(ServiceCategory)
|
|
if q.strip():
|
|
term = f"%{q.strip()}%"
|
|
query = query.where(or_(ServiceCategory.code.ilike(term), ServiceCategory.name.ilike(term)))
|
|
return db.execute(query.order_by(ServiceCategory.sort_order.asc(), ServiceCategory.name.asc())).scalars().all()
|
|
|
|
|
|
def get_category(db: Session, category_id: int) -> ServiceCategory | None:
|
|
return db.execute(select(ServiceCategory).where(ServiceCategory.id == category_id)).scalar_one_or_none()
|
|
|
|
|
|
def list_catalogue_payload(db: Session, *, q: str = "", category_id: int | None = None, recurrence_type: str = "", engagement_type: str = "", page: int = 1, per_page: int = 20):
|
|
query = select(ServiceCatalogue).where(
|
|
ServiceCatalogue.is_active.is_(True)
|
|
).options(
|
|
selectinload(ServiceCatalogue.service_category),
|
|
selectinload(ServiceCatalogue.default_task_templates),
|
|
selectinload(ServiceCatalogue.due_date_rules),
|
|
)
|
|
if q.strip():
|
|
term = f"%{q.strip()}%"
|
|
query = query.outerjoin(ServiceCategory, ServiceCategory.id == ServiceCatalogue.category_id).where(
|
|
or_(
|
|
ServiceCatalogue.service_code.ilike(term),
|
|
ServiceCatalogue.service_name.ilike(term),
|
|
ServiceCatalogue.category.ilike(term),
|
|
ServiceCategory.name.ilike(term),
|
|
)
|
|
)
|
|
if category_id:
|
|
query = query.where(ServiceCatalogue.category_id == category_id)
|
|
if recurrence_type.strip():
|
|
query = query.where(ServiceCatalogue.recurrence_type == recurrence_type.strip())
|
|
if engagement_type.strip():
|
|
query = query.where(ServiceCatalogue.engagement_type == normalize_engagement_type(engagement_type))
|
|
|
|
total = db.execute(select(func.count()).select_from(query.subquery())).scalar_one()
|
|
rows = db.execute(
|
|
query.order_by(ServiceCatalogue.sort_order.asc(), ServiceCatalogue.service_name.asc())
|
|
.offset((page - 1) * per_page)
|
|
.limit(per_page)
|
|
).scalars().all()
|
|
|
|
return {
|
|
"rows": rows,
|
|
"q": q,
|
|
"category_id": category_id,
|
|
"recurrence_type": recurrence_type,
|
|
"engagement_type": engagement_type,
|
|
"page": page,
|
|
"per_page": per_page,
|
|
"total": total,
|
|
"pages": max(1, (total + per_page - 1) // per_page),
|
|
}
|
|
|
|
|
|
def list_firm_services_payload(db: Session, *, tenant_id: int, q: str = ""):
|
|
query = (
|
|
select(FirmServiceSelection, ServiceCatalogue, Branch)
|
|
.join(ServiceCatalogue, ServiceCatalogue.id == FirmServiceSelection.service_catalogue_id)
|
|
.outerjoin(Branch, Branch.id == FirmServiceSelection.default_branch_id)
|
|
.where(
|
|
FirmServiceSelection.tenant_id == tenant_id,
|
|
FirmServiceSelection.is_enabled.is_(True),
|
|
ServiceCatalogue.is_active.is_(True),
|
|
)
|
|
)
|
|
if q.strip():
|
|
term = f"%{q.strip()}%"
|
|
query = query.outerjoin(ServiceCategory, ServiceCategory.id == ServiceCatalogue.category_id).where(
|
|
or_(
|
|
ServiceCatalogue.service_code.ilike(term),
|
|
ServiceCatalogue.service_name.ilike(term),
|
|
ServiceCatalogue.category.ilike(term),
|
|
ServiceCategory.name.ilike(term),
|
|
)
|
|
)
|
|
rows = db.execute(query.order_by(ServiceCatalogue.sort_order.asc(), ServiceCatalogue.service_name.asc())).all()
|
|
return [
|
|
{"selection": selection, "catalogue": catalogue, "branch": branch}
|
|
for selection, catalogue, branch in rows
|
|
]
|
|
|
|
|
|
def list_disabled_catalogues(db: Session, *, tenant_id: int, q: str = ""):
|
|
enabled_subq = (
|
|
select(FirmServiceSelection.service_catalogue_id)
|
|
.where(
|
|
FirmServiceSelection.tenant_id == tenant_id,
|
|
FirmServiceSelection.is_enabled.is_(True),
|
|
)
|
|
)
|
|
|
|
query = select(ServiceCatalogue).where(
|
|
ServiceCatalogue.is_active.is_(True),
|
|
~ServiceCatalogue.id.in_(enabled_subq),
|
|
).options(
|
|
selectinload(ServiceCatalogue.service_category),
|
|
selectinload(ServiceCatalogue.default_task_templates),
|
|
selectinload(ServiceCatalogue.due_date_rules),
|
|
)
|
|
if q.strip():
|
|
term = f"%{q.strip()}%"
|
|
query = query.outerjoin(ServiceCategory, ServiceCategory.id == ServiceCatalogue.category_id).where(
|
|
or_(
|
|
ServiceCatalogue.service_code.ilike(term),
|
|
ServiceCatalogue.service_name.ilike(term),
|
|
ServiceCatalogue.category.ilike(term),
|
|
ServiceCategory.name.ilike(term),
|
|
)
|
|
)
|
|
return db.execute(query.order_by(ServiceCatalogue.sort_order.asc(), ServiceCatalogue.service_name.asc())).scalars().all()
|
|
|
|
|
|
def get_catalogue(db: Session, catalogue_id: int) -> ServiceCatalogue | None:
|
|
return db.execute(
|
|
select(ServiceCatalogue)
|
|
.options(
|
|
selectinload(ServiceCatalogue.service_category),
|
|
selectinload(ServiceCatalogue.default_task_templates),
|
|
)
|
|
.where(ServiceCatalogue.id == catalogue_id)
|
|
).scalar_one_or_none()
|
|
|
|
|
|
def get_firm_selection(db: Session, *, tenant_id: int, catalogue_id: int) -> FirmServiceSelection | None:
|
|
return db.execute(
|
|
select(FirmServiceSelection).where(
|
|
FirmServiceSelection.tenant_id == tenant_id,
|
|
FirmServiceSelection.service_catalogue_id == catalogue_id,
|
|
)
|
|
).scalar_one_or_none()
|
|
|
|
|
|
def get_firm_task_templates(db: Session, *, tenant_id: int, catalogue_id: int):
|
|
return db.execute(
|
|
select(FirmServiceTaskTemplate)
|
|
.where(
|
|
FirmServiceTaskTemplate.tenant_id == tenant_id,
|
|
FirmServiceTaskTemplate.service_catalogue_id == catalogue_id,
|
|
)
|
|
.order_by(FirmServiceTaskTemplate.sequence_no.asc(), FirmServiceTaskTemplate.id.asc())
|
|
).scalars().all()
|
|
|
|
|
|
def get_default_task_templates(db: Session, *, catalogue_id: int):
|
|
return db.execute(
|
|
select(ServiceDefaultTaskTemplate)
|
|
.where(ServiceDefaultTaskTemplate.service_catalogue_id == catalogue_id)
|
|
.order_by(ServiceDefaultTaskTemplate.sequence_no.asc(), ServiceDefaultTaskTemplate.id.asc())
|
|
).scalars().all()
|
|
|
|
|
|
def next_task_sequence(db: Session, *, tenant_id: int, catalogue_id: int) -> int:
|
|
max_seq = db.execute(
|
|
select(func.max(FirmServiceTaskTemplate.sequence_no)).where(
|
|
FirmServiceTaskTemplate.tenant_id == tenant_id,
|
|
FirmServiceTaskTemplate.service_catalogue_id == catalogue_id,
|
|
)
|
|
).scalar_one()
|
|
return int(max_seq or 0) + 1
|
|
|
|
|
|
def next_default_task_sequence(db: Session, *, catalogue_id: int) -> int:
|
|
max_seq = db.execute(
|
|
select(func.max(ServiceDefaultTaskTemplate.sequence_no)).where(
|
|
ServiceDefaultTaskTemplate.service_catalogue_id == catalogue_id,
|
|
)
|
|
).scalar_one()
|
|
return int(max_seq or 0) + 1
|
|
|
|
def get_firm_task_template(
|
|
db: Session,
|
|
*,
|
|
tenant_id: int,
|
|
catalogue_id: int,
|
|
task_id: int,
|
|
) -> FirmServiceTaskTemplate | None:
|
|
return db.execute(
|
|
select(FirmServiceTaskTemplate).where(
|
|
FirmServiceTaskTemplate.id == task_id,
|
|
FirmServiceTaskTemplate.tenant_id == tenant_id,
|
|
FirmServiceTaskTemplate.service_catalogue_id == catalogue_id,
|
|
)
|
|
).scalar_one_or_none()
|
|
|
|
|
|
def get_default_task_template(
|
|
db: Session,
|
|
*,
|
|
catalogue_id: int,
|
|
task_id: int,
|
|
) -> ServiceDefaultTaskTemplate | None:
|
|
return db.execute(
|
|
select(ServiceDefaultTaskTemplate).where(
|
|
ServiceDefaultTaskTemplate.id == task_id,
|
|
ServiceDefaultTaskTemplate.service_catalogue_id == catalogue_id,
|
|
)
|
|
).scalar_one_or_none()
|