Add controlled client subscription team reassignment
This commit is contained in:
@@ -1,22 +1,41 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from datetime import date, datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Form, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.core.db.common import CommonSessionLocal
|
||||
from app.core.security.csrf import get_or_create_csrf_token
|
||||
from app.core.security.csrf import get_or_create_csrf_token, validate_csrf
|
||||
from app.core.security.session_auth import get_current_user
|
||||
from app.core.templating import templates
|
||||
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
|
||||
from app.modules.core.rbac.permission_guard import require_permission
|
||||
from app.modules.services.models import ClientServicePlan, ClientServiceSubscription
|
||||
from app.modules.services.client_services import (
|
||||
SUBSCRIPTION_STATUSES,
|
||||
list_assignable_users,
|
||||
list_review_partners,
|
||||
parse_date,
|
||||
)
|
||||
from app.modules.services.models import (
|
||||
ClientServicePlan,
|
||||
ClientServiceSubscription,
|
||||
ClientServiceTaskInstance,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/services/subscriptions", tags=["client-service-subscriptions-ui"])
|
||||
|
||||
|
||||
def _tenant_id(request, user):
|
||||
return int(request.session.get("active_tenant_id") or request.session.get("selected_tenant_id") or request.session.get("tenant_id") or user.tenant_id)
|
||||
return int(
|
||||
request.session.get("active_tenant_id")
|
||||
or request.session.get("selected_tenant_id")
|
||||
or request.session.get("tenant_id")
|
||||
or user.tenant_id
|
||||
)
|
||||
|
||||
|
||||
def _branch_id(request, user):
|
||||
value = request.session.get("active_branch_id")
|
||||
@@ -24,47 +43,426 @@ def _branch_id(request, user):
|
||||
return int(getattr(user, "branch_id", 0) or 0) or None
|
||||
return int(value)
|
||||
|
||||
|
||||
def _ctx(request, db, user, **extra):
|
||||
data={"request":request,"current_user":user,"current_user_roles":get_user_roles(db,user.id),"current_user_permissions":get_user_permissions(db,user.id),"csrf_token":get_or_create_csrf_token(request)}
|
||||
data.update(extra); return data
|
||||
data = {
|
||||
"request": request,
|
||||
"current_user": user,
|
||||
"current_user_roles": get_user_roles(db, user.id),
|
||||
"current_user_permissions": get_user_permissions(db, user.id),
|
||||
"csrf_token": get_or_create_csrf_token(request),
|
||||
"subscription_statuses": SUBSCRIPTION_STATUSES,
|
||||
}
|
||||
data.update(extra)
|
||||
return data
|
||||
|
||||
|
||||
def _denied():
|
||||
from app.core.http_responses import ui_access_denied
|
||||
return ui_access_denied()
|
||||
|
||||
|
||||
def _load_plan(db, *, plan_id: int, tenant_id: int, branch_id: int | None):
|
||||
stmt = (
|
||||
select(ClientServicePlan)
|
||||
.options(
|
||||
selectinload(ClientServicePlan.client),
|
||||
selectinload(ClientServicePlan.catalogue),
|
||||
selectinload(ClientServicePlan.default_partner),
|
||||
selectinload(ClientServicePlan.default_performing_partner),
|
||||
selectinload(ClientServicePlan.default_manager),
|
||||
selectinload(ClientServicePlan.default_staff),
|
||||
selectinload(ClientServicePlan.default_review_partner),
|
||||
)
|
||||
.where(
|
||||
ClientServicePlan.id == plan_id,
|
||||
ClientServicePlan.tenant_id == tenant_id,
|
||||
)
|
||||
)
|
||||
if branch_id:
|
||||
stmt = stmt.where(ClientServicePlan.branch_id == branch_id)
|
||||
return db.execute(stmt).scalar_one_or_none()
|
||||
|
||||
|
||||
def _optional_int(value: str) -> int | None:
|
||||
value = (value or "").strip()
|
||||
return int(value) if value else None
|
||||
|
||||
|
||||
def _allowed_id(value: int | None, allowed: set[int], field_name: str) -> int | None:
|
||||
if value is not None and value not in allowed:
|
||||
raise ValueError(field_name)
|
||||
return value
|
||||
|
||||
|
||||
def _engagement_is_open(row: ClientServiceSubscription) -> bool:
|
||||
return (
|
||||
not bool(getattr(row, "is_locked", False))
|
||||
and (getattr(row, "status", "") or "").lower() not in {"completed", "cancelled", "inactive"}
|
||||
)
|
||||
|
||||
|
||||
def _engagement_is_effective(row: ClientServiceSubscription, effective_from: date | None) -> bool:
|
||||
if not effective_from:
|
||||
return True
|
||||
comparison_date = (
|
||||
getattr(row, "start_date", None)
|
||||
or getattr(row, "current_due_date", None)
|
||||
or getattr(row, "original_due_date", None)
|
||||
)
|
||||
return comparison_date is None or comparison_date >= effective_from
|
||||
|
||||
|
||||
@router.get("")
|
||||
def subscription_master_list(request: Request, q: str = "", include_inactive: bool = False):
|
||||
db=CommonSessionLocal()
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user=get_current_user(request, db=db)
|
||||
if not user: return RedirectResponse("/login",303)
|
||||
try: require_permission(db,user,"clients.view")
|
||||
user = get_current_user(request, db=db)
|
||||
if not user:
|
||||
return RedirectResponse("/login", 303)
|
||||
try:
|
||||
require_permission(db, user, "clients.view")
|
||||
except Exception:
|
||||
from app.core.http_responses import ui_access_denied
|
||||
return ui_access_denied()
|
||||
tenant_id=_tenant_id(request,user); branch_id=_branch_id(request,user)
|
||||
counts=(select(ClientServiceSubscription.service_plan_id, func.count(ClientServiceSubscription.id).label("engagement_count"), func.max(ClientServiceSubscription.current_due_date).label("latest_due_date")).where(ClientServiceSubscription.service_plan_id.is_not(None)).group_by(ClientServiceSubscription.service_plan_id).subquery())
|
||||
stmt=(select(ClientServicePlan, counts.c.engagement_count, counts.c.latest_due_date).options(selectinload(ClientServicePlan.client),selectinload(ClientServicePlan.catalogue),selectinload(ClientServicePlan.default_partner),selectinload(ClientServicePlan.default_performing_partner),selectinload(ClientServicePlan.default_manager),selectinload(ClientServicePlan.default_staff),selectinload(ClientServicePlan.default_review_partner)).outerjoin(counts,counts.c.service_plan_id==ClientServicePlan.id).where(ClientServicePlan.tenant_id==tenant_id))
|
||||
if branch_id: stmt=stmt.where(ClientServicePlan.branch_id==branch_id)
|
||||
if not include_inactive: stmt=stmt.where(ClientServicePlan.is_active.is_(True))
|
||||
return _denied()
|
||||
|
||||
tenant_id = _tenant_id(request, user)
|
||||
branch_id = _branch_id(request, user)
|
||||
counts = (
|
||||
select(
|
||||
ClientServiceSubscription.service_plan_id,
|
||||
func.count(ClientServiceSubscription.id).label("engagement_count"),
|
||||
func.max(ClientServiceSubscription.current_due_date).label("latest_due_date"),
|
||||
)
|
||||
.where(ClientServiceSubscription.service_plan_id.is_not(None))
|
||||
.group_by(ClientServiceSubscription.service_plan_id)
|
||||
.subquery()
|
||||
)
|
||||
stmt = (
|
||||
select(ClientServicePlan, counts.c.engagement_count, counts.c.latest_due_date)
|
||||
.options(
|
||||
selectinload(ClientServicePlan.client),
|
||||
selectinload(ClientServicePlan.catalogue),
|
||||
selectinload(ClientServicePlan.default_partner),
|
||||
selectinload(ClientServicePlan.default_performing_partner),
|
||||
selectinload(ClientServicePlan.default_manager),
|
||||
selectinload(ClientServicePlan.default_staff),
|
||||
selectinload(ClientServicePlan.default_review_partner),
|
||||
)
|
||||
.outerjoin(counts, counts.c.service_plan_id == ClientServicePlan.id)
|
||||
.where(ClientServicePlan.tenant_id == tenant_id)
|
||||
)
|
||||
if branch_id:
|
||||
stmt = stmt.where(ClientServicePlan.branch_id == branch_id)
|
||||
if not include_inactive:
|
||||
stmt = stmt.where(ClientServicePlan.is_active.is_(True))
|
||||
if q.strip():
|
||||
from app.modules.clients.models import Client
|
||||
from app.modules.services.models import ServiceCatalogue
|
||||
term=f"%{q.strip()}%"
|
||||
stmt=stmt.join(Client,Client.id==ClientServicePlan.client_id).join(ServiceCatalogue,ServiceCatalogue.id==ClientServicePlan.service_catalogue_id).where((Client.client_name.ilike(term)) | (Client.client_code.ilike(term)) | (ServiceCatalogue.service_name.ilike(term)) | (ServiceCatalogue.service_code.ilike(term)))
|
||||
rows=db.execute(stmt.order_by(ClientServicePlan.is_active.desc(),ClientServicePlan.id.desc())).all()
|
||||
return templates.TemplateResponse("modules/services/templates/services/subscriptions/list.html",_ctx(request,db,user,title="Client Service Subscriptions",rows=rows,q=q,include_inactive=include_inactive))
|
||||
finally: db.close()
|
||||
term = f"%{q.strip()}%"
|
||||
stmt = (
|
||||
stmt.join(Client, Client.id == ClientServicePlan.client_id)
|
||||
.join(ServiceCatalogue, ServiceCatalogue.id == ClientServicePlan.service_catalogue_id)
|
||||
.where(
|
||||
(Client.client_name.ilike(term))
|
||||
| (Client.client_code.ilike(term))
|
||||
| (ServiceCatalogue.service_name.ilike(term))
|
||||
| (ServiceCatalogue.service_code.ilike(term))
|
||||
)
|
||||
)
|
||||
rows = db.execute(
|
||||
stmt.order_by(ClientServicePlan.is_active.desc(), ClientServicePlan.id.desc())
|
||||
).all()
|
||||
return templates.TemplateResponse(
|
||||
"modules/services/templates/services/subscriptions/list.html",
|
||||
_ctx(
|
||||
request,
|
||||
db,
|
||||
user,
|
||||
title="Client Service Subscriptions",
|
||||
rows=rows,
|
||||
q=q,
|
||||
include_inactive=include_inactive,
|
||||
),
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/{plan_id}/edit")
|
||||
def subscription_master_edit_page(
|
||||
request: Request,
|
||||
plan_id: int,
|
||||
saved: int = 0,
|
||||
updated_engagements: int = 0,
|
||||
updated_tasks: int = 0,
|
||||
error: str = "",
|
||||
):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
if not user:
|
||||
return RedirectResponse("/login", 303)
|
||||
try:
|
||||
require_permission(db, user, "clients.edit")
|
||||
except Exception:
|
||||
return _denied()
|
||||
|
||||
tenant_id = _tenant_id(request, user)
|
||||
branch_id = _branch_id(request, user)
|
||||
plan = _load_plan(db, plan_id=plan_id, tenant_id=tenant_id, branch_id=branch_id)
|
||||
if not plan:
|
||||
return RedirectResponse("/services/subscriptions", 303)
|
||||
|
||||
partners = list_assignable_users(
|
||||
db, tenant_id=tenant_id, branch_id=plan.branch_id, role_names=("Partner",)
|
||||
)
|
||||
managers = list_assignable_users(
|
||||
db, tenant_id=tenant_id, branch_id=plan.branch_id, role_names=("Branch Manager",)
|
||||
)
|
||||
staff_users = list_assignable_users(
|
||||
db, tenant_id=tenant_id, branch_id=plan.branch_id, role_names=("Staff",)
|
||||
)
|
||||
review_partners = list_review_partners(
|
||||
db, tenant_id=tenant_id, branch_id=plan.branch_id
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
"modules/services/templates/services/subscriptions/edit.html",
|
||||
_ctx(
|
||||
request,
|
||||
db,
|
||||
user,
|
||||
title="Edit Client Service Subscription",
|
||||
plan=plan,
|
||||
partners=partners,
|
||||
managers=managers,
|
||||
staff_users=staff_users,
|
||||
review_partners=review_partners,
|
||||
saved=saved,
|
||||
updated_engagements=updated_engagements,
|
||||
updated_tasks=updated_tasks,
|
||||
error=error,
|
||||
),
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/{plan_id}/edit")
|
||||
def subscription_master_edit_submit(
|
||||
request: Request,
|
||||
plan_id: int,
|
||||
default_partner_user_id: str = Form(""),
|
||||
default_performing_partner_user_id: str = Form(""),
|
||||
default_manager_user_id: str = Form(""),
|
||||
default_staff_user_id: str = Form(""),
|
||||
default_review_partner_user_id: str = Form(""),
|
||||
effective_from: str = Form(""),
|
||||
effective_to: str = Form(""),
|
||||
status: str = Form("active"),
|
||||
auto_generate_periods: str | None = Form(None),
|
||||
remarks: str = Form(""),
|
||||
propagation_mode: str = Form("future_only"),
|
||||
reassignment_effective_from: str = Form(""),
|
||||
reassignment_reason: str = Form(""),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
validate_csrf(request, csrf_token)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
if not user:
|
||||
return RedirectResponse("/login", 303)
|
||||
try:
|
||||
require_permission(db, user, "clients.edit")
|
||||
except Exception:
|
||||
return _denied()
|
||||
|
||||
tenant_id = _tenant_id(request, user)
|
||||
branch_id = _branch_id(request, user)
|
||||
plan = _load_plan(db, plan_id=plan_id, tenant_id=tenant_id, branch_id=branch_id)
|
||||
if not plan:
|
||||
return RedirectResponse("/services/subscriptions", 303)
|
||||
|
||||
partners = list_assignable_users(
|
||||
db, tenant_id=tenant_id, branch_id=plan.branch_id, role_names=("Partner",)
|
||||
)
|
||||
managers = list_assignable_users(
|
||||
db, tenant_id=tenant_id, branch_id=plan.branch_id, role_names=("Branch Manager",)
|
||||
)
|
||||
staff_users = list_assignable_users(
|
||||
db, tenant_id=tenant_id, branch_id=plan.branch_id, role_names=("Staff",)
|
||||
)
|
||||
review_partners = list_review_partners(
|
||||
db, tenant_id=tenant_id, branch_id=plan.branch_id
|
||||
)
|
||||
|
||||
try:
|
||||
new_partner_id = _allowed_id(
|
||||
_optional_int(default_partner_user_id), {row.id for row in partners}, "partner"
|
||||
)
|
||||
requested_performing_id = _allowed_id(
|
||||
_optional_int(default_performing_partner_user_id),
|
||||
{row.id for row in partners},
|
||||
"performing_partner",
|
||||
)
|
||||
new_performing_id = requested_performing_id or new_partner_id
|
||||
new_manager_id = _allowed_id(
|
||||
_optional_int(default_manager_user_id), {row.id for row in managers}, "manager"
|
||||
)
|
||||
new_staff_id = _allowed_id(
|
||||
_optional_int(default_staff_user_id), {row.id for row in staff_users}, "staff"
|
||||
)
|
||||
new_review_partner_id = _allowed_id(
|
||||
_optional_int(default_review_partner_user_id),
|
||||
{row.id for row in review_partners},
|
||||
"review_partner",
|
||||
)
|
||||
plan_effective_from = parse_date(effective_from)
|
||||
plan_effective_to = parse_date(effective_to)
|
||||
change_effective_from = parse_date(reassignment_effective_from)
|
||||
except (TypeError, ValueError) as exc:
|
||||
code = str(exc) if str(exc) else "validation"
|
||||
db.rollback()
|
||||
return RedirectResponse(
|
||||
f"/services/subscriptions/{plan.id}/edit?error={code}", status_code=303
|
||||
)
|
||||
|
||||
allowed_statuses = {item[0] for item in SUBSCRIPTION_STATUSES}
|
||||
normalized_status = (status or "active").strip().lower()
|
||||
if normalized_status not in allowed_statuses:
|
||||
return RedirectResponse(
|
||||
f"/services/subscriptions/{plan.id}/edit?error=status", status_code=303
|
||||
)
|
||||
if plan_effective_from and plan_effective_to and plan_effective_to < plan_effective_from:
|
||||
return RedirectResponse(
|
||||
f"/services/subscriptions/{plan.id}/edit?error=dates", status_code=303
|
||||
)
|
||||
if propagation_mode not in {"future_only", "open_engagements"}:
|
||||
return RedirectResponse(
|
||||
f"/services/subscriptions/{plan.id}/edit?error=propagation", status_code=303
|
||||
)
|
||||
|
||||
old_staff_id = plan.default_staff_user_id
|
||||
plan.default_partner_user_id = new_partner_id
|
||||
plan.default_performing_partner_user_id = new_performing_id
|
||||
plan.default_manager_user_id = new_manager_id
|
||||
plan.default_staff_user_id = new_staff_id
|
||||
plan.default_review_partner_user_id = new_review_partner_id
|
||||
plan.effective_from = plan_effective_from
|
||||
plan.effective_to = plan_effective_to
|
||||
plan.auto_generate_periods = auto_generate_periods is not None
|
||||
plan.status = normalized_status
|
||||
plan.is_active = normalized_status not in {"inactive", "cancelled"}
|
||||
plan.remarks = remarks.strip() or None
|
||||
plan.updated_by_user_id = user.id
|
||||
plan.updated_at_utc = datetime.now(timezone.utc)
|
||||
|
||||
updated_engagement_count = 0
|
||||
updated_task_count = 0
|
||||
if propagation_mode == "open_engagements":
|
||||
engagements = db.execute(
|
||||
select(ClientServiceSubscription).where(
|
||||
ClientServiceSubscription.service_plan_id == plan.id,
|
||||
ClientServiceSubscription.tenant_id == tenant_id,
|
||||
)
|
||||
).scalars().all()
|
||||
|
||||
affected_ids: list[int] = []
|
||||
for row in engagements:
|
||||
if not _engagement_is_open(row):
|
||||
continue
|
||||
if not _engagement_is_effective(row, change_effective_from):
|
||||
continue
|
||||
row.assigned_partner_user_id = new_partner_id
|
||||
row.performing_partner_user_id = new_performing_id
|
||||
row.assigned_manager_user_id = new_manager_id
|
||||
row.assigned_staff_user_id = new_staff_id
|
||||
row.review_partner_user_id = new_review_partner_id
|
||||
row.updated_by_user_id = user.id
|
||||
row.updated_at_utc = datetime.now(timezone.utc)
|
||||
if reassignment_reason.strip():
|
||||
note = (
|
||||
f"Subscription team updated on {datetime.now(timezone.utc).date().isoformat()} "
|
||||
f"by user {user.id}: {reassignment_reason.strip()}"
|
||||
)
|
||||
row.remarks = f"{row.remarks.strip()}\n{note}" if row.remarks else note
|
||||
affected_ids.append(row.id)
|
||||
updated_engagement_count += 1
|
||||
|
||||
if affected_ids and old_staff_id != new_staff_id:
|
||||
task_stmt = select(ClientServiceTaskInstance).where(
|
||||
ClientServiceTaskInstance.subscription_id.in_(affected_ids),
|
||||
ClientServiceTaskInstance.is_active.is_(True),
|
||||
ClientServiceTaskInstance.is_locked.is_(False),
|
||||
ClientServiceTaskInstance.status.notin_(("completed", "cancelled")),
|
||||
)
|
||||
if old_staff_id is None:
|
||||
task_stmt = task_stmt.where(
|
||||
ClientServiceTaskInstance.assigned_to_user_id.is_(None),
|
||||
func.lower(func.coalesce(ClientServiceTaskInstance.default_role_name, "")) == "staff",
|
||||
)
|
||||
else:
|
||||
task_stmt = task_stmt.where(
|
||||
ClientServiceTaskInstance.assigned_to_user_id == old_staff_id
|
||||
)
|
||||
tasks = db.execute(task_stmt).scalars().all()
|
||||
for task in tasks:
|
||||
task.assigned_to_user_id = new_staff_id
|
||||
task.updated_by_user_id = user.id
|
||||
task.updated_at_utc = datetime.now(timezone.utc)
|
||||
updated_task_count += 1
|
||||
|
||||
db.commit()
|
||||
return RedirectResponse(
|
||||
f"/services/subscriptions/{plan.id}/edit?saved=1"
|
||||
f"&updated_engagements={updated_engagement_count}"
|
||||
f"&updated_tasks={updated_task_count}",
|
||||
status_code=303,
|
||||
)
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/{plan_id}")
|
||||
def subscription_master_detail(request: Request, plan_id: int):
|
||||
db=CommonSessionLocal()
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user=get_current_user(request, db=db)
|
||||
if not user: return RedirectResponse("/login",303)
|
||||
try: require_permission(db,user,"clients.view")
|
||||
user = get_current_user(request, db=db)
|
||||
if not user:
|
||||
return RedirectResponse("/login", 303)
|
||||
try:
|
||||
require_permission(db, user, "clients.view")
|
||||
except Exception:
|
||||
from app.core.http_responses import ui_access_denied
|
||||
return ui_access_denied()
|
||||
tenant_id=_tenant_id(request,user)
|
||||
plan=db.execute(select(ClientServicePlan).options(selectinload(ClientServicePlan.client),selectinload(ClientServicePlan.catalogue),selectinload(ClientServicePlan.default_partner),selectinload(ClientServicePlan.default_performing_partner),selectinload(ClientServicePlan.default_manager),selectinload(ClientServicePlan.default_staff),selectinload(ClientServicePlan.default_review_partner)).where(ClientServicePlan.id==plan_id,ClientServicePlan.tenant_id==tenant_id)).scalar_one_or_none()
|
||||
if not plan: return RedirectResponse("/services/subscriptions",303)
|
||||
engagements=db.execute(select(ClientServiceSubscription).where(ClientServiceSubscription.service_plan_id==plan.id).order_by(ClientServiceSubscription.financial_year.desc(),ClientServiceSubscription.period_label.asc(),ClientServiceSubscription.id.desc())).scalars().all()
|
||||
return templates.TemplateResponse("modules/services/templates/services/subscriptions/detail.html",_ctx(request,db,user,title="Client Service Subscription",plan=plan,engagements=engagements))
|
||||
finally: db.close()
|
||||
return _denied()
|
||||
tenant_id = _tenant_id(request, user)
|
||||
branch_id = _branch_id(request, user)
|
||||
plan = _load_plan(db, plan_id=plan_id, tenant_id=tenant_id, branch_id=branch_id)
|
||||
if not plan:
|
||||
return RedirectResponse("/services/subscriptions", 303)
|
||||
engagements = db.execute(
|
||||
select(ClientServiceSubscription)
|
||||
.where(ClientServiceSubscription.service_plan_id == plan.id)
|
||||
.order_by(
|
||||
ClientServiceSubscription.financial_year.desc(),
|
||||
ClientServiceSubscription.period_label.asc(),
|
||||
ClientServiceSubscription.id.desc(),
|
||||
)
|
||||
).scalars().all()
|
||||
permissions = set(get_user_permissions(db, user.id))
|
||||
return templates.TemplateResponse(
|
||||
"modules/services/templates/services/subscriptions/detail.html",
|
||||
_ctx(
|
||||
request,
|
||||
db,
|
||||
user,
|
||||
title="Client Service Subscription",
|
||||
plan=plan,
|
||||
engagements=engagements,
|
||||
can_edit_subscription="clients.edit" in permissions,
|
||||
),
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
Reference in New Issue
Block a user