Add performing partner to clients and engagements
This commit is contained in:
@@ -0,0 +1,44 @@
|
|||||||
|
"""add performing partner to clients and engagements
|
||||||
|
|
||||||
|
Revision ID: 20260805_performing_partner
|
||||||
|
Revises: 20260725_client_groups_family_tracking
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
|
revision = "20260805_performing_partner"
|
||||||
|
down_revision = "20260725_client_groups_family_tracking"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
def _has_column(table, column):
|
||||||
|
return column in {c["name"] for c in inspect(op.get_bind()).get_columns(table)}
|
||||||
|
|
||||||
|
def _has_index(table, name):
|
||||||
|
return name in {i["name"] for i in inspect(op.get_bind()).get_indexes(table)}
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
if not _has_column("clients", "default_performing_partner_user_id"):
|
||||||
|
op.add_column("clients", sa.Column("default_performing_partner_user_id", sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key("fk_clients_default_performing_partner", "clients", "users", ["default_performing_partner_user_id"], ["id"], ondelete="SET NULL")
|
||||||
|
if not _has_index("clients", "ix_clients_default_performing_partner_user_id"):
|
||||||
|
op.create_index("ix_clients_default_performing_partner_user_id", "clients", ["default_performing_partner_user_id"])
|
||||||
|
if not _has_column("client_service_subscriptions", "performing_partner_user_id"):
|
||||||
|
op.add_column("client_service_subscriptions", sa.Column("performing_partner_user_id", sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key("fk_css_performing_partner", "client_service_subscriptions", "users", ["performing_partner_user_id"], ["id"], ondelete="SET NULL")
|
||||||
|
if not _has_index("client_service_subscriptions", "ix_client_service_subscriptions_performing_partner_user_id"):
|
||||||
|
op.create_index("ix_client_service_subscriptions_performing_partner_user_id", "client_service_subscriptions", ["performing_partner_user_id"])
|
||||||
|
op.execute("""UPDATE client_service_subscriptions SET performing_partner_user_id = assigned_partner_user_id WHERE performing_partner_user_id IS NULL""")
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
if _has_index("client_service_subscriptions", "ix_client_service_subscriptions_performing_partner_user_id"):
|
||||||
|
op.drop_index("ix_client_service_subscriptions_performing_partner_user_id", table_name="client_service_subscriptions")
|
||||||
|
if _has_column("client_service_subscriptions", "performing_partner_user_id"):
|
||||||
|
op.drop_constraint("fk_css_performing_partner", "client_service_subscriptions", type_="foreignkey")
|
||||||
|
op.drop_column("client_service_subscriptions", "performing_partner_user_id")
|
||||||
|
if _has_index("clients", "ix_clients_default_performing_partner_user_id"):
|
||||||
|
op.drop_index("ix_clients_default_performing_partner_user_id", table_name="clients")
|
||||||
|
if _has_column("clients", "default_performing_partner_user_id"):
|
||||||
|
op.drop_constraint("fk_clients_default_performing_partner", "clients", type_="foreignkey")
|
||||||
|
op.drop_column("clients", "default_performing_partner_user_id")
|
||||||
@@ -23,6 +23,7 @@ class Client(CommonBase):
|
|||||||
client_group_id: Mapped[int | None] = mapped_column(ForeignKey("client_groups.id", ondelete="SET NULL"), nullable=True, index=True)
|
client_group_id: Mapped[int | None] = mapped_column(ForeignKey("client_groups.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||||
group_relationship: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
group_relationship: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||||
is_group_head: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, index=True)
|
is_group_head: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, index=True)
|
||||||
|
default_performing_partner_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True)
|
||||||
default_review_partner_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True)
|
default_review_partner_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True)
|
||||||
referred_by_consultant_id: Mapped[int | None] = mapped_column(ForeignKey("consultant_profiles.id", ondelete="SET NULL"), nullable=True, index=True)
|
referred_by_consultant_id: Mapped[int | None] = mapped_column(ForeignKey("consultant_profiles.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||||
referral_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
referral_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ def build_clients_query(
|
|||||||
select(ClientServiceSubscription.id).where(
|
select(ClientServiceSubscription.id).where(
|
||||||
ClientServiceSubscription.tenant_id == tenant_id,
|
ClientServiceSubscription.tenant_id == tenant_id,
|
||||||
ClientServiceSubscription.client_id == Client.id,
|
ClientServiceSubscription.client_id == Client.id,
|
||||||
ClientServiceSubscription.review_partner_user_id == viewer_partner_id,
|
or_(
|
||||||
|
ClientServiceSubscription.performing_partner_user_id == viewer_partner_id,
|
||||||
|
ClientServiceSubscription.review_partner_user_id == viewer_partner_id,
|
||||||
|
),
|
||||||
ClientServiceSubscription.is_active.is_(True),
|
ClientServiceSubscription.is_active.is_(True),
|
||||||
)
|
)
|
||||||
) if viewer_partner_id is not None else None
|
) if viewer_partner_id is not None else None
|
||||||
@@ -133,7 +136,10 @@ def list_clients(
|
|||||||
review_stats = exists(select(ClientServiceSubscription.id).where(
|
review_stats = exists(select(ClientServiceSubscription.id).where(
|
||||||
ClientServiceSubscription.tenant_id == tenant_id,
|
ClientServiceSubscription.tenant_id == tenant_id,
|
||||||
ClientServiceSubscription.client_id == Client.id,
|
ClientServiceSubscription.client_id == Client.id,
|
||||||
ClientServiceSubscription.review_partner_user_id == viewer_partner_id,
|
or_(
|
||||||
|
ClientServiceSubscription.performing_partner_user_id == viewer_partner_id,
|
||||||
|
ClientServiceSubscription.review_partner_user_id == viewer_partner_id,
|
||||||
|
),
|
||||||
ClientServiceSubscription.is_active.is_(True),
|
ClientServiceSubscription.is_active.is_(True),
|
||||||
))
|
))
|
||||||
stats_stmt = stats_stmt.where(or_(Client.partner_id == viewer_partner_id, review_stats))
|
stats_stmt = stats_stmt.where(or_(Client.partner_id == viewer_partner_id, review_stats))
|
||||||
@@ -149,7 +155,10 @@ def has_partner_review_access(db: Session, *, tenant_id: int, client_id: int, pa
|
|||||||
select(ClientServiceSubscription.id).where(
|
select(ClientServiceSubscription.id).where(
|
||||||
ClientServiceSubscription.tenant_id == tenant_id,
|
ClientServiceSubscription.tenant_id == tenant_id,
|
||||||
ClientServiceSubscription.client_id == client_id,
|
ClientServiceSubscription.client_id == client_id,
|
||||||
ClientServiceSubscription.review_partner_user_id == partner_user_id,
|
or_(
|
||||||
|
ClientServiceSubscription.performing_partner_user_id == partner_user_id,
|
||||||
|
ClientServiceSubscription.review_partner_user_id == partner_user_id,
|
||||||
|
),
|
||||||
ClientServiceSubscription.is_active.is_(True),
|
ClientServiceSubscription.is_active.is_(True),
|
||||||
).limit(1)
|
).limit(1)
|
||||||
).scalar_one_or_none())
|
).scalar_one_or_none())
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class ClientBase(BaseModel):
|
|||||||
client_group_id: Optional[int] = None
|
client_group_id: Optional[int] = None
|
||||||
group_relationship: Optional[str] = None
|
group_relationship: Optional[str] = None
|
||||||
is_group_head: bool = False
|
is_group_head: bool = False
|
||||||
|
default_performing_partner_user_id: Optional[int] = None
|
||||||
default_review_partner_user_id: Optional[int] = None
|
default_review_partner_user_id: Optional[int] = None
|
||||||
referred_by_consultant_id: Optional[int] = None
|
referred_by_consultant_id: Optional[int] = None
|
||||||
primary_consultant_id: Optional[int] = None
|
primary_consultant_id: Optional[int] = None
|
||||||
@@ -219,6 +220,7 @@ class ClientUpdate(BaseModel):
|
|||||||
client_group_id: Optional[int] = None
|
client_group_id: Optional[int] = None
|
||||||
group_relationship: Optional[str] = None
|
group_relationship: Optional[str] = None
|
||||||
is_group_head: Optional[bool] = None
|
is_group_head: Optional[bool] = None
|
||||||
|
default_performing_partner_user_id: Optional[int] = None
|
||||||
default_review_partner_user_id: Optional[int] = None
|
default_review_partner_user_id: Optional[int] = None
|
||||||
referred_by_consultant_id: Optional[int] = None
|
referred_by_consultant_id: Optional[int] = None
|
||||||
primary_consultant_id: Optional[int] = None
|
primary_consultant_id: Optional[int] = None
|
||||||
@@ -398,6 +400,7 @@ class ClientOut(BaseModel):
|
|||||||
tenant_id: int
|
tenant_id: int
|
||||||
branch_id: int
|
branch_id: int
|
||||||
partner_id: Optional[int] = None
|
partner_id: Optional[int] = None
|
||||||
|
default_performing_partner_user_id: Optional[int] = None
|
||||||
default_review_partner_user_id: Optional[int] = None
|
default_review_partner_user_id: Optional[int] = None
|
||||||
engagement_mode: str
|
engagement_mode: str
|
||||||
client_code: str
|
client_code: str
|
||||||
|
|||||||
@@ -117,6 +117,7 @@
|
|||||||
<div><span class="font-medium">Audit Firm:</span> {{ row.assoc_firm_tenant_id or row.tenant_id or '-' }}</div>
|
<div><span class="font-medium">Audit Firm:</span> {{ row.assoc_firm_tenant_id or row.tenant_id or '-' }}</div>
|
||||||
<div><span class="font-medium">Branch:</span> {{ row.branch_id or '-' }}</div>
|
<div><span class="font-medium">Branch:</span> {{ row.branch_id or '-' }}</div>
|
||||||
<div><span class="font-medium">Partner:</span> {{ row.assoc_partner_user_id or row.partner_id or '-' }}</div>
|
<div><span class="font-medium">Partner:</span> {{ row.assoc_partner_user_id or row.partner_id or '-' }}</div>
|
||||||
|
<div><span class="font-medium">Default Performing Partner:</span> {{ row.default_performing_partner_user_id or row.partner_id or '-' }}</div>
|
||||||
<div><span class="font-medium">Default Review Partner:</span> {{ row.default_review_partner_user_id or '-' }}</div>
|
<div><span class="font-medium">Default Review Partner:</span> {{ row.default_review_partner_user_id or '-' }}</div>
|
||||||
<div><span class="font-medium">Referred by:</span> {{ consultant_summary.referred_by.contact_person if consultant_summary and consultant_summary.referred_by else 'Direct / Not recorded' }}</div>
|
<div><span class="font-medium">Referred by:</span> {{ consultant_summary.referred_by.contact_person if consultant_summary and consultant_summary.referred_by else 'Direct / Not recorded' }}</div>
|
||||||
<div><span class="font-medium">Primary consultant:</span> {{ consultant_summary.primary.contact_person if consultant_summary and consultant_summary.primary else 'Firm managed' }}</div>
|
<div><span class="font-medium">Primary consultant:</span> {{ consultant_summary.primary.contact_person if consultant_summary and consultant_summary.primary else 'Firm managed' }}</div>
|
||||||
|
|||||||
@@ -316,6 +316,19 @@
|
|||||||
<input name="referral_reference" value="{{ form_data.referral_reference or (row.referral_reference if is_edit else '') }}" class="mt-1 w-full rounded-xl border border-slate-300 px-4 py-2 text-sm" placeholder="Source, campaign, agreement or note">
|
<input name="referral_reference" value="{{ form_data.referral_reference or (row.referral_reference if is_edit else '') }}" class="mt-1 w-full rounded-xl border border-slate-300 px-4 py-2 text-sm" placeholder="Source, campaign, agreement or note">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700">Default Performing Partner</label>
|
||||||
|
<select name="default_performing_partner_user_id" class="mt-1 w-full rounded-xl border border-slate-300 px-4 py-2 text-sm">
|
||||||
|
<option value="">-- Same as Engagement Partner --</option>
|
||||||
|
{% for p in form_options.performing_partners or [] %}
|
||||||
|
<option value="{{ p.id }}" {% if (form_data.default_performing_partner_user_id or (row.default_performing_partner_user_id if is_edit else None)) == p.id %}selected{% endif %}>
|
||||||
|
{{ p.full_name or p.email }}{% if p.tenant_name %} ({{ p.tenant_name }}){% endif %}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<p class="mt-1 text-xs text-slate-500">Defaults the Partner who directly performs or supervises the engagement. When blank, the Engagement Partner is used.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-slate-700">Default Review Partner</label>
|
<label class="block text-sm font-medium text-slate-700">Default Review Partner</label>
|
||||||
<select name="default_review_partner_user_id" class="mt-1 w-full rounded-xl border border-slate-300 px-4 py-2 text-sm">
|
<select name="default_review_partner_user_id" class="mt-1 w-full rounded-xl border border-slate-300 px-4 py-2 text-sm">
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ def _build_form_payload(request: Request, user, scope, *, include_client_code: b
|
|||||||
"client_group_id": int(form.get("client_group_id")) if form.get("client_group_id") not in (None, "", "None") else None,
|
"client_group_id": int(form.get("client_group_id")) if form.get("client_group_id") not in (None, "", "None") else None,
|
||||||
"group_relationship": form.get("group_relationship"),
|
"group_relationship": form.get("group_relationship"),
|
||||||
"is_group_head": _form_bool(form.get("is_group_head")),
|
"is_group_head": _form_bool(form.get("is_group_head")),
|
||||||
|
"default_performing_partner_user_id": int(form.get("default_performing_partner_user_id")) if form.get("default_performing_partner_user_id") not in (None, "", "None") else None,
|
||||||
"default_review_partner_user_id": int(form.get("default_review_partner_user_id")) if form.get("default_review_partner_user_id") not in (None, "", "None") else None,
|
"default_review_partner_user_id": int(form.get("default_review_partner_user_id")) if form.get("default_review_partner_user_id") not in (None, "", "None") else None,
|
||||||
"referred_by_consultant_id": int(form.get("referred_by_consultant_id")) if form.get("referred_by_consultant_id") not in (None, "", "None") else None,
|
"referred_by_consultant_id": int(form.get("referred_by_consultant_id")) if form.get("referred_by_consultant_id") not in (None, "", "None") else None,
|
||||||
"primary_consultant_id": int(form.get("primary_consultant_id")) if form.get("primary_consultant_id") not in (None, "", "None") else None,
|
"primary_consultant_id": int(form.get("primary_consultant_id")) if form.get("primary_consultant_id") not in (None, "", "None") else None,
|
||||||
@@ -269,6 +270,7 @@ def _form_options(db, scope, form_mode: str):
|
|||||||
"tenants": repository.list_tenants(db),
|
"tenants": repository.list_tenants(db),
|
||||||
"branches": repository.list_all_branches(db),
|
"branches": repository.list_all_branches(db),
|
||||||
"partners": repository.list_all_partners(db),
|
"partners": repository.list_all_partners(db),
|
||||||
|
"performing_partners": repository.list_all_partners(db),
|
||||||
"review_partners": repository.list_all_partners(db),
|
"review_partners": repository.list_all_partners(db),
|
||||||
"active_tenant_id": tenant_id,
|
"active_tenant_id": tenant_id,
|
||||||
"active_branch_id": branch_id,
|
"active_branch_id": branch_id,
|
||||||
@@ -286,6 +288,11 @@ def _form_options(db, scope, form_mode: str):
|
|||||||
),
|
),
|
||||||
"consultants": list_consultants(db, tenant_id=tenant_id, branch_id=branch_id, include_inactive=False),
|
"consultants": list_consultants(db, tenant_id=tenant_id, branch_id=branch_id, include_inactive=False),
|
||||||
"client_groups": [item["group"] for item in list_groups(db, tenant_id=tenant_id)],
|
"client_groups": [item["group"] for item in list_groups(db, tenant_id=tenant_id)],
|
||||||
|
"performing_partners": repository.list_partners_for_scope(
|
||||||
|
db,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
branch_id=None if scope.allow_cross_branch else branch_id,
|
||||||
|
),
|
||||||
"review_partners": repository.list_partners_for_scope(
|
"review_partners": repository.list_partners_for_scope(
|
||||||
db,
|
db,
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ def _task_scope(stmt, tenant_id: int | None, branch_id: int | None, current_user
|
|||||||
ClientServiceTaskInstance.subscription.has(
|
ClientServiceTaskInstance.subscription.has(
|
||||||
or_(
|
or_(
|
||||||
ClientServiceSubscription.assigned_partner_user_id == current_user.id,
|
ClientServiceSubscription.assigned_partner_user_id == current_user.id,
|
||||||
|
ClientServiceSubscription.performing_partner_user_id == current_user.id,
|
||||||
ClientServiceSubscription.review_partner_user_id == current_user.id,
|
ClientServiceSubscription.review_partner_user_id == current_user.id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -157,6 +158,7 @@ def _subscription_scope(stmt, tenant_id: int | None, branch_id: int | None, curr
|
|||||||
stmt = stmt.where(
|
stmt = stmt.where(
|
||||||
or_(
|
or_(
|
||||||
ClientServiceSubscription.assigned_partner_user_id == current_user.id,
|
ClientServiceSubscription.assigned_partner_user_id == current_user.id,
|
||||||
|
ClientServiceSubscription.performing_partner_user_id == current_user.id,
|
||||||
ClientServiceSubscription.review_partner_user_id == current_user.id,
|
ClientServiceSubscription.review_partner_user_id == current_user.id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -258,23 +260,10 @@ def _load_clients(db: Session, tenant_id: int | None, branch_id: int | None, cur
|
|||||||
def _staff_rows(db: Session, tenant_id: int | None, branch_id: int | None, tasks: list[ClientServiceTaskInstance]) -> list[dict[str, Any]]:
|
def _staff_rows(db: Session, tenant_id: int | None, branch_id: int | None, tasks: list[ClientServiceTaskInstance]) -> list[dict[str, Any]]:
|
||||||
if not tenant_id:
|
if not tenant_id:
|
||||||
return []
|
return []
|
||||||
stmt = (
|
stmt = select(User).where(User.tenant_id == tenant_id, User.is_active.is_(True))
|
||||||
select(User)
|
|
||||||
.join(UserRole, UserRole.user_id == User.id)
|
|
||||||
.join(Role, Role.id == UserRole.role_id)
|
|
||||||
.where(
|
|
||||||
User.tenant_id == tenant_id,
|
|
||||||
User.is_active.is_(True),
|
|
||||||
Role.is_active.is_(True),
|
|
||||||
Role.name.in_(("Staff", "Branch Manager")),
|
|
||||||
)
|
|
||||||
.distinct()
|
|
||||||
)
|
|
||||||
if branch_id is not None:
|
if branch_id is not None:
|
||||||
stmt = stmt.where(User.branch_id == branch_id)
|
stmt = stmt.where(User.branch_id == branch_id)
|
||||||
users = db.execute(
|
users = db.execute(stmt.order_by(User.full_name.asc(), User.email.asc()).limit(100)).scalars().all()
|
||||||
stmt.order_by(User.full_name.asc(), User.email.asc()).limit(100)
|
|
||||||
).scalars().all()
|
|
||||||
by_user: dict[int, dict[str, int]] = {}
|
by_user: dict[int, dict[str, int]] = {}
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
uid = getattr(task, "assigned_to_user_id", None)
|
uid = getattr(task, "assigned_to_user_id", None)
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ def list_subscription_payload(
|
|||||||
selectinload(ClientServiceSubscription.client),
|
selectinload(ClientServiceSubscription.client),
|
||||||
selectinload(ClientServiceSubscription.catalogue),
|
selectinload(ClientServiceSubscription.catalogue),
|
||||||
selectinload(ClientServiceSubscription.assigned_partner),
|
selectinload(ClientServiceSubscription.assigned_partner),
|
||||||
|
selectinload(ClientServiceSubscription.performing_partner),
|
||||||
selectinload(ClientServiceSubscription.assigned_manager),
|
selectinload(ClientServiceSubscription.assigned_manager),
|
||||||
selectinload(ClientServiceSubscription.assigned_staff),
|
selectinload(ClientServiceSubscription.assigned_staff),
|
||||||
selectinload(ClientServiceSubscription.review_partner),
|
selectinload(ClientServiceSubscription.review_partner),
|
||||||
@@ -118,6 +119,7 @@ def get_subscription(db: Session, *, subscription_id: int, tenant_id: int) -> Cl
|
|||||||
selectinload(ClientServiceSubscription.client),
|
selectinload(ClientServiceSubscription.client),
|
||||||
selectinload(ClientServiceSubscription.catalogue),
|
selectinload(ClientServiceSubscription.catalogue),
|
||||||
selectinload(ClientServiceSubscription.assigned_partner),
|
selectinload(ClientServiceSubscription.assigned_partner),
|
||||||
|
selectinload(ClientServiceSubscription.performing_partner),
|
||||||
selectinload(ClientServiceSubscription.assigned_manager),
|
selectinload(ClientServiceSubscription.assigned_manager),
|
||||||
selectinload(ClientServiceSubscription.assigned_staff),
|
selectinload(ClientServiceSubscription.assigned_staff),
|
||||||
selectinload(ClientServiceSubscription.review_partner),
|
selectinload(ClientServiceSubscription.review_partner),
|
||||||
|
|||||||
@@ -223,6 +223,7 @@ def subscription_create_page(request: Request, client_id: int | None = None):
|
|||||||
)
|
)
|
||||||
enabled_services = list_enabled_services_for_assignment(db, tenant_id=tenant_id)
|
enabled_services = list_enabled_services_for_assignment(db, tenant_id=tenant_id)
|
||||||
assignable_users = list_assignable_users(db, tenant_id=tenant_id, branch_id=branch_id)
|
assignable_users = list_assignable_users(db, tenant_id=tenant_id, branch_id=branch_id)
|
||||||
|
performing_partners = list_assignable_users(db, tenant_id=tenant_id, branch_id=branch_id, role_names=("Partner",))
|
||||||
review_partners = list_review_partners(db, tenant_id=tenant_id, branch_id=branch_id)
|
review_partners = list_review_partners(db, tenant_id=tenant_id, branch_id=branch_id)
|
||||||
|
|
||||||
return _render(
|
return _render(
|
||||||
@@ -236,6 +237,7 @@ def subscription_create_page(request: Request, client_id: int | None = None):
|
|||||||
clients=clients,
|
clients=clients,
|
||||||
enabled_services=enabled_services,
|
enabled_services=enabled_services,
|
||||||
assignable_users=assignable_users,
|
assignable_users=assignable_users,
|
||||||
|
performing_partners=performing_partners,
|
||||||
review_partners=review_partners,
|
review_partners=review_partners,
|
||||||
selected_client_id=client_id,
|
selected_client_id=client_id,
|
||||||
financial_year=_active_financial_year(request),
|
financial_year=_active_financial_year(request),
|
||||||
@@ -252,6 +254,7 @@ def subscription_create_submit(
|
|||||||
assigned_partner_user_id: str = Form(""),
|
assigned_partner_user_id: str = Form(""),
|
||||||
assigned_manager_user_id: str = Form(""),
|
assigned_manager_user_id: str = Form(""),
|
||||||
assigned_staff_user_id: str = Form(""),
|
assigned_staff_user_id: str = Form(""),
|
||||||
|
performing_partner_user_id: str = Form(""),
|
||||||
review_partner_user_id: str = Form(""),
|
review_partner_user_id: str = Form(""),
|
||||||
financial_year: str = Form(""),
|
financial_year: str = Form(""),
|
||||||
start_date: str = Form(""),
|
start_date: str = Form(""),
|
||||||
@@ -317,6 +320,8 @@ def subscription_create_submit(
|
|||||||
row.branch_id = firm_selection.default_branch_id or getattr(user, "branch_id", None)
|
row.branch_id = firm_selection.default_branch_id or getattr(user, "branch_id", None)
|
||||||
row.firm_service_selection_id = firm_selection.id
|
row.firm_service_selection_id = firm_selection.id
|
||||||
row.assigned_partner_user_id = int(assigned_partner_user_id) if assigned_partner_user_id.strip() else None
|
row.assigned_partner_user_id = int(assigned_partner_user_id) if assigned_partner_user_id.strip() else None
|
||||||
|
requested_performing_partner_id = int(performing_partner_user_id) if performing_partner_user_id.strip() else None
|
||||||
|
row.performing_partner_user_id = requested_performing_partner_id or getattr(client, "default_performing_partner_user_id", None) or row.assigned_partner_user_id
|
||||||
row.assigned_manager_user_id = int(assigned_manager_user_id) if assigned_manager_user_id.strip() else None
|
row.assigned_manager_user_id = int(assigned_manager_user_id) if assigned_manager_user_id.strip() else None
|
||||||
row.assigned_staff_user_id = int(assigned_staff_user_id) if assigned_staff_user_id.strip() else None
|
row.assigned_staff_user_id = int(assigned_staff_user_id) if assigned_staff_user_id.strip() else None
|
||||||
if review_partner_required_for_engagement(db, tenant_id=tenant_id, engagement_type=row.engagement_type):
|
if review_partner_required_for_engagement(db, tenant_id=tenant_id, engagement_type=row.engagement_type):
|
||||||
@@ -364,6 +369,7 @@ def subscription_bulk_create_page(request: Request, error: str = ""):
|
|||||||
)
|
)
|
||||||
enabled_services = list_enabled_services_for_assignment(db, tenant_id=tenant_id)
|
enabled_services = list_enabled_services_for_assignment(db, tenant_id=tenant_id)
|
||||||
partners = list_assignable_users(db, tenant_id=tenant_id, role_names=("Partner",))
|
partners = list_assignable_users(db, tenant_id=tenant_id, role_names=("Partner",))
|
||||||
|
performing_partners = partners
|
||||||
managers = list_assignable_users(db, tenant_id=tenant_id, role_names=("Branch Manager",))
|
managers = list_assignable_users(db, tenant_id=tenant_id, role_names=("Branch Manager",))
|
||||||
staff_users = list_assignable_users(db, tenant_id=tenant_id, role_names=("Staff",))
|
staff_users = list_assignable_users(db, tenant_id=tenant_id, role_names=("Staff",))
|
||||||
review_partners = list_review_partners(db, tenant_id=tenant_id)
|
review_partners = list_review_partners(db, tenant_id=tenant_id)
|
||||||
@@ -377,6 +383,7 @@ def subscription_bulk_create_page(request: Request, error: str = ""):
|
|||||||
clients=clients,
|
clients=clients,
|
||||||
enabled_services=enabled_services,
|
enabled_services=enabled_services,
|
||||||
partners=partners,
|
partners=partners,
|
||||||
|
performing_partners=performing_partners,
|
||||||
client_partner_names={row.id: (row.full_name or row.email) for row in partners},
|
client_partner_names={row.id: (row.full_name or row.email) for row in partners},
|
||||||
managers=managers,
|
managers=managers,
|
||||||
staff_users=staff_users,
|
staff_users=staff_users,
|
||||||
@@ -385,6 +392,7 @@ def subscription_bulk_create_page(request: Request, error: str = ""):
|
|||||||
error_message={
|
error_message={
|
||||||
"service": "Select a valid enabled firm service.",
|
"service": "Select a valid enabled firm service.",
|
||||||
"partner": "Select a valid active Partner.",
|
"partner": "Select a valid active Partner.",
|
||||||
|
"performing_partner": "The selected Performing Partner is not available for this firm.",
|
||||||
"manager": "The selected Manager is not available for this firm.",
|
"manager": "The selected Manager is not available for this firm.",
|
||||||
"staff": "The selected Staff member is not available for this firm.",
|
"staff": "The selected Staff member is not available for this firm.",
|
||||||
"review_partner": "The selected Review Partner is not available for this firm.",
|
"review_partner": "The selected Review Partner is not available for this firm.",
|
||||||
@@ -404,6 +412,7 @@ def subscription_bulk_create_submit(
|
|||||||
assigned_partner_user_id: int = Form(...),
|
assigned_partner_user_id: int = Form(...),
|
||||||
assigned_manager_user_id: str = Form(""),
|
assigned_manager_user_id: str = Form(""),
|
||||||
assigned_staff_user_id: str = Form(""),
|
assigned_staff_user_id: str = Form(""),
|
||||||
|
performing_partner_user_id: str = Form(""),
|
||||||
review_partner_user_id: str = Form(""),
|
review_partner_user_id: str = Form(""),
|
||||||
financial_year: str = Form(""),
|
financial_year: str = Form(""),
|
||||||
remarks: str = Form(""),
|
remarks: str = Form(""),
|
||||||
@@ -448,6 +457,7 @@ def subscription_bulk_create_submit(
|
|||||||
review_partners = list_review_partners(db, tenant_id=tenant_id)
|
review_partners = list_review_partners(db, tenant_id=tenant_id)
|
||||||
|
|
||||||
partner_ids = {row.id for row in partners}
|
partner_ids = {row.id for row in partners}
|
||||||
|
performing_partner_ids = partner_ids
|
||||||
manager_ids = {row.id for row in managers}
|
manager_ids = {row.id for row in managers}
|
||||||
staff_ids = {row.id for row in staff_users}
|
staff_ids = {row.id for row in staff_users}
|
||||||
review_partner_ids = {row.id for row in review_partners}
|
review_partner_ids = {row.id for row in review_partners}
|
||||||
@@ -455,9 +465,12 @@ def subscription_bulk_create_submit(
|
|||||||
if assigned_partner_user_id not in partner_ids:
|
if assigned_partner_user_id not in partner_ids:
|
||||||
return RedirectResponse(url="/services/engagements/bulk-new?error=partner", status_code=303)
|
return RedirectResponse(url="/services/engagements/bulk-new?error=partner", status_code=303)
|
||||||
|
|
||||||
|
performing_partner_id = int(performing_partner_user_id) if performing_partner_user_id.strip() else assigned_partner_user_id
|
||||||
manager_id = int(assigned_manager_user_id) if assigned_manager_user_id.strip() else None
|
manager_id = int(assigned_manager_user_id) if assigned_manager_user_id.strip() else None
|
||||||
staff_id = int(assigned_staff_user_id) if assigned_staff_user_id.strip() else None
|
staff_id = int(assigned_staff_user_id) if assigned_staff_user_id.strip() else None
|
||||||
review_partner_id = int(review_partner_user_id) if review_partner_user_id.strip() else None
|
review_partner_id = int(review_partner_user_id) if review_partner_user_id.strip() else None
|
||||||
|
if performing_partner_id not in performing_partner_ids:
|
||||||
|
return RedirectResponse(url="/services/engagements/bulk-new?error=performing_partner", status_code=303)
|
||||||
if manager_id is not None and manager_id not in manager_ids:
|
if manager_id is not None and manager_id not in manager_ids:
|
||||||
return RedirectResponse(url="/services/engagements/bulk-new?error=manager", status_code=303)
|
return RedirectResponse(url="/services/engagements/bulk-new?error=manager", status_code=303)
|
||||||
if staff_id is not None and staff_id not in staff_ids:
|
if staff_id is not None and staff_id not in staff_ids:
|
||||||
@@ -531,6 +544,7 @@ def subscription_bulk_create_submit(
|
|||||||
service_catalogue_id=service_catalogue_id,
|
service_catalogue_id=service_catalogue_id,
|
||||||
firm_service_selection_id=firm_selection.id,
|
firm_service_selection_id=firm_selection.id,
|
||||||
assigned_partner_user_id=assigned_partner_user_id,
|
assigned_partner_user_id=assigned_partner_user_id,
|
||||||
|
performing_partner_user_id=performing_partner_id or getattr(client, "default_performing_partner_user_id", None) or assigned_partner_user_id,
|
||||||
assigned_manager_user_id=manager_id,
|
assigned_manager_user_id=manager_id,
|
||||||
assigned_staff_user_id=staff_id,
|
assigned_staff_user_id=staff_id,
|
||||||
review_partner_user_id=(
|
review_partner_user_id=(
|
||||||
@@ -980,6 +994,7 @@ def subscription_edit_page(request: Request, subscription_id: int):
|
|||||||
)
|
)
|
||||||
enabled_services = list_enabled_services_for_assignment(db, tenant_id=tenant_id)
|
enabled_services = list_enabled_services_for_assignment(db, tenant_id=tenant_id)
|
||||||
assignable_users = list_assignable_users(db, tenant_id=tenant_id, branch_id=branch_id)
|
assignable_users = list_assignable_users(db, tenant_id=tenant_id, branch_id=branch_id)
|
||||||
|
performing_partners = list_assignable_users(db, tenant_id=tenant_id, branch_id=branch_id, role_names=("Partner",))
|
||||||
review_partners = list_review_partners(db, tenant_id=tenant_id, branch_id=branch_id)
|
review_partners = list_review_partners(db, tenant_id=tenant_id, branch_id=branch_id)
|
||||||
return _render(
|
return _render(
|
||||||
request,
|
request,
|
||||||
@@ -992,6 +1007,7 @@ def subscription_edit_page(request: Request, subscription_id: int):
|
|||||||
clients=clients,
|
clients=clients,
|
||||||
enabled_services=enabled_services,
|
enabled_services=enabled_services,
|
||||||
assignable_users=assignable_users,
|
assignable_users=assignable_users,
|
||||||
|
performing_partners=performing_partners,
|
||||||
review_partners=review_partners,
|
review_partners=review_partners,
|
||||||
selected_client_id=row.client_id,
|
selected_client_id=row.client_id,
|
||||||
financial_year=row.financial_year,
|
financial_year=row.financial_year,
|
||||||
@@ -1007,6 +1023,7 @@ def subscription_edit_submit(
|
|||||||
assigned_partner_user_id: str = Form(""),
|
assigned_partner_user_id: str = Form(""),
|
||||||
assigned_manager_user_id: str = Form(""),
|
assigned_manager_user_id: str = Form(""),
|
||||||
assigned_staff_user_id: str = Form(""),
|
assigned_staff_user_id: str = Form(""),
|
||||||
|
performing_partner_user_id: str = Form(""),
|
||||||
review_partner_user_id: str = Form(""),
|
review_partner_user_id: str = Form(""),
|
||||||
start_date: str = Form(""),
|
start_date: str = Form(""),
|
||||||
end_date: str = Form(""),
|
end_date: str = Form(""),
|
||||||
@@ -1038,6 +1055,8 @@ def subscription_edit_submit(
|
|||||||
return RedirectResponse(url=f"/services/engagements/{row.id}?year_locked=1", status_code=303)
|
return RedirectResponse(url=f"/services/engagements/{row.id}?year_locked=1", status_code=303)
|
||||||
|
|
||||||
row.assigned_partner_user_id = int(assigned_partner_user_id) if assigned_partner_user_id.strip() else None
|
row.assigned_partner_user_id = int(assigned_partner_user_id) if assigned_partner_user_id.strip() else None
|
||||||
|
requested_performing_partner_id = int(performing_partner_user_id) if performing_partner_user_id.strip() else None
|
||||||
|
row.performing_partner_user_id = requested_performing_partner_id or getattr(row.client, "default_performing_partner_user_id", None) or row.assigned_partner_user_id
|
||||||
row.assigned_manager_user_id = int(assigned_manager_user_id) if assigned_manager_user_id.strip() else None
|
row.assigned_manager_user_id = int(assigned_manager_user_id) if assigned_manager_user_id.strip() else None
|
||||||
row.assigned_staff_user_id = int(assigned_staff_user_id) if assigned_staff_user_id.strip() else None
|
row.assigned_staff_user_id = int(assigned_staff_user_id) if assigned_staff_user_id.strip() else None
|
||||||
if review_partner_required_for_engagement(db, tenant_id=tenant_id, engagement_type=row.engagement_type):
|
if review_partner_required_for_engagement(db, tenant_id=tenant_id, engagement_type=row.engagement_type):
|
||||||
|
|||||||
@@ -394,6 +394,7 @@ class ClientServiceSubscription(CommonBase):
|
|||||||
firm_service_selection_id: Mapped[int | None] = mapped_column(ForeignKey("firm_service_selections.id", ondelete="SET NULL"), nullable=True, index=True)
|
firm_service_selection_id: Mapped[int | None] = mapped_column(ForeignKey("firm_service_selections.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||||
|
|
||||||
assigned_partner_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True)
|
assigned_partner_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True)
|
||||||
|
performing_partner_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True)
|
||||||
assigned_manager_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True)
|
assigned_manager_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True)
|
||||||
assigned_staff_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True)
|
assigned_staff_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True)
|
||||||
review_partner_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True)
|
review_partner_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True, index=True)
|
||||||
@@ -449,6 +450,7 @@ class ClientServiceSubscription(CommonBase):
|
|||||||
due_date_rule = relationship("ServiceDueDateRule", foreign_keys=[due_date_rule_id])
|
due_date_rule = relationship("ServiceDueDateRule", foreign_keys=[due_date_rule_id])
|
||||||
firm_selection = relationship("FirmServiceSelection")
|
firm_selection = relationship("FirmServiceSelection")
|
||||||
assigned_partner = relationship("User", foreign_keys=[assigned_partner_user_id])
|
assigned_partner = relationship("User", foreign_keys=[assigned_partner_user_id])
|
||||||
|
performing_partner = relationship("User", foreign_keys=[performing_partner_user_id])
|
||||||
assigned_manager = relationship("User", foreign_keys=[assigned_manager_user_id])
|
assigned_manager = relationship("User", foreign_keys=[assigned_manager_user_id])
|
||||||
assigned_staff = relationship("User", foreign_keys=[assigned_staff_user_id])
|
assigned_staff = relationship("User", foreign_keys=[assigned_staff_user_id])
|
||||||
review_partner = relationship("User", foreign_keys=[review_partner_user_id])
|
review_partner = relationship("User", foreign_keys=[review_partner_user_id])
|
||||||
|
|||||||
@@ -44,6 +44,15 @@
|
|||||||
<p class="mt-1 text-xs text-slate-500">The engagement branch is derived automatically from the selected Partner’s branch.</p>
|
<p class="mt-1 text-xs text-slate-500">The engagement branch is derived automatically from the selected Partner’s branch.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="mb-2 block text-sm font-medium text-slate-700">Performing Partner</label>
|
||||||
|
<select name="performing_partner_user_id" id="bulk-performing-partner" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm">
|
||||||
|
<option value="">Same as Engagement Partner</option>
|
||||||
|
{% for u in performing_partners %}<option value="{{ u.id }}" data-branch="{{ u.branch_id }}">{{ u.full_name or u.email }}</option>{% endfor %}
|
||||||
|
</select>
|
||||||
|
<p class="mt-1 text-xs text-slate-500">Defaults to the Engagement Partner when left blank.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-2 block text-sm font-medium text-slate-700">Default Manager</label>
|
<label class="mb-2 block text-sm font-medium text-slate-700">Default Manager</label>
|
||||||
<select name="assigned_manager_user_id" id="bulk-manager" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm">
|
<select name="assigned_manager_user_id" id="bulk-manager" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm">
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
{% if row.is_locked %}<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">This engagement is locked as historical record. It cannot be edited.</div>{% endif %}
|
{% if row.is_locked %}<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">This engagement is locked as historical record. It cannot be edited.</div>{% endif %}
|
||||||
<div class="grid gap-6 lg:grid-cols-2">
|
<div class="grid gap-6 lg:grid-cols-2">
|
||||||
<section class="rounded-2xl bg-white p-5 shadow-soft"><h3 class="text-sm font-semibold text-slate-900">Client & Service</h3><dl class="mt-4 space-y-3 text-sm"><div><dt class="text-slate-500">Client</dt><dd class="font-medium text-slate-900">{{ row.client.client_name if row.client else '-' }}</dd></div><div><dt class="text-slate-500">Service</dt><dd class="font-medium text-slate-900">{{ row.catalogue.service_name if row.catalogue else '-' }}</dd></div><div><dt class="text-slate-500">Financial Year</dt><dd>{{ row.financial_year or '-' }}</dd></div><div><dt class="text-slate-500">Assessment Year</dt><dd>{{ row.assessment_year or '-' }}</dd></div><div><dt class="text-slate-500">Engagement Type</dt><dd>{{ 'Assurance' if row.engagement_type == 'assurance' else 'Non-Assurance' }}</dd></div><div><dt class="text-slate-500">Original Due Date</dt><dd>{{ row.original_due_date or '-' }}</dd></div><div><dt class="text-slate-500">Expiry Date</dt><dd>{{ row.expiry_date or '-' }}</dd></div><div><dt class="text-slate-500">Current Due Date</dt><dd class="font-medium text-slate-900">{{ row.current_due_date or '-' }}{% if row.due_date_source %}<span class="ml-2 rounded-full bg-slate-100 px-2 py-1 text-xs font-medium text-slate-600">{{ row.due_date_source|replace('_',' ')|title }}</span>{% endif %}</dd></div><div><dt class="text-slate-500">Status</dt><dd>{{ 'Locked' if row.is_locked else row.status|replace('_',' ')|title }}{% if not row.is_active %} / Inactive{% endif %}</dd></div><div><dt class="text-slate-500">Period</dt><dd>{{ row.start_date or '-' }} to {{ row.end_date or '-' }}</dd></div></dl></section>
|
<section class="rounded-2xl bg-white p-5 shadow-soft"><h3 class="text-sm font-semibold text-slate-900">Client & Service</h3><dl class="mt-4 space-y-3 text-sm"><div><dt class="text-slate-500">Client</dt><dd class="font-medium text-slate-900">{{ row.client.client_name if row.client else '-' }}</dd></div><div><dt class="text-slate-500">Service</dt><dd class="font-medium text-slate-900">{{ row.catalogue.service_name if row.catalogue else '-' }}</dd></div><div><dt class="text-slate-500">Financial Year</dt><dd>{{ row.financial_year or '-' }}</dd></div><div><dt class="text-slate-500">Assessment Year</dt><dd>{{ row.assessment_year or '-' }}</dd></div><div><dt class="text-slate-500">Engagement Type</dt><dd>{{ 'Assurance' if row.engagement_type == 'assurance' else 'Non-Assurance' }}</dd></div><div><dt class="text-slate-500">Original Due Date</dt><dd>{{ row.original_due_date or '-' }}</dd></div><div><dt class="text-slate-500">Expiry Date</dt><dd>{{ row.expiry_date or '-' }}</dd></div><div><dt class="text-slate-500">Current Due Date</dt><dd class="font-medium text-slate-900">{{ row.current_due_date or '-' }}{% if row.due_date_source %}<span class="ml-2 rounded-full bg-slate-100 px-2 py-1 text-xs font-medium text-slate-600">{{ row.due_date_source|replace('_',' ')|title }}</span>{% endif %}</dd></div><div><dt class="text-slate-500">Status</dt><dd>{{ 'Locked' if row.is_locked else row.status|replace('_',' ')|title }}{% if not row.is_active %} / Inactive{% endif %}</dd></div><div><dt class="text-slate-500">Period</dt><dd>{{ row.start_date or '-' }} to {{ row.end_date or '-' }}</dd></div></dl></section>
|
||||||
<section class="rounded-2xl bg-white p-5 shadow-soft"><h3 class="text-sm font-semibold text-slate-900">Assignment</h3><dl class="mt-4 space-y-3 text-sm"><div><dt class="text-slate-500">Partner</dt><dd>{{ row.assigned_partner.full_name if row.assigned_partner and row.assigned_partner.full_name else (row.assigned_partner.email if row.assigned_partner else '-') }}</dd></div><div><dt class="text-slate-500">Manager</dt><dd>{{ row.assigned_manager.full_name if row.assigned_manager and row.assigned_manager.full_name else (row.assigned_manager.email if row.assigned_manager else '-') }}</dd></div><div><dt class="text-slate-500">Staff</dt><dd>{{ row.assigned_staff.full_name if row.assigned_staff and row.assigned_staff.full_name else (row.assigned_staff.email if row.assigned_staff else '-') }}</dd></div><div><dt class="text-slate-500">Review Partner</dt><dd>{{ row.review_partner.full_name if row.review_partner and row.review_partner.full_name else (row.review_partner.email if row.review_partner else '-') }}</dd></div></dl></section>
|
<section class="rounded-2xl bg-white p-5 shadow-soft"><h3 class="text-sm font-semibold text-slate-900">Assignment</h3><dl class="mt-4 space-y-3 text-sm"><div><dt class="text-slate-500">Partner</dt><dd>{{ row.assigned_partner.full_name if row.assigned_partner and row.assigned_partner.full_name else (row.assigned_partner.email if row.assigned_partner else '-') }}</dd></div><div><dt class="text-slate-500">Performing Partner</dt><dd>{{ row.performing_partner.full_name if row.performing_partner and row.performing_partner.full_name else (row.performing_partner.email if row.performing_partner else (row.assigned_partner.full_name if row.assigned_partner and row.assigned_partner.full_name else (row.assigned_partner.email if row.assigned_partner else '-'))) }}</dd></div><div><dt class="text-slate-500">Manager</dt><dd>{{ row.assigned_manager.full_name if row.assigned_manager and row.assigned_manager.full_name else (row.assigned_manager.email if row.assigned_manager else '-') }}</dd></div><div><dt class="text-slate-500">Staff</dt><dd>{{ row.assigned_staff.full_name if row.assigned_staff and row.assigned_staff.full_name else (row.assigned_staff.email if row.assigned_staff else '-') }}</dd></div><div><dt class="text-slate-500">Review Partner</dt><dd>{{ row.review_partner.full_name if row.review_partner and row.review_partner.full_name else (row.review_partner.email if row.review_partner else '-') }}</dd></div></dl></section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if row.quality_workflow_required %}
|
{% if row.quality_workflow_required %}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-semibold text-slate-900">{% if mode == 'edit' %}Edit Engagement{% else %}Assign Service to Client{% endif %}</h2>
|
<h2 class="text-xl font-semibold text-slate-900">{% if mode == 'edit' %}Edit Engagement{% else %}Assign Service to Client{% endif %}</h2>
|
||||||
<p class="text-sm text-slate-500">Engagements are maintained financial-year wise. Review partner is used only for assurance engagements of partnership audit firms.</p>
|
<p class="text-sm text-slate-500">Engagement Partner owns the assignment, Performing Partner executes or supervises it, and Review Partner independently reviews applicable assurance engagements.</p>
|
||||||
</div>
|
</div>
|
||||||
<a href="/services/engagements" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Back</a>
|
<a href="/services/engagements" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Back</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div><label class="mb-2 block text-sm font-medium text-slate-700">Assigned Partner</label><select name="assigned_partner_user_id" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm"><option value="">Not assigned</option>{% for u in assignable_users %}<option value="{{ u.id }}" {% if subscription and subscription.assigned_partner_user_id == u.id %}selected{% endif %}>{{ u.full_name or u.email }}</option>{% endfor %}</select></div>
|
<div><label class="mb-2 block text-sm font-medium text-slate-700">Assigned Partner</label><select name="assigned_partner_user_id" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm"><option value="">Not assigned</option>{% for u in assignable_users %}<option value="{{ u.id }}" {% if subscription and subscription.assigned_partner_user_id == u.id %}selected{% endif %}>{{ u.full_name or u.email }}</option>{% endfor %}</select></div>
|
||||||
|
<div><label class="mb-2 block text-sm font-medium text-slate-700">Performing Partner</label><select name="performing_partner_user_id" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm"><option value="">Use client default / Engagement Partner</option>{% for u in performing_partners or [] %}<option value="{{ u.id }}" {% if subscription and subscription.performing_partner_user_id == u.id %}selected{% endif %}>{{ u.full_name or u.email }}</option>{% endfor %}</select><p class="mt-1 text-xs text-slate-500">The Partner who directly performs or supervises this engagement.</p></div>
|
||||||
<div><label class="mb-2 block text-sm font-medium text-slate-700">Assigned Manager</label><select name="assigned_manager_user_id" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm"><option value="">Not assigned</option>{% for u in assignable_users %}<option value="{{ u.id }}" {% if subscription and subscription.assigned_manager_user_id == u.id %}selected{% endif %}>{{ u.full_name or u.email }}</option>{% endfor %}</select></div>
|
<div><label class="mb-2 block text-sm font-medium text-slate-700">Assigned Manager</label><select name="assigned_manager_user_id" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm"><option value="">Not assigned</option>{% for u in assignable_users %}<option value="{{ u.id }}" {% if subscription and subscription.assigned_manager_user_id == u.id %}selected{% endif %}>{{ u.full_name or u.email }}</option>{% endfor %}</select></div>
|
||||||
<div><label class="mb-2 block text-sm font-medium text-slate-700">Assigned Staff</label><select name="assigned_staff_user_id" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm"><option value="">Not assigned</option>{% for u in assignable_users %}<option value="{{ u.id }}" {% if subscription and subscription.assigned_staff_user_id == u.id %}selected{% endif %}>{{ u.full_name or u.email }}</option>{% endfor %}</select></div>
|
<div><label class="mb-2 block text-sm font-medium text-slate-700">Assigned Staff</label><select name="assigned_staff_user_id" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm"><option value="">Not assigned</option>{% for u in assignable_users %}<option value="{{ u.id }}" {% if subscription and subscription.assigned_staff_user_id == u.id %}selected{% endif %}>{{ u.full_name or u.email }}</option>{% endfor %}</select></div>
|
||||||
<div><label class="mb-2 block text-sm font-medium text-slate-700">Review Partner</label><select name="review_partner_user_id" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm"><option value="">Use client default / Not required</option>{% for u in review_partners or [] %}<option value="{{ u.id }}" {% if subscription and subscription.review_partner_user_id == u.id %}selected{% endif %}>{{ u.full_name or u.email }}</option>{% endfor %}</select><p class="mt-1 text-xs text-slate-500">Saved only for assurance engagements of partnership audit firms.</p></div>
|
<div><label class="mb-2 block text-sm font-medium text-slate-700">Review Partner</label><select name="review_partner_user_id" class="w-full rounded-xl border border-slate-300 px-4 py-2 text-sm"><option value="">Use client default / Not required</option>{% for u in review_partners or [] %}<option value="{{ u.id }}" {% if subscription and subscription.review_partner_user_id == u.id %}selected{% endif %}>{{ u.full_name or u.email }}</option>{% endfor %}</select><p class="mt-1 text-xs text-slate-500">Saved only for assurance engagements of partnership audit firms.</p></div>
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
<td class="px-4 py-3 text-xs text-slate-600"><div>FY: {{ row.financial_year or '-' }}</div><div>AY: {{ row.assessment_year or '-' }}</div></td>
|
<td class="px-4 py-3 text-xs text-slate-600"><div>FY: {{ row.financial_year or '-' }}</div><div>AY: {{ row.assessment_year or '-' }}</div></td>
|
||||||
<td class="px-4 py-3 text-xs text-slate-600"><div>Current: <span class="font-medium text-slate-900">{{ row.current_due_date or '-' }}</span></div><div>Original: {{ row.original_due_date or '-' }}</div>{% if row.expiry_date %}<div>Expiry: {{ row.expiry_date }}</div>{% endif %}{% if row.due_date_source %}<div class="text-slate-500">{{ row.due_date_source|replace('_',' ')|title }}</div>{% endif %}</td>
|
<td class="px-4 py-3 text-xs text-slate-600"><div>Current: <span class="font-medium text-slate-900">{{ row.current_due_date or '-' }}</span></div><div>Original: {{ row.original_due_date or '-' }}</div>{% if row.expiry_date %}<div>Expiry: {{ row.expiry_date }}</div>{% endif %}{% if row.due_date_source %}<div class="text-slate-500">{{ row.due_date_source|replace('_',' ')|title }}</div>{% endif %}</td>
|
||||||
<td class="px-4 py-3 text-sm text-slate-700">{{ 'Assurance' if row.engagement_type == 'assurance' else 'Non-Assurance' }}</td>
|
<td class="px-4 py-3 text-sm text-slate-700">{{ 'Assurance' if row.engagement_type == 'assurance' else 'Non-Assurance' }}</td>
|
||||||
<td class="px-4 py-3 text-xs text-slate-600"><div>Partner: {{ row.assigned_partner.full_name if row.assigned_partner and row.assigned_partner.full_name else (row.assigned_partner.email if row.assigned_partner else '-') }}</div><div>Manager: {{ row.assigned_manager.full_name if row.assigned_manager and row.assigned_manager.full_name else (row.assigned_manager.email if row.assigned_manager else '-') }}</div><div>Staff: {{ row.assigned_staff.full_name if row.assigned_staff and row.assigned_staff.full_name else (row.assigned_staff.email if row.assigned_staff else '-') }}</div><div>Review: {{ row.review_partner.full_name if row.review_partner and row.review_partner.full_name else (row.review_partner.email if row.review_partner else '-') }}</div></td>
|
<td class="px-4 py-3 text-xs text-slate-600"><div>Partner: {{ row.assigned_partner.full_name if row.assigned_partner and row.assigned_partner.full_name else (row.assigned_partner.email if row.assigned_partner else '-') }}</div><div>Performing: {{ row.performing_partner.full_name if row.performing_partner and row.performing_partner.full_name else (row.performing_partner.email if row.performing_partner else (row.assigned_partner.full_name if row.assigned_partner and row.assigned_partner.full_name else (row.assigned_partner.email if row.assigned_partner else '-'))) }}</div><div>Manager: {{ row.assigned_manager.full_name if row.assigned_manager and row.assigned_manager.full_name else (row.assigned_manager.email if row.assigned_manager else '-') }}</div><div>Staff: {{ row.assigned_staff.full_name if row.assigned_staff and row.assigned_staff.full_name else (row.assigned_staff.email if row.assigned_staff else '-') }}</div><div>Review: {{ row.review_partner.full_name if row.review_partner and row.review_partner.full_name else (row.review_partner.email if row.review_partner else '-') }}</div></td>
|
||||||
<td class="px-4 py-3 text-sm"><span class="rounded-full px-2 py-1 text-xs font-medium {% if row.is_locked %}bg-amber-100 text-amber-700{% elif row.is_active and row.status == 'active' %}bg-emerald-100 text-emerald-700{% else %}bg-slate-200 text-slate-700{% endif %}">{{ 'Locked' if row.is_locked else row.status|replace('_',' ')|title }}{% if not row.is_active %} / Inactive{% endif %}</span></td>
|
<td class="px-4 py-3 text-sm"><span class="rounded-full px-2 py-1 text-xs font-medium {% if row.is_locked %}bg-amber-100 text-amber-700{% elif row.is_active and row.status == 'active' %}bg-emerald-100 text-emerald-700{% else %}bg-slate-200 text-slate-700{% endif %}">{{ 'Locked' if row.is_locked else row.status|replace('_',' ')|title }}{% if not row.is_active %} / Inactive{% endif %}</span></td>
|
||||||
<td class="px-4 py-3 text-right text-sm"><a href="/services/engagements/{{ row.id }}" class="font-medium text-brand-700 hover:underline">View</a>{% if can_manage and not row.is_locked %}<a href="/services/engagements/{{ row.id }}/edit" class="ml-3 font-medium text-brand-700 hover:underline">Edit</a>{% endif %}</td>
|
<td class="px-4 py-3 text-right text-sm"><a href="/services/engagements/{{ row.id }}" class="font-medium text-brand-700 hover:underline">View</a>{% if can_manage and not row.is_locked %}<a href="/services/engagements/{{ row.id }}/edit" class="ml-3 font-medium text-brand-700 hover:underline">Edit</a>{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ def _is_manager_for_engagement(engagement: ClientServiceSubscription, user) -> b
|
|||||||
def _is_partner_for_engagement(engagement: ClientServiceSubscription, user) -> bool:
|
def _is_partner_for_engagement(engagement: ClientServiceSubscription, user) -> bool:
|
||||||
return user.id in {
|
return user.id in {
|
||||||
_safe_int(getattr(engagement, "assigned_partner_user_id", None)),
|
_safe_int(getattr(engagement, "assigned_partner_user_id", None)),
|
||||||
|
_safe_int(getattr(engagement, "performing_partner_user_id", None)),
|
||||||
_safe_int(getattr(engagement, "review_partner_user_id", None)),
|
_safe_int(getattr(engagement, "review_partner_user_id", None)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user