Enforce partner client ownership and partner-scoped imports

This commit is contained in:
A R R R Associates
2026-07-29 06:05:10 +05:30
parent 83f4cd2d4f
commit a58ec7b807
9 changed files with 205 additions and 108 deletions
+22 -4
View File
@@ -1,10 +1,11 @@
from __future__ import annotations from __future__ import annotations
from sqlalchemy import func, select from sqlalchemy import exists, func, or_, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.modules.client_groups.models import ClientGroup from app.modules.client_groups.models import ClientGroup
from app.modules.clients.models import Client from app.modules.clients.models import Client
from app.modules.services.models import ClientServiceSubscription
GROUP_TYPES = ("Family", "Business Group", "Promoter Group", "Trust Group", "Common Management", "Other") GROUP_TYPES = ("Family", "Business Group", "Promoter Group", "Trust Group", "Common Management", "Other")
@@ -13,7 +14,7 @@ def normalise_group_code(value: str | None) -> str:
return (value or "").strip().upper() return (value or "").strip().upper()
def list_groups(db: Session, *, tenant_id: int, include_inactive: bool = False): def list_groups(db: Session, *, tenant_id: int, include_inactive: bool = False, viewer_partner_id: int | None = None):
stmt = ( stmt = (
select(ClientGroup, func.count(Client.id).label("client_count")) select(ClientGroup, func.count(Client.id).label("client_count"))
.outerjoin(Client, Client.client_group_id == ClientGroup.id) .outerjoin(Client, Client.client_group_id == ClientGroup.id)
@@ -21,6 +22,14 @@ def list_groups(db: Session, *, tenant_id: int, include_inactive: bool = False):
.group_by(ClientGroup.id) .group_by(ClientGroup.id)
.order_by(ClientGroup.group_name.asc()) .order_by(ClientGroup.group_name.asc())
) )
if viewer_partner_id is not None:
review_access = exists(select(ClientServiceSubscription.id).where(
ClientServiceSubscription.tenant_id == tenant_id,
ClientServiceSubscription.client_id == Client.id,
ClientServiceSubscription.review_partner_user_id == viewer_partner_id,
ClientServiceSubscription.is_active.is_(True),
))
stmt = stmt.where(or_(Client.partner_id == viewer_partner_id, review_access))
if not include_inactive: if not include_inactive:
stmt = stmt.where(ClientGroup.is_active.is_(True)) stmt = stmt.where(ClientGroup.is_active.is_(True))
return [{"group": group, "client_count": int(count or 0)} for group, count in db.execute(stmt).all()] return [{"group": group, "client_count": int(count or 0)} for group, count in db.execute(stmt).all()]
@@ -37,8 +46,17 @@ def get_group_by_code(db: Session, *, tenant_id: int, group_code: str):
return db.execute(select(ClientGroup).where(ClientGroup.tenant_id == tenant_id, ClientGroup.group_code == code)).scalar_one_or_none() return db.execute(select(ClientGroup).where(ClientGroup.tenant_id == tenant_id, ClientGroup.group_code == code)).scalar_one_or_none()
def list_group_clients(db: Session, *, tenant_id: int, group_id: int): def list_group_clients(db: Session, *, tenant_id: int, group_id: int, viewer_partner_id: int | None = None):
return db.execute(select(Client).where(Client.tenant_id == tenant_id, Client.client_group_id == group_id, Client.is_archived.is_(False)).order_by(Client.is_group_head.desc(), Client.client_name.asc())).scalars().all() stmt = select(Client).where(Client.tenant_id == tenant_id, Client.client_group_id == group_id, Client.is_archived.is_(False))
if viewer_partner_id is not None:
review_access = exists(select(ClientServiceSubscription.id).where(
ClientServiceSubscription.tenant_id == tenant_id,
ClientServiceSubscription.client_id == Client.id,
ClientServiceSubscription.review_partner_user_id == viewer_partner_id,
ClientServiceSubscription.is_active.is_(True),
))
stmt = stmt.where(or_(Client.partner_id == viewer_partner_id, review_access))
return db.execute(stmt.order_by(Client.is_group_head.desc(), Client.client_name.asc())).scalars().all()
def create_group(db: Session, *, tenant_id: int, actor_user_id: int, payload: dict): def create_group(db: Session, *, tenant_id: int, actor_user_id: int, payload: dict):
+29 -15
View File
@@ -10,12 +10,20 @@ from app.core.templating import templates
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles 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.core.rbac.permission_guard import require_permission
from app.modules.client_groups.service import GROUP_TYPES, create_group, get_group, list_group_clients, list_groups, update_group from app.modules.client_groups.service import GROUP_TYPES, create_group, get_group, list_group_clients, list_groups, update_group
from app.modules.clients.access import build_scope from app.modules.clients.access import build_scope, enforce_partner_scope, is_partner_role
from app.modules.clients import repository from app.modules.clients import repository
from app.modules.consultants.service import list_consultants from app.modules.consultants.service import list_consultants
router = APIRouter(prefix="/client-groups", tags=["client-groups-ui"]) router = APIRouter(prefix="/client-groups", tags=["client-groups-ui"])
def _roles(db, user):
return {str(name).strip().lower() for name in get_user_roles(db, user.id)}
def _scope(request, user, db):
roles = _roles(db, user)
scope = build_scope(request, user, lambda code: code in set(get_user_permissions(db, user.id)))
return enforce_partner_scope(scope, user=user, role_names=roles), roles
def _ctx(request, user, db, **extra): def _ctx(request, user, db, **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)}; data.update(extra); return data
@@ -32,8 +40,8 @@ def groups_page(request: Request):
try: try:
user=get_current_user(request,db=db) user=get_current_user(request,db=db)
if not user: return RedirectResponse('/login',303) if not user: return RedirectResponse('/login',303)
require_permission(db,user,'clients.view'); scope=build_scope(request,user,lambda code: True) require_permission(db,user,'clients.view'); scope, roles=_scope(request,user,db)
return templates.TemplateResponse('modules/client_groups/templates/client_groups/list.html',_ctx(request,user,db,title='Client Groups',groups=list_groups(db,tenant_id=scope.tenant_id,include_inactive=True))) return templates.TemplateResponse('modules/client_groups/templates/client_groups/list.html',_ctx(request,user,db,title='Client Groups',groups=list_groups(db,tenant_id=scope.tenant_id,include_inactive=True,viewer_partner_id=user.id if is_partner_role(roles) else None)))
finally: db.close() finally: db.close()
@router.get("/new") @router.get("/new")
@@ -42,8 +50,8 @@ def group_new(request: Request):
try: try:
user=get_current_user(request,db=db) user=get_current_user(request,db=db)
if not user: return RedirectResponse('/login',303) if not user: return RedirectResponse('/login',303)
require_permission(db,user,'clients.create'); scope=build_scope(request,user,lambda code: True) require_permission(db,user,'clients.create'); scope, roles=_scope(request,user,db)
return templates.TemplateResponse('modules/client_groups/templates/client_groups/form.html',_ctx(request,user,db,title='Add Client Group',row=None,group_types=GROUP_TYPES,partners=repository.list_partners_for_scope(db,tenant_id=scope.tenant_id,branch_id=None),consultants=list_consultants(db,tenant_id=scope.tenant_id,include_inactive=False),errors=[])) return templates.TemplateResponse('modules/client_groups/templates/client_groups/form.html',_ctx(request,user,db,title='Add Client Group',row=None,group_types=GROUP_TYPES,partners=([repository.get_partner(db,user.id)] if is_partner_role(roles) else repository.list_partners_for_scope(db,tenant_id=scope.tenant_id,branch_id=None)),consultants=list_consultants(db,tenant_id=scope.tenant_id,include_inactive=False),errors=[]))
finally: db.close() finally: db.close()
@router.post("") @router.post("")
@@ -52,9 +60,12 @@ async def group_create(request: Request):
try: try:
user=get_current_user(request,db=db); form=await request.form(); validate_csrf(request,form.get('csrf_token')) user=get_current_user(request,db=db); form=await request.form(); validate_csrf(request,form.get('csrf_token'))
if not user: return RedirectResponse('/login',303) if not user: return RedirectResponse('/login',303)
require_permission(db,user,'clients.create'); scope=build_scope(request,user,lambda code: True) require_permission(db,user,'clients.create'); scope, roles=_scope(request,user,db)
try: row=create_group(db,tenant_id=scope.tenant_id,actor_user_id=user.id,payload=_payload(form)); return RedirectResponse(f'/client-groups/{row.id}',303) try:
except Exception as exc: return templates.TemplateResponse('modules/client_groups/templates/client_groups/form.html',_ctx(request,user,db,title='Add Client Group',row=_payload(form),group_types=GROUP_TYPES,partners=repository.list_partners_for_scope(db,tenant_id=scope.tenant_id,branch_id=None),consultants=list_consultants(db,tenant_id=scope.tenant_id,include_inactive=False),errors=[str(exc)]),status_code=400) payload=_payload(form)
if is_partner_role(roles): payload["assigned_partner_user_id"] = user.id
row=create_group(db,tenant_id=scope.tenant_id,actor_user_id=user.id,payload=payload); return RedirectResponse(f'/client-groups/{row.id}',303)
except Exception as exc: return templates.TemplateResponse('modules/client_groups/templates/client_groups/form.html',_ctx(request,user,db,title='Add Client Group',row=_payload(form),group_types=GROUP_TYPES,partners=([repository.get_partner(db,user.id)] if is_partner_role(roles) else repository.list_partners_for_scope(db,tenant_id=scope.tenant_id,branch_id=None)),consultants=list_consultants(db,tenant_id=scope.tenant_id,include_inactive=False),errors=[str(exc)]),status_code=400)
finally: db.close() finally: db.close()
@router.get("/{group_id}") @router.get("/{group_id}")
@@ -63,9 +74,9 @@ def group_detail(request: Request, group_id:int):
try: try:
user=get_current_user(request,db=db) user=get_current_user(request,db=db)
if not user: return RedirectResponse('/login',303) if not user: return RedirectResponse('/login',303)
require_permission(db,user,'clients.view'); scope=build_scope(request,user,lambda code: True); row=get_group(db,tenant_id=scope.tenant_id,group_id=group_id) require_permission(db,user,'clients.view'); scope, roles=_scope(request,user,db); row=get_group(db,tenant_id=scope.tenant_id,group_id=group_id)
if not row: return RedirectResponse('/client-groups',303) if not row: return RedirectResponse('/client-groups',303)
return templates.TemplateResponse('modules/client_groups/templates/client_groups/detail.html',_ctx(request,user,db,title=row.group_name,row=row,clients=list_group_clients(db,tenant_id=scope.tenant_id,group_id=row.id))) return templates.TemplateResponse('modules/client_groups/templates/client_groups/detail.html',_ctx(request,user,db,title=row.group_name,row=row,clients=list_group_clients(db,tenant_id=scope.tenant_id,group_id=row.id,viewer_partner_id=user.id if is_partner_role(roles) else None)))
finally: db.close() finally: db.close()
@router.get("/{group_id}/edit") @router.get("/{group_id}/edit")
@@ -74,9 +85,9 @@ def group_edit(request: Request, group_id:int):
try: try:
user=get_current_user(request,db=db) user=get_current_user(request,db=db)
if not user: return RedirectResponse('/login',303) if not user: return RedirectResponse('/login',303)
require_permission(db,user,'clients.edit'); scope=build_scope(request,user,lambda code: True); row=get_group(db,tenant_id=scope.tenant_id,group_id=group_id) require_permission(db,user,'clients.edit'); scope, roles=_scope(request,user,db); row=get_group(db,tenant_id=scope.tenant_id,group_id=group_id)
if not row: return RedirectResponse('/client-groups',303) if not row: return RedirectResponse('/client-groups',303)
return templates.TemplateResponse('modules/client_groups/templates/client_groups/form.html',_ctx(request,user,db,title='Edit Client Group',row=row,group_types=GROUP_TYPES,partners=repository.list_partners_for_scope(db,tenant_id=scope.tenant_id,branch_id=None),consultants=list_consultants(db,tenant_id=scope.tenant_id,include_inactive=False),errors=[])) return templates.TemplateResponse('modules/client_groups/templates/client_groups/form.html',_ctx(request,user,db,title='Edit Client Group',row=row,group_types=GROUP_TYPES,partners=([repository.get_partner(db,user.id)] if is_partner_role(roles) else repository.list_partners_for_scope(db,tenant_id=scope.tenant_id,branch_id=None)),consultants=list_consultants(db,tenant_id=scope.tenant_id,include_inactive=False),errors=[]))
finally: db.close() finally: db.close()
@router.post("/{group_id}/edit") @router.post("/{group_id}/edit")
@@ -85,8 +96,11 @@ async def group_update(request: Request, group_id:int):
try: try:
user=get_current_user(request,db=db); form=await request.form(); validate_csrf(request,form.get('csrf_token')) user=get_current_user(request,db=db); form=await request.form(); validate_csrf(request,form.get('csrf_token'))
if not user: return RedirectResponse('/login',303) if not user: return RedirectResponse('/login',303)
require_permission(db,user,'clients.edit'); scope=build_scope(request,user,lambda code: True); row=get_group(db,tenant_id=scope.tenant_id,group_id=group_id) require_permission(db,user,'clients.edit'); scope, roles=_scope(request,user,db); row=get_group(db,tenant_id=scope.tenant_id,group_id=group_id)
if not row: return RedirectResponse('/client-groups',303) if not row: return RedirectResponse('/client-groups',303)
try: update_group(db,row=row,actor_user_id=user.id,payload=_payload(form)); return RedirectResponse(f'/client-groups/{row.id}',303) try:
except Exception as exc: return templates.TemplateResponse('modules/client_groups/templates/client_groups/form.html',_ctx(request,user,db,title='Edit Client Group',row=row,group_types=GROUP_TYPES,partners=repository.list_partners_for_scope(db,tenant_id=scope.tenant_id,branch_id=None),consultants=list_consultants(db,tenant_id=scope.tenant_id,include_inactive=False),errors=[str(exc)]),status_code=400) payload=_payload(form)
if is_partner_role(roles): payload["assigned_partner_user_id"] = user.id
update_group(db,row=row,actor_user_id=user.id,payload=payload); return RedirectResponse(f'/client-groups/{row.id}',303)
except Exception as exc: return templates.TemplateResponse('modules/client_groups/templates/client_groups/form.html',_ctx(request,user,db,title='Edit Client Group',row=row,group_types=GROUP_TYPES,partners=([repository.get_partner(db,user.id)] if is_partner_role(roles) else repository.list_partners_for_scope(db,tenant_id=scope.tenant_id,branch_id=None)),consultants=list_consultants(db,tenant_id=scope.tenant_id,include_inactive=False),errors=[str(exc)]),status_code=400)
finally: db.close() finally: db.close()
+3 -15
View File
@@ -1,29 +1,17 @@
"""Clients module. """Clients module with lazy router exports.
Routers are exposed lazily so importing ``app.modules.clients.models`` does Importing client models must not initialise the complete UI/import stack.
not initialise the clients API, UI, importer, and related modules. This keeps
the existing ``api_router`` and ``ui_router`` public exports while preventing
circular imports with client groups.
""" """
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
__all__ = ["api_router", "ui_router"] __all__ = ["api_router", "ui_router"]
def __getattr__(name: str) -> Any: def __getattr__(name: str) -> Any:
if name == "api_router": if name == "api_router":
from .api import router from .api import router
return router return router
if name == "ui_router": if name == "ui_router":
from .ui import router from .ui import router
return router return router
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
raise AttributeError(
f"module {__name__!r} has no attribute {name!r}"
)
+16 -1
View File
@@ -47,6 +47,21 @@ def build_scope(request, user, permission_checker):
) )
def is_partner_role(role_names) -> bool:
names = {str(name).strip().lower() for name in (role_names or [])}
return "partner" in names and "firm admin" not in names and "system admin" not in names
def enforce_partner_scope(scope: ClientAccessScope, *, user, role_names) -> ClientAccessScope:
if is_partner_role(role_names):
scope.allow_all_clients = False
scope.own_only = True
scope.locked_partner_id = int(user.id)
scope.can_assign_partner = False
return scope
def effective_partner_id(row: dict): def effective_partner_id(row: dict):
return row.get("assoc_partner_user_id") or row.get("partner_id") return row.get("assoc_partner_user_id") or row.get("partner_id")
@@ -71,6 +86,6 @@ def can_view_client_row(scope: ClientAccessScope, row: dict, *, user_id: int) ->
return False return False
if scope.own_only and scope.locked_partner_id and effective_partner_id(row) != scope.locked_partner_id: if scope.own_only and scope.locked_partner_id and effective_partner_id(row) != scope.locked_partner_id:
return False return bool(row.get("has_review_access"))
return True return True
+16 -13
View File
@@ -7,7 +7,7 @@ from sqlalchemy.orm import Session
from app.core.db.deps import get_common_db from app.core.db.deps import get_common_db
from app.core.security.session_auth import require_login from app.core.security.session_auth import require_login
from app.modules.clients.access import ClientAccessScope from app.modules.clients.access import ClientAccessScope, enforce_partner_scope, is_partner_role
from app.modules.clients.schemas import ClientAuditLogOut, ClientFilterOptions, ClientListResponse, ClientOut, ClientUpdate, ClientCreate from app.modules.clients.schemas import ClientAuditLogOut, ClientFilterOptions, ClientListResponse, ClientOut, ClientUpdate, ClientCreate
from app.modules.clients.service import ( from app.modules.clients.service import (
activate_client_service, activate_client_service,
@@ -23,6 +23,7 @@ from app.modules.clients.service import (
update_client_service, update_client_service,
) )
from app.modules.core.rbac.permission_guard import require_permission from app.modules.core.rbac.permission_guard import require_permission
from app.modules.core.rbac.deps import get_user_roles
router = APIRouter(prefix="/api/v1/clients", tags=["clients-api"]) router = APIRouter(prefix="/api/v1/clients", tags=["clients-api"])
@@ -34,17 +35,19 @@ def _api_scope_from_user(db, user):
except Exception: except Exception:
return False return False
own_only = has("clients.view.own_only") or not has("clients.assign_partner") own_only = has("clients.view.own_only") or not has("clients.assign_partner")
return ClientAccessScope( scope = ClientAccessScope(
tenant_id=user.tenant_id, tenant_id=user.tenant_id,
branch_id=user.branch_id, branch_id=user.branch_id,
allow_cross_branch=has("clients.cross_branch"), allow_cross_branch=has("clients.cross_branch"),
allow_cross_tenant=has("clients.cross_tenant"), allow_cross_tenant=has("clients.cross_tenant"),
allow_all_clients=has("clients.view.all") and not own_only,
own_only=own_only, own_only=own_only,
locked_partner_id=user.id if own_only else None, locked_partner_id=user.id if own_only else None,
can_assign_partner=has("clients.assign_partner"), can_assign_partner=has("clients.assign_partner"),
can_change_branch=has("clients.cross_branch"), can_change_branch=has("clients.cross_branch"),
can_change_tenant=has("clients.cross_tenant"), can_change_tenant=has("clients.cross_tenant"),
) )
return enforce_partner_scope(scope, user=user, role_names=get_user_roles(db, user.id))
@router.get("/filters", response_model=ClientFilterOptions) @router.get("/filters", response_model=ClientFilterOptions)
def api_client_filters(): def api_client_filters():
@@ -73,7 +76,8 @@ def api_list_clients(
tenant_id=scope.tenant_id, tenant_id=scope.tenant_id,
branch_id=scope.branch_id, branch_id=scope.branch_id,
allow_cross_branch=scope.allow_cross_branch, allow_cross_branch=scope.allow_cross_branch,
partner_id=partner_id, partner_id=None if scope.own_only else partner_id,
viewer_partner_id=user.id if scope.own_only else None,
q=q, q=q,
status=status, status=status,
client_type=client_type, client_type=client_type,
@@ -105,7 +109,8 @@ def api_export_clients(
tenant_id=scope.tenant_id, tenant_id=scope.tenant_id,
branch_id=scope.branch_id, branch_id=scope.branch_id,
allow_cross_branch=scope.allow_cross_branch, allow_cross_branch=scope.allow_cross_branch,
partner_id=partner_id, partner_id=None if scope.own_only else partner_id,
viewer_partner_id=user.id if scope.own_only else None,
q=q, q=q,
status=status, status=status,
client_type=client_type, client_type=client_type,
@@ -122,9 +127,7 @@ def api_export_clients(
def api_get_client(client_id: int, db: Session = Depends(get_common_db), user=Depends(require_login)): def api_get_client(client_id: int, db: Session = Depends(get_common_db), user=Depends(require_login)):
require_permission(db, user, "clients.view") require_permission(db, user, "clients.view")
scope = _api_scope_from_user(db, user) scope = _api_scope_from_user(db, user)
row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch) row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients, viewer_partner_id=user.id if scope.own_only else None, allow_review_access=True)
if scope.own_only and row.partner_id != scope.locked_partner_id:
raise HTTPException(status_code=404, detail="Client not found.")
return row return row
@router.post("", response_model=ClientOut, status_code=201) @router.post("", response_model=ClientOut, status_code=201)
@@ -137,7 +140,7 @@ def api_create_client(data: ClientCreate, db: Session = Depends(get_common_db),
def api_update_client(client_id: int, data: ClientUpdate, db: Session = Depends(get_common_db), user=Depends(require_login)): def api_update_client(client_id: int, data: ClientUpdate, db: Session = Depends(get_common_db), user=Depends(require_login)):
require_permission(db, user, "clients.edit") require_permission(db, user, "clients.edit")
scope = _api_scope_from_user(db, user) scope = _api_scope_from_user(db, user)
row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch) row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients, viewer_partner_id=user.id if scope.own_only else None)
if scope.own_only and row.partner_id != scope.locked_partner_id: if scope.own_only and row.partner_id != scope.locked_partner_id:
raise HTTPException(status_code=404, detail="Client not found.") raise HTTPException(status_code=404, detail="Client not found.")
return update_client_service(db, row=row, data=data, actor_user_id=user.id, scope=scope) return update_client_service(db, row=row, data=data, actor_user_id=user.id, scope=scope)
@@ -146,33 +149,33 @@ def api_update_client(client_id: int, data: ClientUpdate, db: Session = Depends(
def api_deactivate_client(client_id: int, db: Session = Depends(get_common_db), user=Depends(require_login)): def api_deactivate_client(client_id: int, db: Session = Depends(get_common_db), user=Depends(require_login)):
require_permission(db, user, "clients.deactivate") require_permission(db, user, "clients.deactivate")
scope = _api_scope_from_user(db, user) scope = _api_scope_from_user(db, user)
row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch) row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients, viewer_partner_id=user.id if scope.own_only else None)
return deactivate_client_service(db, row=row, actor_user_id=user.id) return deactivate_client_service(db, row=row, actor_user_id=user.id)
@router.post("/{client_id}/activate", response_model=ClientOut) @router.post("/{client_id}/activate", response_model=ClientOut)
def api_activate_client(client_id: int, db: Session = Depends(get_common_db), user=Depends(require_login)): def api_activate_client(client_id: int, db: Session = Depends(get_common_db), user=Depends(require_login)):
require_permission(db, user, "clients.activate") require_permission(db, user, "clients.activate")
scope = _api_scope_from_user(db, user) scope = _api_scope_from_user(db, user)
row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch) row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients, viewer_partner_id=user.id if scope.own_only else None)
return activate_client_service(db, row=row, actor_user_id=user.id) return activate_client_service(db, row=row, actor_user_id=user.id)
@router.post("/{client_id}/archive", response_model=ClientOut) @router.post("/{client_id}/archive", response_model=ClientOut)
def api_archive_client(client_id: int, db: Session = Depends(get_common_db), user=Depends(require_login)): def api_archive_client(client_id: int, db: Session = Depends(get_common_db), user=Depends(require_login)):
require_permission(db, user, "clients.archive") require_permission(db, user, "clients.archive")
scope = _api_scope_from_user(db, user) scope = _api_scope_from_user(db, user)
row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch) row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients, viewer_partner_id=user.id if scope.own_only else None)
return archive_client_service(db, row=row, actor_user_id=user.id) return archive_client_service(db, row=row, actor_user_id=user.id)
@router.post("/{client_id}/restore", response_model=ClientOut) @router.post("/{client_id}/restore", response_model=ClientOut)
def api_restore_client(client_id: int, db: Session = Depends(get_common_db), user=Depends(require_login)): def api_restore_client(client_id: int, db: Session = Depends(get_common_db), user=Depends(require_login)):
require_permission(db, user, "clients.restore") require_permission(db, user, "clients.restore")
scope = _api_scope_from_user(db, user) scope = _api_scope_from_user(db, user)
row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch) row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients, viewer_partner_id=user.id if scope.own_only else None)
return restore_client_service(db, row=row, actor_user_id=user.id) return restore_client_service(db, row=row, actor_user_id=user.id)
@router.get("/{client_id}/audit-logs", response_model=list[ClientAuditLogOut]) @router.get("/{client_id}/audit-logs", response_model=list[ClientAuditLogOut])
def api_client_audit_logs(client_id: int, limit: int = Query(50, ge=1, le=200), db: Session = Depends(get_common_db), user=Depends(require_login)): def api_client_audit_logs(client_id: int, limit: int = Query(50, ge=1, le=200), db: Session = Depends(get_common_db), user=Depends(require_login)):
require_permission(db, user, "clients.audit_log.view") require_permission(db, user, "clients.audit_log.view")
scope = _api_scope_from_user(db, user) scope = _api_scope_from_user(db, user)
row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch) row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients, viewer_partner_id=user.id if scope.own_only else None)
return list_client_audit_logs(db, row=row, limit=limit) return list_client_audit_logs(db, row=row, limit=limit)
+48 -8
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from math import ceil from math import ceil
from sqlalchemy import asc, case, desc, func, or_, select from sqlalchemy import and_, asc, case, desc, exists, func, or_, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.core.security.passwords import hash_password from app.core.security.passwords import hash_password
@@ -13,6 +13,7 @@ from app.modules.client_groups.models import ClientGroup
from app.modules.core.iam.models import User from app.modules.core.iam.models import User
from app.modules.core.rbac.models import Role, UserRole from app.modules.core.rbac.models import Role, UserRole
from app.modules.core.tenancy.models import Branch, Tenant from app.modules.core.tenancy.models import Branch, Tenant
from app.modules.services.models import ClientServiceSubscription
def _safe_sort(sort_by: str, sort_order: str): def _safe_sort(sort_by: str, sort_order: str):
@@ -28,6 +29,7 @@ def build_clients_query(
allow_cross_branch: bool = False, allow_cross_branch: bool = False,
allow_all_clients: bool = False, allow_all_clients: bool = False,
partner_id: int | None = None, partner_id: int | None = None,
viewer_partner_id: int | None = None,
q: str = "", q: str = "",
status: str = "", status: str = "",
client_type: str = "", client_type: str = "",
@@ -35,6 +37,14 @@ def build_clients_query(
include_archived: bool = False, include_archived: bool = False,
): ):
assoc = ClientAssociation assoc = ClientAssociation
review_access = exists(
select(ClientServiceSubscription.id).where(
ClientServiceSubscription.tenant_id == tenant_id,
ClientServiceSubscription.client_id == Client.id,
ClientServiceSubscription.review_partner_user_id == viewer_partner_id,
ClientServiceSubscription.is_active.is_(True),
)
) if viewer_partner_id is not None else None
stmt = ( stmt = (
select( select(
Client, Client,
@@ -48,6 +58,7 @@ def build_clients_query(
assoc.consultant_id.label("assoc_consultant_id"), assoc.consultant_id.label("assoc_consultant_id"),
assoc.partner_user_id.label("assoc_partner_user_id"), assoc.partner_user_id.label("assoc_partner_user_id"),
assoc.created_source.label("assoc_created_source"), assoc.created_source.label("assoc_created_source"),
(review_access if review_access is not None else False).label("has_review_access"),
) )
.outerjoin(assoc, assoc.client_id == Client.id) .outerjoin(assoc, assoc.client_id == Client.id)
.outerjoin(ClientGroup, ClientGroup.id == Client.client_group_id) .outerjoin(ClientGroup, ClientGroup.id == Client.client_group_id)
@@ -61,7 +72,11 @@ def build_clients_query(
stmt = stmt.where(Client.is_archived.is_(False)) stmt = stmt.where(Client.is_archived.is_(False))
if branch_id and not allow_all_clients and not allow_cross_branch: if branch_id and not allow_all_clients and not allow_cross_branch:
stmt = stmt.where(Client.branch_id == branch_id) stmt = stmt.where(Client.branch_id == branch_id)
if partner_id: if viewer_partner_id is not None:
stmt = stmt.where(
or_(Client.partner_id == viewer_partner_id, assoc.partner_user_id == viewer_partner_id, review_access)
)
elif partner_id:
stmt = stmt.where((Client.partner_id == partner_id) | (assoc.partner_user_id == partner_id)) stmt = stmt.where((Client.partner_id == partner_id) | (assoc.partner_user_id == partner_id))
if status: if status:
stmt = stmt.where(Client.status == status) stmt = stmt.where(Client.status == status)
@@ -82,13 +97,13 @@ def build_clients_query(
def list_clients( def list_clients(
db: Session, *, tenant_id: int, branch_id: int | None = None, db: Session, *, tenant_id: int, branch_id: int | None = None,
allow_cross_branch: bool = False, allow_all_clients: bool = False, allow_cross_branch: bool = False, allow_all_clients: bool = False,
partner_id: int | None = None, q: str = "", status: str = "", client_type: str = "", partner_id: int | None = None, viewer_partner_id: int | None = None, q: str = "", status: str = "", client_type: str = "",
client_group_id: int | None = None, include_archived: bool = False, client_group_id: int | None = None, include_archived: bool = False,
page: int = 1, per_page: int = 25, sort_by: str = "client_name", sort_order: str = "asc", page: int = 1, per_page: int = 25, sort_by: str = "client_name", sort_order: str = "asc",
) -> dict: ) -> dict:
stmt = build_clients_query( stmt = build_clients_query(
tenant_id=tenant_id, branch_id=branch_id, allow_cross_branch=allow_cross_branch, tenant_id=tenant_id, branch_id=branch_id, allow_cross_branch=allow_cross_branch,
allow_all_clients=allow_all_clients, partner_id=partner_id, q=q, status=status, allow_all_clients=allow_all_clients, partner_id=partner_id, viewer_partner_id=viewer_partner_id, q=q, status=status,
client_type=client_type, client_group_id=client_group_id, include_archived=include_archived, client_type=client_type, client_group_id=client_group_id, include_archived=include_archived,
) )
total = db.execute(select(func.count()).select_from(stmt.subquery())).scalar_one() total = db.execute(select(func.count()).select_from(stmt.subquery())).scalar_one()
@@ -100,7 +115,7 @@ def list_clients(
stmt.order_by(_safe_sort(sort_by, sort_order)).offset(offset).limit(per_page) stmt.order_by(_safe_sort(sort_by, sort_order)).offset(offset).limit(per_page)
).all() ).all()
rows=[] rows=[]
for client, partner_name, branch_name, tenant_name, client_group_name, client_group_code, association_type, assoc_firm_tenant_id, assoc_consultant_id, assoc_partner_user_id, assoc_created_source in result: for client, partner_name, branch_name, tenant_name, client_group_name, client_group_code, association_type, assoc_firm_tenant_id, assoc_consultant_id, assoc_partner_user_id, assoc_created_source, has_review_access in result:
row={**client.__dict__}; row.pop("_sa_instance_state",None) row={**client.__dict__}; row.pop("_sa_instance_state",None)
row.update({ row.update({
"partner_name":partner_name,"branch_name":branch_name,"tenant_name":tenant_name, "partner_name":partner_name,"branch_name":branch_name,"tenant_name":tenant_name,
@@ -108,17 +123,39 @@ def list_clients(
"association_type":association_type,"assoc_firm_tenant_id":assoc_firm_tenant_id, "association_type":association_type,"assoc_firm_tenant_id":assoc_firm_tenant_id,
"assoc_consultant_id":assoc_consultant_id,"assoc_partner_user_id":assoc_partner_user_id, "assoc_consultant_id":assoc_consultant_id,"assoc_partner_user_id":assoc_partner_user_id,
"assoc_created_source":assoc_created_source,"effective_partner_id":assoc_partner_user_id or row.get("partner_id"), "assoc_created_source":assoc_created_source,"effective_partner_id":assoc_partner_user_id or row.get("partner_id"),
"has_review_access": bool(has_review_access),
}); rows.append(row) }); rows.append(row)
stats_stmt=select(func.count(Client.id),func.sum(case((Client.status=="active",1),else_=0)),func.sum(case((Client.status=="inactive",1),else_=0)),func.sum(case((Client.status=="archived",1),else_=0))) stats_stmt=select(func.count(Client.id),func.sum(case((Client.status=="active",1),else_=0)),func.sum(case((Client.status=="inactive",1),else_=0)),func.sum(case((Client.status=="archived",1),else_=0)))
if not allow_all_clients: if not allow_all_clients:
stats_stmt=stats_stmt.where(Client.tenant_id==tenant_id) stats_stmt=stats_stmt.where(Client.tenant_id==tenant_id)
if branch_id and not allow_cross_branch: stats_stmt=stats_stmt.where(Client.branch_id==branch_id) if branch_id and not allow_cross_branch: stats_stmt=stats_stmt.where(Client.branch_id==branch_id)
if partner_id: stats_stmt=stats_stmt.where(Client.partner_id==partner_id) if viewer_partner_id is not None:
review_stats = exists(select(ClientServiceSubscription.id).where(
ClientServiceSubscription.tenant_id == tenant_id,
ClientServiceSubscription.client_id == Client.id,
ClientServiceSubscription.review_partner_user_id == viewer_partner_id,
ClientServiceSubscription.is_active.is_(True),
))
stats_stmt = stats_stmt.where(or_(Client.partner_id == viewer_partner_id, review_stats))
elif partner_id:
stats_stmt=stats_stmt.where(Client.partner_id==partner_id)
total_all,active,inactive,archived=db.execute(stats_stmt).one() total_all,active,inactive,archived=db.execute(stats_stmt).one()
return {"rows":rows,"meta":{"total":total,"page":page,"per_page":per_page,"pages":pages},"stats":{"total":int(total_all or 0),"active":int(active or 0),"inactive":int(inactive or 0),"archived":int(archived or 0)}} return {"rows":rows,"meta":{"total":total,"page":page,"per_page":per_page,"pages":pages},"stats":{"total":int(total_all or 0),"active":int(active or 0),"inactive":int(inactive or 0),"archived":int(archived or 0)}}
def get_client_detail_payload(db: Session, client_id: int):
def has_partner_review_access(db: Session, *, tenant_id: int, client_id: int, partner_user_id: int) -> bool:
return bool(db.execute(
select(ClientServiceSubscription.id).where(
ClientServiceSubscription.tenant_id == tenant_id,
ClientServiceSubscription.client_id == client_id,
ClientServiceSubscription.review_partner_user_id == partner_user_id,
ClientServiceSubscription.is_active.is_(True),
).limit(1)
).scalar_one_or_none())
def get_client_detail_payload(db: Session, client_id: int, *, viewer_partner_id: int | None = None):
assoc=ClientAssociation assoc=ClientAssociation
stmt=(select( stmt=(select(
Client, ClientGroup.group_name.label("client_group_name"), ClientGroup.group_code.label("client_group_code"), Client, ClientGroup.group_name.label("client_group_name"), ClientGroup.group_code.label("client_group_code"),
@@ -133,7 +170,10 @@ def get_client_detail_payload(db: Session, client_id: int):
row.update({"client_group_name":client_group_name,"client_group_code":client_group_code,"association_type":association_type, row.update({"client_group_name":client_group_name,"client_group_code":client_group_code,"association_type":association_type,
"assoc_firm_tenant_id":assoc_firm_tenant_id,"assoc_consultant_id":assoc_consultant_id, "assoc_firm_tenant_id":assoc_firm_tenant_id,"assoc_consultant_id":assoc_consultant_id,
"assoc_partner_user_id":assoc_partner_user_id,"assoc_created_source":assoc_created_source, "assoc_partner_user_id":assoc_partner_user_id,"assoc_created_source":assoc_created_source,
"effective_partner_id":assoc_partner_user_id or row.get("partner_id")}) "effective_partner_id":assoc_partner_user_id or row.get("partner_id"),
"has_review_access": bool(viewer_partner_id is not None and has_partner_review_access(
db, tenant_id=int(row["tenant_id"]), client_id=int(row["id"]), partner_user_id=int(viewer_partner_id)
))})
return row return row
+9 -3
View File
@@ -30,16 +30,15 @@ from app.modules.core.iam.models import User
from app.modules.core.rbac.models import Role, UserRole from app.modules.core.rbac.models import Role, UserRole
from app.modules.documents.models import PermanentClientDocument from app.modules.documents.models import PermanentClientDocument
from app.modules.consultants.service import sync_primary_client_consultant_link from app.modules.consultants.service import sync_primary_client_consultant_link
from app.modules.clients.models import Client from app.modules.clients.models import Client
def _get_client_group(*args, **kwargs): def _get_client_group(*args, **kwargs):
# Imported lazily to prevent clients ↔ client_groups circular imports.
from app.modules.client_groups.service import get_group from app.modules.client_groups.service import get_group
return get_group(*args, **kwargs) return get_group(*args, **kwargs)
def _payload_from_schema(data): def _payload_from_schema(data):
payload = data.model_dump(exclude_none=True) if hasattr(data, "model_dump") else data.dict(exclude_none=True) payload = data.model_dump(exclude_none=True) if hasattr(data, "model_dump") else data.dict(exclude_none=True)
# primary_consultant_id belongs to ClientConsultantLink, not the clients table. # primary_consultant_id belongs to ClientConsultantLink, not the clients table.
@@ -367,6 +366,8 @@ def get_client_or_404(
branch_id: int | None, branch_id: int | None,
allow_cross_branch: bool, allow_cross_branch: bool,
allow_all_clients: bool = False, allow_all_clients: bool = False,
viewer_partner_id: int | None = None,
allow_review_access: bool = False,
): ):
row = repository.get_client_by_id(db, client_id) row = repository.get_client_by_id(db, client_id)
if not row: if not row:
@@ -375,6 +376,11 @@ def get_client_or_404(
raise HTTPException(status_code=404, detail="Client not found in current tenant.") raise HTTPException(status_code=404, detail="Client not found in current tenant.")
if not allow_all_clients and not allow_cross_branch and branch_id and row.branch_id != branch_id: if not allow_all_clients and not allow_cross_branch and branch_id and row.branch_id != branch_id:
raise HTTPException(status_code=404, detail="Client not found in current branch.") raise HTTPException(status_code=404, detail="Client not found in current branch.")
if viewer_partner_id is not None and int(row.partner_id or 0) != int(viewer_partner_id):
if not allow_review_access or not repository.has_partner_review_access(
db, tenant_id=tenant_id, client_id=client_id, partner_user_id=viewer_partner_id
):
raise HTTPException(status_code=404, detail="Client not found.")
return row return row
@@ -51,12 +51,8 @@
<input type="hidden" name="q" value="{{ q }}"> <input type="hidden" name="q" value="{{ q }}">
<input type="hidden" name="status" value="{{ status }}"> <input type="hidden" name="status" value="{{ status }}">
<input type="hidden" name="client_type" value="{{ client_type }}"> <input type="hidden" name="client_type" value="{{ client_type }}">
{% if client_group_id %} {% if client_group_id %}<input type="hidden" name="client_group_id" value="{{ client_group_id }}">{% endif %}
<input type="hidden" name="client_group_id" value="{{ client_group_id }}"> {% if partner_id %}<input type="hidden" name="partner_id" value="{{ partner_id }}">{% endif %}
{% endif %}
{% if partner_id %}
<input type="hidden" name="partner_id" value="{{ partner_id }}">
{% endif %}
<input type="hidden" name="include_archived" value="{{ 'true' if include_archived else 'false' }}"> <input type="hidden" name="include_archived" value="{{ 'true' if include_archived else 'false' }}">
<input type="hidden" name="sort_by" value="{{ sort_by }}"> <input type="hidden" name="sort_by" value="{{ sort_by }}">
<input type="hidden" name="sort_order" value="{{ sort_order }}"> <input type="hidden" name="sort_order" value="{{ sort_order }}">
@@ -73,12 +69,8 @@
{% if meta.pages > 1 %} {% if meta.pages > 1 %}
<nav class="flex items-center gap-1" aria-label="Client list pagination"> <nav class="flex items-center gap-1" aria-label="Client list pagination">
{% set query = namespace(value='q=' ~ (q|urlencode) ~ '&status=' ~ (status|urlencode) ~ '&client_type=' ~ (client_type|urlencode) ~ '&include_archived=' ~ ('true' if include_archived else 'false') ~ '&per_page=' ~ meta.per_page ~ '&sort_by=' ~ (sort_by|urlencode) ~ '&sort_order=' ~ (sort_order|urlencode)) %} {% set query = namespace(value='q=' ~ (q|urlencode) ~ '&status=' ~ (status|urlencode) ~ '&client_type=' ~ (client_type|urlencode) ~ '&include_archived=' ~ ('true' if include_archived else 'false') ~ '&per_page=' ~ meta.per_page ~ '&sort_by=' ~ (sort_by|urlencode) ~ '&sort_order=' ~ (sort_order|urlencode)) %}
{% if client_group_id %} {% if client_group_id %}{% set query.value = query.value ~ '&client_group_id=' ~ client_group_id %}{% endif %}
{% set query.value = query.value ~ '&client_group_id=' ~ client_group_id %} {% if partner_id %}{% set query.value = query.value ~ '&partner_id=' ~ partner_id %}{% endif %}
{% endif %}
{% if partner_id %}
{% set query.value = query.value ~ '&partner_id=' ~ partner_id %}
{% endif %}
{% set common_query = query.value %} {% set common_query = query.value %}
{% if meta.page > 1 %} {% if meta.page > 1 %}
+58 -37
View File
@@ -11,7 +11,7 @@ from app.core.security.session_auth import get_current_user
from app.core.security.otp import start_otp, verify_otp from app.core.security.otp import start_otp, verify_otp
from app.core.templating import templates from app.core.templating import templates
from app.modules.clients import repository from app.modules.clients import repository
from app.modules.clients.access import build_scope, can_view_client_row from app.modules.clients.access import build_scope, can_view_client_row, enforce_partner_scope, is_partner_role
from app.modules.clients.constants import CLIENT_ACCEPTANCE_STATUS, CLIENT_CATEGORY_OPTIONS, CLIENT_STATUS, CLIENT_TYPES, RISK_CATEGORIES from app.modules.clients.constants import CLIENT_ACCEPTANCE_STATUS, CLIENT_CATEGORY_OPTIONS, CLIENT_STATUS, CLIENT_TYPES, RISK_CATEGORIES
from app.modules.clients.filters import ClientListFilters from app.modules.clients.filters import ClientListFilters
from app.modules.clients.import_service import ( from app.modules.clients.import_service import (
@@ -150,6 +150,15 @@ def _elevate_scope_for_system_admin(scope, role_names: set[str]):
return scope return scope
def _apply_role_scope(scope, user, role_names):
scope = _elevate_scope_for_system_admin(scope, role_names)
return enforce_partner_scope(scope, user=user, role_names=role_names)
def _viewer_partner_id(user, role_names):
return int(user.id) if is_partner_role(role_names) else None
def _form_bool(value): def _form_bool(value):
return value in ("1", "true", "True", "on", "yes") return value in ("1", "true", "True", "on", "yes")
@@ -313,7 +322,7 @@ def clients_list(
role_names = _role_names(db, user) role_names = _role_names(db, user)
scope = build_scope(request, user, has) scope = build_scope(request, user, has)
scope = _elevate_scope_for_system_admin(scope, role_names) scope = _apply_role_scope(scope, user, role_names)
filters = ClientListFilters.from_params( filters = ClientListFilters.from_params(
q=q, q=q,
@@ -336,7 +345,8 @@ def clients_list(
branch_id=scope.branch_id, branch_id=scope.branch_id,
allow_cross_branch=scope.allow_cross_branch, allow_cross_branch=scope.allow_cross_branch,
allow_all_clients=scope.allow_all_clients, allow_all_clients=scope.allow_all_clients,
partner_id=filters.partner_id, partner_id=None if is_partner_role(role_names) else filters.partner_id,
viewer_partner_id=_viewer_partner_id(user, role_names),
q=filters.q, q=filters.q,
status=filters.status, status=filters.status,
client_type=filters.client_type, client_type=filters.client_type,
@@ -400,7 +410,7 @@ def clients_export(
role_names = _role_names(db, user) role_names = _role_names(db, user)
scope = build_scope(request, user, has) scope = build_scope(request, user, has)
scope = _elevate_scope_for_system_admin(scope, role_names) scope = _apply_role_scope(scope, user, role_names)
filters = ClientListFilters.from_params( filters = ClientListFilters.from_params(
q=q, q=q,
@@ -422,7 +432,8 @@ def clients_export(
branch_id=scope.branch_id, branch_id=scope.branch_id,
allow_cross_branch=scope.allow_cross_branch, allow_cross_branch=scope.allow_cross_branch,
allow_all_clients=scope.allow_all_clients, allow_all_clients=scope.allow_all_clients,
partner_id=filters.partner_id, partner_id=None if is_partner_role(role_names) else filters.partner_id,
viewer_partner_id=_viewer_partner_id(user, role_names),
q=filters.q, q=filters.q,
status=filters.status, status=filters.status,
client_type=filters.client_type, client_type=filters.client_type,
@@ -458,7 +469,7 @@ def client_import_page(request: Request):
role_names = _role_names(db, user) role_names = _role_names(db, user)
scope = build_scope(request, user, has) scope = build_scope(request, user, has)
scope = _elevate_scope_for_system_admin(scope, role_names) scope = _apply_role_scope(scope, user, role_names)
return _render( return _render(
request, request,
@@ -469,7 +480,7 @@ def client_import_page(request: Request):
scope=scope, scope=scope,
role_names=sorted(role_names), role_names=sorted(role_names),
current_tenant=repository.get_tenant(db, scope.tenant_id), current_tenant=repository.get_tenant(db, scope.tenant_id),
partners=repository.list_partners_for_scope(db, tenant_id=scope.tenant_id, branch_id=None if scope.allow_cross_branch else scope.branch_id), partners=([repository.get_partner(db, user.id)] if is_partner_role(role_names) else repository.list_partners_for_scope(db, tenant_id=scope.tenant_id, branch_id=None if scope.allow_cross_branch else scope.branch_id)),
import_errors=[], import_errors=[],
) )
finally: finally:
@@ -488,7 +499,7 @@ def client_import_template(request: Request):
return _redirect_denied() return _redirect_denied()
role_names = _role_names(db, user) role_names = _role_names(db, user)
scope = build_scope(request, user, has) scope = build_scope(request, user, has)
scope = _elevate_scope_for_system_admin(scope, role_names) scope = _apply_role_scope(scope, user, role_names)
payload = build_client_import_template_bytes(current_user=user, tenant_id=scope.tenant_id, partner_id=user.id if 'partner' in role_names else None) payload = build_client_import_template_bytes(current_user=user, tenant_id=scope.tenant_id, partner_id=user.id if 'partner' in role_names else None)
return Response( return Response(
content=payload, content=payload,
@@ -511,7 +522,7 @@ async def client_import_preview(request: Request, excel_file: UploadFile = File(
return _redirect_denied() return _redirect_denied()
role_names = _role_names(db, user) role_names = _role_names(db, user)
scope = build_scope(request, user, has) scope = build_scope(request, user, has)
scope = _elevate_scope_for_system_admin(scope, role_names) scope = _apply_role_scope(scope, user, role_names)
form = await request.form() form = await request.form()
validate_csrf(request, form.get("csrf_token")) validate_csrf(request, form.get("csrf_token"))
@@ -543,7 +554,7 @@ async def client_import_commit(request: Request):
return _redirect_denied() return _redirect_denied()
role_names = _role_names(db, user) role_names = _role_names(db, user)
scope = build_scope(request, user, has) scope = build_scope(request, user, has)
scope = _elevate_scope_for_system_admin(scope, role_names) scope = _apply_role_scope(scope, user, role_names)
form = await request.form() form = await request.form()
validate_csrf(request, form.get("csrf_token")) validate_csrf(request, form.get("csrf_token"))
preview_rows = deserialize_preview_rows(form.get("preview_payload") or "[]") preview_rows = deserialize_preview_rows(form.get("preview_payload") or "[]")
@@ -578,7 +589,7 @@ def client_new_page(request: Request):
role_names = _role_names(db, user) role_names = _role_names(db, user)
form_mode = _resolve_form_mode(role_names) form_mode = _resolve_form_mode(role_names)
scope = build_scope(request, user, has) scope = build_scope(request, user, has)
scope = _elevate_scope_for_system_admin(scope, role_names) scope = _apply_role_scope(scope, user, role_names)
defaults = { defaults = {
"status": "active", "status": "active",
@@ -626,7 +637,7 @@ async def client_create(request: Request):
role_names = _role_names(db, user) role_names = _role_names(db, user)
form_mode = _resolve_form_mode(role_names) form_mode = _resolve_form_mode(role_names)
scope = build_scope(request, user, has) scope = build_scope(request, user, has)
scope = _elevate_scope_for_system_admin(scope, role_names) scope = _apply_role_scope(scope, user, role_names)
form = await request.form() form = await request.form()
request._form = form request._form = form
@@ -678,13 +689,14 @@ def client_detail(request: Request, client_id: int):
role_names = _role_names(db, user) role_names = _role_names(db, user)
scope = build_scope(request, user, has) scope = build_scope(request, user, has)
scope = _elevate_scope_for_system_admin(scope, role_names) scope = _apply_role_scope(scope, user, role_names)
row = repository.get_client_detail_payload(db, client_id) row = repository.get_client_detail_payload(db, client_id, viewer_partner_id=_viewer_partner_id(user, role_names))
if not row or not can_view_client_row(scope, row, user_id=user.id): if not row or not can_view_client_row(scope, row, user_id=user.id):
return _redirect_denied() return _redirect_denied()
audit_logs = list_client_audit_logs(db, row=type("Tmp", (), {"id": row["id"]})(), limit=10) if has("clients.audit_log.view") else [] review_only = bool(is_partner_role(role_names) and int(row.get("effective_partner_id") or 0) != int(user.id))
audit_logs = list_client_audit_logs(db, row=type("Tmp", (), {"id": row["id"]})(), limit=10) if has("clients.audit_log.view") and not review_only else []
permanent_document_count = db.execute( permanent_document_count = db.execute(
select(func.count(PermanentClientDocument.id)).where( select(func.count(PermanentClientDocument.id)).where(
PermanentClientDocument.client_id == client_id, PermanentClientDocument.client_id == client_id,
@@ -701,11 +713,11 @@ def client_detail(request: Request, client_id: int):
audit_logs=audit_logs, audit_logs=audit_logs,
permanent_document_count=permanent_document_count, permanent_document_count=permanent_document_count,
scope=scope, scope=scope,
can_edit=has("clients.edit"), can_edit=has("clients.edit") and not review_only,
can_deactivate=has("clients.deactivate"), can_deactivate=has("clients.deactivate") and not review_only,
can_activate=has("clients.activate"), can_activate=has("clients.activate") and not review_only,
can_archive=has("clients.archive"), can_archive=has("clients.archive") and not review_only,
can_restore=has("clients.restore"), can_restore=has("clients.restore") and not review_only,
consultant_summary=get_client_consultant_summary(db, tenant_id=int(row["tenant_id"]), client_id=client_id), consultant_summary=get_client_consultant_summary(db, tenant_id=int(row["tenant_id"]), client_id=client_id),
client_group=get_group(db, tenant_id=int(row["tenant_id"]), group_id=int(row["client_group_id"])) if row.get("client_group_id") else None, client_group=get_group(db, tenant_id=int(row["tenant_id"]), group_id=int(row["client_group_id"])) if row.get("client_group_id") else None,
can_manage_acceptance=has("clients.acceptance.manage"), can_manage_acceptance=has("clients.acceptance.manage"),
@@ -730,7 +742,7 @@ def client_edit_page(request: Request, client_id: int):
role_names = _role_names(db, user) role_names = _role_names(db, user)
form_mode = _resolve_form_mode(role_names) form_mode = _resolve_form_mode(role_names)
scope = build_scope(request, user, has) scope = build_scope(request, user, has)
scope = _elevate_scope_for_system_admin(scope, role_names) scope = _apply_role_scope(scope, user, role_names)
row = get_client_or_404( row = get_client_or_404(
db, db,
@@ -739,6 +751,7 @@ def client_edit_page(request: Request, client_id: int):
branch_id=scope.branch_id, branch_id=scope.branch_id,
allow_cross_branch=scope.allow_cross_branch, allow_cross_branch=scope.allow_cross_branch,
allow_all_clients=scope.allow_all_clients, allow_all_clients=scope.allow_all_clients,
viewer_partner_id=_viewer_partner_id(user, role_names),
) )
return _render( return _render(
@@ -773,7 +786,7 @@ async def client_update(request: Request, client_id: int):
role_names = _role_names(db, user) role_names = _role_names(db, user)
form_mode = _resolve_form_mode(role_names) form_mode = _resolve_form_mode(role_names)
scope = build_scope(request, user, has) scope = build_scope(request, user, has)
scope = _elevate_scope_for_system_admin(scope, role_names) scope = _apply_role_scope(scope, user, role_names)
row = get_client_or_404( row = get_client_or_404(
db, db,
@@ -782,6 +795,7 @@ async def client_update(request: Request, client_id: int):
branch_id=scope.branch_id, branch_id=scope.branch_id,
allow_cross_branch=scope.allow_cross_branch, allow_cross_branch=scope.allow_cross_branch,
allow_all_clients=scope.allow_all_clients, allow_all_clients=scope.allow_all_clients,
viewer_partner_id=_viewer_partner_id(user, role_names),
) )
form = await request.form() form = await request.form()
@@ -834,7 +848,7 @@ async def client_acceptance_approve(request: Request, client_id: int):
return _redirect_denied() return _redirect_denied()
role_names = _role_names(db, user) role_names = _role_names(db, user)
scope = build_scope(request, user, has) scope = build_scope(request, user, has)
scope = _elevate_scope_for_system_admin(scope, role_names) scope = _apply_role_scope(scope, user, role_names)
form = await request.form() form = await request.form()
validate_csrf(request, form.get("csrf_token")) validate_csrf(request, form.get("csrf_token"))
row = get_client_or_404( row = get_client_or_404(
@@ -844,6 +858,7 @@ async def client_acceptance_approve(request: Request, client_id: int):
branch_id=scope.branch_id, branch_id=scope.branch_id,
allow_cross_branch=scope.allow_cross_branch, allow_cross_branch=scope.allow_cross_branch,
allow_all_clients=scope.allow_all_clients, allow_all_clients=scope.allow_all_clients,
viewer_partner_id=_viewer_partner_id(user, role_names),
) )
try: try:
row = approve_client_acceptance_service(db, row=row, actor_user_id=user.id, review_notes=form.get("acceptance_review_notes")) row = approve_client_acceptance_service(db, row=row, actor_user_id=user.id, review_notes=form.get("acceptance_review_notes"))
@@ -883,7 +898,7 @@ async def client_acceptance_reject(request: Request, client_id: int):
return _redirect_denied() return _redirect_denied()
role_names = _role_names(db, user) role_names = _role_names(db, user)
scope = build_scope(request, user, has) scope = build_scope(request, user, has)
scope = _elevate_scope_for_system_admin(scope, role_names) scope = _apply_role_scope(scope, user, role_names)
form = await request.form() form = await request.form()
validate_csrf(request, form.get("csrf_token")) validate_csrf(request, form.get("csrf_token"))
row = get_client_or_404( row = get_client_or_404(
@@ -893,6 +908,7 @@ async def client_acceptance_reject(request: Request, client_id: int):
branch_id=scope.branch_id, branch_id=scope.branch_id,
allow_cross_branch=scope.allow_cross_branch, allow_cross_branch=scope.allow_cross_branch,
allow_all_clients=scope.allow_all_clients, allow_all_clients=scope.allow_all_clients,
viewer_partner_id=_viewer_partner_id(user, role_names),
) )
row = reject_client_acceptance_service(db, row=row, actor_user_id=user.id, rejection_reason=form.get("acceptance_rejection_reason")) row = reject_client_acceptance_service(db, row=row, actor_user_id=user.id, rejection_reason=form.get("acceptance_rejection_reason"))
return RedirectResponse(url=f"/clients/{row.id}", status_code=303) return RedirectResponse(url=f"/clients/{row.id}", status_code=303)
@@ -912,7 +928,7 @@ async def client_acceptance_pending(request: Request, client_id: int):
return _redirect_denied() return _redirect_denied()
role_names = _role_names(db, user) role_names = _role_names(db, user)
scope = build_scope(request, user, has) scope = build_scope(request, user, has)
scope = _elevate_scope_for_system_admin(scope, role_names) scope = _apply_role_scope(scope, user, role_names)
form = await request.form() form = await request.form()
validate_csrf(request, form.get("csrf_token")) validate_csrf(request, form.get("csrf_token"))
row = get_client_or_404( row = get_client_or_404(
@@ -922,6 +938,7 @@ async def client_acceptance_pending(request: Request, client_id: int):
branch_id=scope.branch_id, branch_id=scope.branch_id,
allow_cross_branch=scope.allow_cross_branch, allow_cross_branch=scope.allow_cross_branch,
allow_all_clients=scope.allow_all_clients, allow_all_clients=scope.allow_all_clients,
viewer_partner_id=_viewer_partner_id(user, role_names),
) )
row = mark_client_acceptance_pending_service(db, row=row, actor_user_id=user.id, review_notes=form.get("acceptance_review_notes")) row = mark_client_acceptance_pending_service(db, row=row, actor_user_id=user.id, review_notes=form.get("acceptance_review_notes"))
return RedirectResponse(url=f"/clients/{row.id}", status_code=303) return RedirectResponse(url=f"/clients/{row.id}", status_code=303)
@@ -943,7 +960,7 @@ async def client_deactivate(request: Request, client_id: int):
role_names = _role_names(db, user) role_names = _role_names(db, user)
scope = build_scope(request, user, has) scope = build_scope(request, user, has)
scope = _elevate_scope_for_system_admin(scope, role_names) scope = _apply_role_scope(scope, user, role_names)
form = await request.form() form = await request.form()
validate_csrf(request, form.get("csrf_token")) validate_csrf(request, form.get("csrf_token"))
@@ -955,6 +972,7 @@ async def client_deactivate(request: Request, client_id: int):
branch_id=scope.branch_id, branch_id=scope.branch_id,
allow_cross_branch=scope.allow_cross_branch, allow_cross_branch=scope.allow_cross_branch,
allow_all_clients=scope.allow_all_clients, allow_all_clients=scope.allow_all_clients,
viewer_partner_id=_viewer_partner_id(user, role_names),
) )
deactivate_client_service(db, row=row, actor_user_id=user.id) deactivate_client_service(db, row=row, actor_user_id=user.id)
return RedirectResponse(url="/clients", status_code=303) return RedirectResponse(url="/clients", status_code=303)
@@ -976,7 +994,7 @@ async def client_activate(request: Request, client_id: int):
role_names = _role_names(db, user) role_names = _role_names(db, user)
scope = build_scope(request, user, has) scope = build_scope(request, user, has)
scope = _elevate_scope_for_system_admin(scope, role_names) scope = _apply_role_scope(scope, user, role_names)
form = await request.form() form = await request.form()
validate_csrf(request, form.get("csrf_token")) validate_csrf(request, form.get("csrf_token"))
@@ -988,6 +1006,7 @@ async def client_activate(request: Request, client_id: int):
branch_id=scope.branch_id, branch_id=scope.branch_id,
allow_cross_branch=scope.allow_cross_branch, allow_cross_branch=scope.allow_cross_branch,
allow_all_clients=scope.allow_all_clients, allow_all_clients=scope.allow_all_clients,
viewer_partner_id=_viewer_partner_id(user, role_names),
) )
activate_client_service(db, row=row, actor_user_id=user.id) activate_client_service(db, row=row, actor_user_id=user.id)
return RedirectResponse(url=f"/clients/{row.id}", status_code=303) return RedirectResponse(url=f"/clients/{row.id}", status_code=303)
@@ -1009,7 +1028,7 @@ async def client_archive(request: Request, client_id: int):
role_names = _role_names(db, user) role_names = _role_names(db, user)
scope = build_scope(request, user, has) scope = build_scope(request, user, has)
scope = _elevate_scope_for_system_admin(scope, role_names) scope = _apply_role_scope(scope, user, role_names)
form = await request.form() form = await request.form()
validate_csrf(request, form.get("csrf_token")) validate_csrf(request, form.get("csrf_token"))
@@ -1021,6 +1040,7 @@ async def client_archive(request: Request, client_id: int):
branch_id=scope.branch_id, branch_id=scope.branch_id,
allow_cross_branch=scope.allow_cross_branch, allow_cross_branch=scope.allow_cross_branch,
allow_all_clients=scope.allow_all_clients, allow_all_clients=scope.allow_all_clients,
viewer_partner_id=_viewer_partner_id(user, role_names),
) )
archive_client_service(db, row=row, actor_user_id=user.id) archive_client_service(db, row=row, actor_user_id=user.id)
return RedirectResponse(url="/clients?include_archived=true", status_code=303) return RedirectResponse(url="/clients?include_archived=true", status_code=303)
@@ -1042,7 +1062,7 @@ async def client_restore(request: Request, client_id: int):
role_names = _role_names(db, user) role_names = _role_names(db, user)
scope = build_scope(request, user, has) scope = build_scope(request, user, has)
scope = _elevate_scope_for_system_admin(scope, role_names) scope = _apply_role_scope(scope, user, role_names)
form = await request.form() form = await request.form()
validate_csrf(request, form.get("csrf_token")) validate_csrf(request, form.get("csrf_token"))
@@ -1054,6 +1074,7 @@ async def client_restore(request: Request, client_id: int):
branch_id=scope.branch_id, branch_id=scope.branch_id,
allow_cross_branch=scope.allow_cross_branch, allow_cross_branch=scope.allow_cross_branch,
allow_all_clients=scope.allow_all_clients, allow_all_clients=scope.allow_all_clients,
viewer_partner_id=_viewer_partner_id(user, role_names),
) )
restore_client_service(db, row=row, actor_user_id=user.id) restore_client_service(db, row=row, actor_user_id=user.id)
return RedirectResponse(url=f"/clients/{row.id}", status_code=303) return RedirectResponse(url=f"/clients/{row.id}", status_code=303)
@@ -1073,7 +1094,7 @@ async def client_acceptance_request_declarations(request: Request, client_id: in
if not has("clients.acceptance.manage"): if not has("clients.acceptance.manage"):
return _redirect_denied() return _redirect_denied()
scope = _elevate_scope_for_system_admin(build_scope(request, user, has), _role_names(db, user)) scope = _elevate_scope_for_system_admin(build_scope(request, user, has), _role_names(db, user))
row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients) row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients, viewer_partner_id=_viewer_partner_id(user, role_names))
request_client_acceptance_declarations_service(db, row=row, actor_user_id=user.id) request_client_acceptance_declarations_service(db, row=row, actor_user_id=user.id)
return RedirectResponse(url=f"/clients/{client_id}", status_code=303) return RedirectResponse(url=f"/clients/{client_id}", status_code=303)
finally: finally:
@@ -1122,7 +1143,7 @@ async def client_acceptance_kyc_sync(request: Request, client_id: int, csrf_toke
if not has("clients.acceptance.manage"): if not has("clients.acceptance.manage"):
return _redirect_denied() return _redirect_denied()
scope = _elevate_scope_for_system_admin(build_scope(request, user, has), _role_names(db, user)) scope = _elevate_scope_for_system_admin(build_scope(request, user, has), _role_names(db, user))
row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients) row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients, viewer_partner_id=_viewer_partner_id(user, role_names))
sync_client_kyc_from_permanent_documents_service(db, row=row, actor_user_id=user.id) sync_client_kyc_from_permanent_documents_service(db, row=row, actor_user_id=user.id)
return RedirectResponse(url=f"/clients/{client_id}", status_code=303) return RedirectResponse(url=f"/clients/{client_id}", status_code=303)
finally: finally:
@@ -1141,7 +1162,7 @@ async def client_acceptance_kyc_verify(request: Request, client_id: int, csrf_to
if not has("clients.acceptance.approve") and not has("clients.acceptance.manage"): if not has("clients.acceptance.approve") and not has("clients.acceptance.manage"):
return _redirect_denied() return _redirect_denied()
scope = _elevate_scope_for_system_admin(build_scope(request, user, has), _role_names(db, user)) scope = _elevate_scope_for_system_admin(build_scope(request, user, has), _role_names(db, user))
row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients) row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients, viewer_partner_id=_viewer_partner_id(user, role_names))
verify_client_kyc_service(db, row=row, actor_user_id=user.id, notes=verification_notes) verify_client_kyc_service(db, row=row, actor_user_id=user.id, notes=verification_notes)
return RedirectResponse(url=f"/clients/{client_id}", status_code=303) return RedirectResponse(url=f"/clients/{client_id}", status_code=303)
finally: finally:
@@ -1160,7 +1181,7 @@ async def client_acceptance_kyc_reject(request: Request, client_id: int, csrf_to
if not has("clients.acceptance.manage"): if not has("clients.acceptance.manage"):
return _redirect_denied() return _redirect_denied()
scope = _elevate_scope_for_system_admin(build_scope(request, user, has), _role_names(db, user)) scope = _elevate_scope_for_system_admin(build_scope(request, user, has), _role_names(db, user))
row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients) row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients, viewer_partner_id=_viewer_partner_id(user, role_names))
reject_client_kyc_service(db, row=row, actor_user_id=user.id, notes=verification_notes) reject_client_kyc_service(db, row=row, actor_user_id=user.id, notes=verification_notes)
return RedirectResponse(url=f"/clients/{client_id}", status_code=303) return RedirectResponse(url=f"/clients/{client_id}", status_code=303)
finally: finally:
@@ -1179,7 +1200,7 @@ async def client_engagement_letter_draft(request: Request, client_id: int, csrf_
if not has("clients.acceptance.manage"): if not has("clients.acceptance.manage"):
return _redirect_denied() return _redirect_denied()
scope = _elevate_scope_for_system_admin(build_scope(request, user, has), _role_names(db, user)) scope = _elevate_scope_for_system_admin(build_scope(request, user, has), _role_names(db, user))
row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients) row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients, viewer_partner_id=_viewer_partner_id(user, role_names))
draft_client_engagement_letter_service(db, row=row, actor_user_id=user.id, title=title, body_text=body_text) draft_client_engagement_letter_service(db, row=row, actor_user_id=user.id, title=title, body_text=body_text)
return RedirectResponse(url=f"/clients/{client_id}", status_code=303) return RedirectResponse(url=f"/clients/{client_id}", status_code=303)
finally: finally:
@@ -1198,7 +1219,7 @@ async def client_engagement_letter_approve_send(request: Request, client_id: int
if not has("clients.acceptance.approve"): if not has("clients.acceptance.approve"):
return _redirect_denied() return _redirect_denied()
scope = _elevate_scope_for_system_admin(build_scope(request, user, has), _role_names(db, user)) scope = _elevate_scope_for_system_admin(build_scope(request, user, has), _role_names(db, user))
row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients) row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients, viewer_partner_id=_viewer_partner_id(user, role_names))
approve_and_send_engagement_letter_service(db, row=row, letter_id=letter_id, actor_user_id=user.id) approve_and_send_engagement_letter_service(db, row=row, letter_id=letter_id, actor_user_id=user.id)
return RedirectResponse(url=f"/clients/{client_id}", status_code=303) return RedirectResponse(url=f"/clients/{client_id}", status_code=303)
finally: finally:
@@ -1214,7 +1235,7 @@ def client_engagement_letter_download(request: Request, client_id: int, letter_i
return RedirectResponse(url="/login", status_code=303) return RedirectResponse(url="/login", status_code=303)
has = _has_perm_factory(db, user) has = _has_perm_factory(db, user)
scope = _elevate_scope_for_system_admin(build_scope(request, user, has), _role_names(db, user)) scope = _elevate_scope_for_system_admin(build_scope(request, user, has), _role_names(db, user))
row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=True, allow_all_clients=scope.allow_all_clients) row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=True, allow_all_clients=scope.allow_all_clients, viewer_partner_id=_viewer_partner_id(user, role_names))
letter = get_current_engagement_letter(db, client_id=client_id) letter = get_current_engagement_letter(db, client_id=client_id)
if not letter or letter.id != letter_id: if not letter or letter.id != letter_id:
return _redirect_denied() return _redirect_denied()
@@ -1235,7 +1256,7 @@ async def client_engagement_letter_verify_manual(request: Request, client_id: in
if not has("clients.acceptance.approve") and not has("clients.acceptance.manage"): if not has("clients.acceptance.approve") and not has("clients.acceptance.manage"):
return _redirect_denied() return _redirect_denied()
scope = _elevate_scope_for_system_admin(build_scope(request, user, has), _role_names(db, user)) scope = _elevate_scope_for_system_admin(build_scope(request, user, has), _role_names(db, user))
row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients) row = get_client_or_404(db, client_id=client_id, tenant_id=scope.tenant_id, branch_id=scope.branch_id, allow_cross_branch=scope.allow_cross_branch, allow_all_clients=scope.allow_all_clients, viewer_partner_id=_viewer_partner_id(user, role_names))
verify_manual_engagement_letter_service(db, row=row, letter_id=letter_id, actor_user_id=user.id) verify_manual_engagement_letter_service(db, row=row, letter_id=letter_id, actor_user_id=user.id)
return RedirectResponse(url=f"/clients/{client_id}", status_code=303) return RedirectResponse(url=f"/clients/{client_id}", status_code=303)
finally: finally: