Files
arrr-erp/app/modules/services/services.py
T
2026-07-17 23:20:08 +05:30

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()