Enforce partner client ownership and partner-scoped imports
This commit is contained in:
@@ -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):
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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}"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user