From 5a135a65a80941006ebb84759c4fc30dc2519662 Mon Sep 17 00:00:00 2001 From: A R R R Associates Date: Wed, 5 Aug 2026 10:52:36 +0530 Subject: [PATCH] Add performing partner to clients and engagements --- .../versions/20260805_performing_partner.py | 44 +++++++++++++++++++ app/modules/clients/models.py | 1 + app/modules/clients/repository.py | 15 +++++-- app/modules/clients/schemas.py | 3 ++ .../clients/templates/clients/detail.html | 1 + .../templates/clients/partials/form.html | 13 ++++++ app/modules/clients/ui.py | 7 +++ app/modules/partner_dashboard/service.py | 19 ++------ app/modules/services/client_services.py | 2 + app/modules/services/engagements_ui.py | 19 ++++++++ app/modules/services/models.py | 2 + .../services/engagements/bulk_form.html | 9 ++++ .../services/engagements/detail.html | 2 +- .../templates/services/engagements/form.html | 3 +- .../templates/services/engagements/list.html | 2 +- app/modules/work_detail/service.py | 1 + 16 files changed, 122 insertions(+), 21 deletions(-) create mode 100644 alembic/versions/20260805_performing_partner.py diff --git a/alembic/versions/20260805_performing_partner.py b/alembic/versions/20260805_performing_partner.py new file mode 100644 index 0000000..f65b1f4 --- /dev/null +++ b/alembic/versions/20260805_performing_partner.py @@ -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") diff --git a/app/modules/clients/models.py b/app/modules/clients/models.py index bbea0a1..411112d 100644 --- a/app/modules/clients/models.py +++ b/app/modules/clients/models.py @@ -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) 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) + 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) 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) diff --git a/app/modules/clients/repository.py b/app/modules/clients/repository.py index 72a2263..77be726 100644 --- a/app/modules/clients/repository.py +++ b/app/modules/clients/repository.py @@ -41,7 +41,10 @@ def build_clients_query( select(ClientServiceSubscription.id).where( ClientServiceSubscription.tenant_id == tenant_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), ) ) if viewer_partner_id is not None else None @@ -133,7 +136,10 @@ def list_clients( 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, + or_( + ClientServiceSubscription.performing_partner_user_id == viewer_partner_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)) @@ -149,7 +155,10 @@ def has_partner_review_access(db: Session, *, tenant_id: int, client_id: int, pa select(ClientServiceSubscription.id).where( ClientServiceSubscription.tenant_id == tenant_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), ).limit(1) ).scalar_one_or_none()) diff --git a/app/modules/clients/schemas.py b/app/modules/clients/schemas.py index d60e0a7..c88929f 100644 --- a/app/modules/clients/schemas.py +++ b/app/modules/clients/schemas.py @@ -24,6 +24,7 @@ class ClientBase(BaseModel): client_group_id: Optional[int] = None group_relationship: Optional[str] = None is_group_head: bool = False + default_performing_partner_user_id: Optional[int] = None default_review_partner_user_id: Optional[int] = None referred_by_consultant_id: Optional[int] = None primary_consultant_id: Optional[int] = None @@ -219,6 +220,7 @@ class ClientUpdate(BaseModel): client_group_id: Optional[int] = None group_relationship: Optional[str] = None is_group_head: Optional[bool] = None + default_performing_partner_user_id: Optional[int] = None default_review_partner_user_id: Optional[int] = None referred_by_consultant_id: Optional[int] = None primary_consultant_id: Optional[int] = None @@ -398,6 +400,7 @@ class ClientOut(BaseModel): tenant_id: int branch_id: int partner_id: Optional[int] = None + default_performing_partner_user_id: Optional[int] = None default_review_partner_user_id: Optional[int] = None engagement_mode: str client_code: str diff --git a/app/modules/clients/templates/clients/detail.html b/app/modules/clients/templates/clients/detail.html index 9a01f15..91de7f6 100644 --- a/app/modules/clients/templates/clients/detail.html +++ b/app/modules/clients/templates/clients/detail.html @@ -117,6 +117,7 @@
Audit Firm: {{ row.assoc_firm_tenant_id or row.tenant_id or '-' }}
Branch: {{ row.branch_id or '-' }}
Partner: {{ row.assoc_partner_user_id or row.partner_id or '-' }}
+
Default Performing Partner: {{ row.default_performing_partner_user_id or row.partner_id or '-' }}
Default Review Partner: {{ row.default_review_partner_user_id or '-' }}
Referred by: {{ consultant_summary.referred_by.contact_person if consultant_summary and consultant_summary.referred_by else 'Direct / Not recorded' }}
Primary consultant: {{ consultant_summary.primary.contact_person if consultant_summary and consultant_summary.primary else 'Firm managed' }}
diff --git a/app/modules/clients/templates/clients/partials/form.html b/app/modules/clients/templates/clients/partials/form.html index 03f1584..693f0fa 100644 --- a/app/modules/clients/templates/clients/partials/form.html +++ b/app/modules/clients/templates/clients/partials/form.html @@ -316,6 +316,19 @@ +
+ + +

Defaults the Partner who directly performs or supervises the engagement. When blank, the Engagement Partner is used.

+
+
+ + {% for u in performing_partners %}{% endfor %} + +

Defaults to the Engagement Partner when left blank.

+
+
{% for u in assignable_users %}{% endfor %}
+

The Partner who directly performs or supervises this engagement.

Saved only for assurance engagements of partnership audit firms.

diff --git a/app/modules/services/templates/services/engagements/list.html b/app/modules/services/templates/services/engagements/list.html index 1d38add..4a4294c 100644 --- a/app/modules/services/templates/services/engagements/list.html +++ b/app/modules/services/templates/services/engagements/list.html @@ -87,7 +87,7 @@
FY: {{ row.financial_year or '-' }}
AY: {{ row.assessment_year or '-' }}
Current: {{ row.current_due_date or '-' }}
Original: {{ row.original_due_date or '-' }}
{% if row.expiry_date %}
Expiry: {{ row.expiry_date }}
{% endif %}{% if row.due_date_source %}
{{ row.due_date_source|replace('_',' ')|title }}
{% endif %} {{ 'Assurance' if row.engagement_type == 'assurance' else 'Non-Assurance' }} -
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 '-') }}
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 '-') }}
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 '-') }}
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 '-') }}
+
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 '-') }}
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 '-'))) }}
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 '-') }}
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 '-') }}
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 '-') }}
{{ 'Locked' if row.is_locked else row.status|replace('_',' ')|title }}{% if not row.is_active %} / Inactive{% endif %} View{% if can_manage and not row.is_locked %}Edit{% endif %} diff --git a/app/modules/work_detail/service.py b/app/modules/work_detail/service.py index dc868aa..eb44ab7 100644 --- a/app/modules/work_detail/service.py +++ b/app/modules/work_detail/service.py @@ -112,6 +112,7 @@ def _is_manager_for_engagement(engagement: ClientServiceSubscription, user) -> b def _is_partner_for_engagement(engagement: ClientServiceSubscription, user) -> bool: return user.id in { _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)), }