diff --git a/app/modules/firm_admin_dashboard/service.py b/app/modules/firm_admin_dashboard/service.py index 6e7d802..5d07d87 100644 --- a/app/modules/firm_admin_dashboard/service.py +++ b/app/modules/firm_admin_dashboard/service.py @@ -18,6 +18,7 @@ from app.modules.core.rbac.models import Role, UserRole from app.modules.core.tenancy.models import Branch, FinancialYear, Tenant from app.modules.clients.models import Client from app.modules.employees.models import Employee +from app.modules.consultants.models import ConsultantProfile from app.modules.services.models import ( FirmServiceSelection, FirmServiceTaskTemplate, @@ -268,6 +269,7 @@ ONBOARDING_ROLE_CONFIG: dict[str, dict[str, str]] = { "default_designation": "Partner", "default_department": "Management", "default_employment_type": "full_time", + "requires_employee": True, }, "manager": { "role_name": "Branch Manager", @@ -275,6 +277,7 @@ ONBOARDING_ROLE_CONFIG: dict[str, dict[str, str]] = { "default_designation": "Branch Manager", "default_department": "Operations", "default_employment_type": "full_time", + "requires_employee": True, }, "staff": { "role_name": "Staff", @@ -282,6 +285,15 @@ ONBOARDING_ROLE_CONFIG: dict[str, dict[str, str]] = { "default_designation": "Staff", "default_department": "Operations", "default_employment_type": "full_time", + "requires_employee": True, + }, + "consultant": { + "role_name": "Consultant", + "label": "Consultant", + "default_designation": "Consultant", + "default_department": "Consulting", + "default_employment_type": "consultant", + "requires_employee": False, }, } @@ -447,6 +459,13 @@ def create_firm_internal_user( designation: str | None = None, date_of_joining: str | None = None, employment_type: str | None = None, + consultant_type: str | None = None, + firm_name: str | None = None, + specialisation: str | None = None, + gstin: str | None = None, + pan: str | None = None, + address: str | None = None, + remarks: str | None = None, ) -> dict[str, Any]: actor_roles = set(get_user_role_names(db, actor.id)) if not actor_roles.intersection(FIRM_ADMIN_ROLES): @@ -501,17 +520,60 @@ def create_firm_internal_user( db.flush() db.add(UserRole(user_id=user.id, role_id=role.id)) - employee_link_result = _ensure_employee_for_onboarded_user( - db, - actor=actor, - user_obj=user, - employee_code=employee_code, - mobile=mobile, - department=(department or config.get("default_department") or ""), - designation=(designation or config.get("default_designation") or ""), - date_of_joining=date_of_joining, - employment_type=(employment_type or config.get("default_employment_type") or "full_time"), - ) + consultant_profile = None + if role_key == "consultant": + duplicate_profile = db.execute( + select(ConsultantProfile).where( + ConsultantProfile.tenant_id == int(tenant_id), + ConsultantProfile.email == email_clean, + ) + ).scalar_one_or_none() + if duplicate_profile: + raise ValueError("A consultant profile with this email already exists in this firm.") + + consultant_profile = ConsultantProfile( + tenant_id=int(tenant_id), + branch_id=int(branch.id), + user_id=int(user.id), + consultant_type=(consultant_type or "external_consultant").strip() or "external_consultant", + firm_name=(firm_name or "").strip() or None, + contact_person=name_clean, + email=email_clean, + mobile=(mobile or "").strip() or None, + specialisation=(specialisation or "").strip() or None, + gstin=(gstin or "").strip().upper() or None, + pan=(pan or "").strip().upper() or None, + address=(address or "").strip() or None, + status="active", + onboarding_status="invited", + is_platform_partner=False, + is_franchise_partner=False, + is_saas_customer=False, + is_active=True, + remarks=(remarks or "").strip() or None, + created_by_user_id=actor.id, + updated_by_user_id=actor.id, + ) + db.add(consultant_profile) + db.flush() + employee_link_result = { + "required": False, + "created": False, + "linked_existing": False, + "employee_id": None, + } + else: + employee_link_result = _ensure_employee_for_onboarded_user( + db, + actor=actor, + user_obj=user, + employee_code=employee_code, + mobile=mobile, + department=(department or config.get("default_department") or ""), + designation=(designation or config.get("default_designation") or ""), + date_of_joining=date_of_joining, + employment_type=(employment_type or config.get("default_employment_type") or "full_time"), + ) try: db.commit() @@ -540,6 +602,7 @@ def create_firm_internal_user( "role_label": config["label"], "branch": branch, "employee_link_result": employee_link_result, + "consultant_profile": consultant_profile, "invite_url": invite_url, "email_status": email_status, "email_error": email_error, diff --git a/app/modules/firm_admin_dashboard/templates/firm_admin_dashboard/partials/users.html b/app/modules/firm_admin_dashboard/templates/firm_admin_dashboard/partials/users.html index b835f83..66872e6 100644 --- a/app/modules/firm_admin_dashboard/templates/firm_admin_dashboard/partials/users.html +++ b/app/modules/firm_admin_dashboard/templates/firm_admin_dashboard/partials/users.html @@ -8,6 +8,7 @@ + Add Partner + Add Manager + Add Staff + + Add Consultant Import Users Template Manage Users diff --git a/app/modules/firm_admin_dashboard/templates/firm_admin_dashboard/partials/wizards.html b/app/modules/firm_admin_dashboard/templates/firm_admin_dashboard/partials/wizards.html index 6d3f32b..d3a186b 100644 --- a/app/modules/firm_admin_dashboard/templates/firm_admin_dashboard/partials/wizards.html +++ b/app/modules/firm_admin_dashboard/templates/firm_admin_dashboard/partials/wizards.html @@ -5,6 +5,7 @@
Add Partner

Create partner user, link employee master and generate invite.

Open
Add Manager

Creates user with Branch Manager role.

Open
Add Staff

Create staff user, employee master and invite link.

Open
+
Add Consultant

Create consultant login, consultant profile and invite link without creating an employee record.

Open
{% for wizard in wizards %}
{{ wizard.title }}

{{ wizard.desc }}

Open
{% endfor %} diff --git a/app/modules/firm_admin_dashboard/templates/firm_admin_dashboard/user_onboarding.html b/app/modules/firm_admin_dashboard/templates/firm_admin_dashboard/user_onboarding.html index 0b120a5..fa8ef3b 100644 --- a/app/modules/firm_admin_dashboard/templates/firm_admin_dashboard/user_onboarding.html +++ b/app/modules/firm_admin_dashboard/templates/firm_admin_dashboard/user_onboarding.html @@ -8,7 +8,7 @@

Firm Admin User Onboarding

Add {{ role_config.label if role_config else 'Firm User' }}

-

Create the user account, assign branch and role, create/link employee master, generate invite link and attempt invite email.

+

Create the user account, assign branch and role, create the appropriate profile, generate invite link and attempt invite email.

Back to Users @@ -20,7 +20,7 @@ {% elif not branches %}
- No active branch is available for this firm. Create a branch first, then add Partner / Manager / Staff. + No active branch is available for this firm. Create a branch first, then add Partner / Manager / Staff / Consultant.
{% elif not role_config %}
@@ -67,6 +67,7 @@ Mobile + {% if role_key != 'consultant' %} + {% else %} + + + + + + + + {% endif %}
- The system will generate an invite token and show the fallback invite link after saving. The user will set their own password from that link. + The system will create the user and role mapping, generate an invite token and show the fallback invite link after saving. Consultants are created in the Consultant Profile master and are not added to the Employee/payroll master.
diff --git a/app/modules/firm_admin_dashboard/templates/firm_admin_dashboard/user_onboarding_done.html b/app/modules/firm_admin_dashboard/templates/firm_admin_dashboard/user_onboarding_done.html index d0950f7..6880185 100644 --- a/app/modules/firm_admin_dashboard/templates/firm_admin_dashboard/user_onboarding_done.html +++ b/app/modules/firm_admin_dashboard/templates/firm_admin_dashboard/user_onboarding_done.html @@ -6,7 +6,7 @@

User Created

{{ result.role_label }} invite is ready

-

The user account, role mapping, branch assignment and employee master link have been completed.

+

The user account, role mapping and branch assignment have been completed. The appropriate employee or consultant profile has also been created.

@@ -14,7 +14,11 @@
User
{{ result.user.full_name or result.user.email }}
{{ result.user.email }}
Role
{{ result.role_name }}
Branch
{{ result.branch.name }}
+ {% if result.consultant_profile %} +
Consultant Profile
Created #{{ result.consultant_profile.id }}
{{ result.consultant_profile.specialisation or result.consultant_profile.consultant_type }}
+ {% else %}
Employee Link
{% if result.employee_link_result.employee_id %}Linked #{{ result.employee_link_result.employee_id }}{% else %}Not linked{% endif %}
+ {% endif %}
Invite Email
{{ result.email_status }}
{% if result.email_error %}
{{ result.email_error }}
{% endif %}
diff --git a/app/modules/firm_admin_dashboard/ui.py b/app/modules/firm_admin_dashboard/ui.py index fac31f6..4ed41f2 100644 --- a/app/modules/firm_admin_dashboard/ui.py +++ b/app/modules/firm_admin_dashboard/ui.py @@ -136,6 +136,13 @@ def user_onboarding_submit( designation: str = Form(""), date_of_joining: str = Form(""), employment_type: str = Form("full_time"), + consultant_type: str = Form("external_consultant"), + firm_name: str = Form(""), + specialisation: str = Form(""), + gstin: str = Form(""), + pan: str = Form(""), + address: str = Form(""), + remarks: str = Form(""), csrf_token: str = Form(...), ): validate_csrf(request, csrf_token) @@ -164,6 +171,13 @@ def user_onboarding_submit( designation=designation, date_of_joining=date_of_joining, employment_type=employment_type, + consultant_type=consultant_type, + firm_name=firm_name, + specialisation=specialisation, + gstin=gstin, + pan=pan, + address=address, + remarks=remarks, ) except Exception as exc: db.rollback() @@ -185,6 +199,13 @@ def user_onboarding_submit( "designation": designation, "date_of_joining": date_of_joining, "employment_type": employment_type, + "consultant_type": consultant_type, + "firm_name": firm_name, + "specialisation": specialisation, + "gstin": gstin, + "pan": pan, + "address": address, + "remarks": remarks, }, ), status_code=400,