diff --git a/app/modules/registrations/templates/registrations/bulk.html b/app/modules/registrations/templates/registrations/bulk.html new file mode 100644 index 0000000..d3c48b1 --- /dev/null +++ b/app/modules/registrations/templates/registrations/bulk.html @@ -0,0 +1,373 @@ +{% extends "ui/templates/base/layout.html" %} +{% block content %} +
+ {% set _uiux_partner_role_text = (current_user_roles or [])|join('|')|lower %} + {% if 'partner' in _uiux_partner_role_text %} + {% include "ui/templates/components/partner_navigation_v2.html" %} + {% endif %} + +
+
+

Bulk Registration Addition

+

+ Add the same registration type to multiple clients while entering each client’s unique registration number. +

+
+ + Registrations Dashboard + +
+ + {% if created or skipped or invalid %} +
+
+
Created
+
{{ created }}
+
+
+
Duplicates Skipped
+
{{ skipped }}
+
+
+
Invalid / Missing
+
{{ invalid }}
+
+
+ {% endif %} + +
+ + +
+

Common Registration Settings

+
+
+ + +
+ +
+ + +

+ Per-client Business Unit and Branch selections in the table override this default. +

+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ + +
+
+
+ +
+
+
+
+ + +
+ +
+
+ 0 clients selected +
+
+ +
+ + + + + + + + + + + + + + + + {% for client in clients %} + {% set client_businesses = businesses_by_client.get(client.id, []) %} + {% set client_branches = branches_by_client.get(client.id, []) %} + + + + + + + + + + + + {% else %} + + + + {% endfor %} + +
+ + ClientPAN / Existing GSTINRegistration Number *Business UnitClient BranchLegal NameTrade / Unit NameState
+ + +
{{ client.client_name }}
+
{{ client.client_code or '' }}
+
+
PAN: {{ client.pan or '-' }}
+
GSTIN: {{ client.gstin or '-' }}
+
+ + + + + + + + + + + +
+ No active clients are available in the current Firm Branch. +
+
+
+ +
+

+ Rows without a registration number are treated as invalid. Existing registration numbers are skipped without changing their records. +

+
+ + Cancel + + +
+
+
+
+ + +{% endblock %} diff --git a/app/modules/registrations/ui.py b/app/modules/registrations/ui.py index e5171c0..b995045 100644 --- a/app/modules/registrations/ui.py +++ b/app/modules/registrations/ui.py @@ -3,11 +3,12 @@ from datetime import date from fastapi import APIRouter, Form, Request from fastapi.responses import HTMLResponse, RedirectResponse from sqlalchemy import or_, select +from sqlalchemy.exc import IntegrityError from app.core.db.common import CommonSessionLocal from app.core.security.csrf import get_or_create_csrf_token, validate_csrf from app.core.security.session_auth import get_current_user from app.core.templating import templates -from app.modules.clients.models import Client +from app.modules.clients.models import Client, ClientBusinessUnit, ClientBranch from app.modules.consultants.models import ConsultantProfile from app.modules.core.rbac.deps import get_user_permissions, get_user_roles from app.modules.core.rbac.permission_guard import require_permission @@ -81,6 +82,319 @@ async def create_doc_template(request:Request,registration_type_id:int=Form(...) user=_user(request,db);validate_csrf(request,csrf_token);require_permission(db,user,"clients.edit") db.add(RegistrationDocumentTemplate(tenant_id=_tenant(request,user),registration_type_id=registration_type_id,action_type=action_type,document_name=document_name.strip(),description=description or None,mandatory=mandatory));db.commit();return RedirectResponse('/registrations/masters',303) + + +@router.get("/bulk", response_class=HTMLResponse) +def bulk_registration_page( + request: Request, + created: int = 0, + skipped: int = 0, + invalid: int = 0, +): + with CommonSessionLocal() as db: + user = _user(request, db) + if not user: + return RedirectResponse("/login", 303) + require_permission(db, user, "clients.edit") + seed_registration_types(db) + + tenant_id = _tenant(request, user) + branch_id = _branch(request, user) + + clients_query = select(Client).where( + Client.tenant_id == tenant_id, + Client.is_active.is_(True), + ) + if branch_id: + clients_query = clients_query.where(Client.branch_id == branch_id) + + clients = db.execute( + clients_query.order_by(Client.client_name, Client.client_code) + ).scalars().all() + client_ids = [row.id for row in clients] + + business_units = [] + client_branches = [] + if client_ids: + business_units = db.execute( + select(ClientBusinessUnit).where( + ClientBusinessUnit.tenant_id == tenant_id, + ClientBusinessUnit.client_id.in_(client_ids), + ClientBusinessUnit.is_active.is_(True), + ).order_by( + ClientBusinessUnit.client_id, + ClientBusinessUnit.is_primary.desc(), + ClientBusinessUnit.business_name, + ) + ).scalars().all() + client_branches = db.execute( + select(ClientBranch).where( + ClientBranch.tenant_id == tenant_id, + ClientBranch.client_id.in_(client_ids), + ClientBranch.is_active.is_(True), + ).order_by( + ClientBranch.client_id, + ClientBranch.is_primary.desc(), + ClientBranch.branch_name, + ) + ).scalars().all() + + businesses_by_client: dict[int, list[ClientBusinessUnit]] = {} + for row in business_units: + businesses_by_client.setdefault(row.client_id, []).append(row) + + branches_by_client: dict[int, list[ClientBranch]] = {} + for row in client_branches: + branches_by_client.setdefault(row.client_id, []).append(row) + + types = db.execute( + select(RegistrationType).where( + RegistrationType.is_active.is_(True) + ).order_by(RegistrationType.sort_order, RegistrationType.name) + ).scalars().all() + + rules = db.execute( + select(RegistrationLifecycleRule, RegistrationType).join( + RegistrationType, + RegistrationType.id == RegistrationLifecycleRule.registration_type_id, + ).where( + or_( + RegistrationLifecycleRule.tenant_id.is_(None), + RegistrationLifecycleRule.tenant_id == tenant_id, + ), + RegistrationLifecycleRule.is_active.is_(True), + ).order_by(RegistrationType.name, RegistrationLifecycleRule.id) + ).all() + + consultants = db.execute( + select(ConsultantProfile).where( + ConsultantProfile.tenant_id == tenant_id, + ConsultantProfile.is_active.is_(True), + ) + ).scalars().all() + + return templates.TemplateResponse( + "modules/registrations/templates/registrations/bulk.html", + _ctx( + request, + user, + db, + title="Bulk Registration Addition", + clients=clients, + businesses_by_client=businesses_by_client, + branches_by_client=branches_by_client, + types=types, + rules=rules, + consultants=consultants, + created=created, + skipped=skipped, + invalid=invalid, + ), + ) + + +@router.post("/bulk") +async def bulk_registration_submit( + request: Request, + client_ids: list[int] = Form([]), + registration_type_id: int = Form(...), + default_scope: str = Form("client"), + jurisdiction: str = Form(""), + state_code: str = Form(""), + issue_date: str = Form(""), + effective_from: str = Form(""), + valid_until: str = Form(""), + next_action_date: str = Form(""), + lifecycle_rule_id: str = Form(""), + responsible_party: str = Form("firm"), + assigned_consultant_id: str = Form(""), + auto_create_task: bool = Form(False), + notes: str = Form(""), + csrf_token: str = Form(...), +): + validate_csrf(request, csrf_token) + + with CommonSessionLocal() as db: + user = _user(request, db) + if not user: + return RedirectResponse("/login", 303) + require_permission(db, user, "clients.edit") + + tenant_id = _tenant(request, user) + branch_id = _branch(request, user) + + registration_type = db.get(RegistrationType, registration_type_id) + if not registration_type or not registration_type.is_active: + return RedirectResponse("/registrations/bulk?invalid=1", 303) + + selected_ids = list(dict.fromkeys(int(value) for value in client_ids)) + if not selected_ids: + return RedirectResponse("/registrations/bulk?invalid=1", 303) + + clients_query = select(Client).where( + Client.tenant_id == tenant_id, + Client.id.in_(selected_ids), + Client.is_active.is_(True), + ) + if branch_id: + clients_query = clients_query.where(Client.branch_id == branch_id) + + permitted_clients = db.execute(clients_query).scalars().all() + clients_by_id = {row.id: row for row in permitted_clients} + + form = await request.form() + created = 0 + skipped = 0 + invalid = 0 + + for client_id in selected_ids: + client = clients_by_id.get(client_id) + if not client: + invalid += 1 + continue + + registration_number = ( + str(form.get(f"registration_number_{client_id}", "")) + .strip() + .upper() + ) + if not registration_number: + invalid += 1 + continue + + business_unit_id = None + client_branch_id = None + + raw_business_id = str( + form.get(f"business_unit_id_{client_id}", "") + ).strip() + raw_branch_id = str( + form.get(f"client_branch_id_{client_id}", "") + ).strip() + + if raw_business_id: + business = db.get(ClientBusinessUnit, int(raw_business_id)) + if ( + not business + or business.tenant_id != tenant_id + or business.client_id != client.id + or not business.is_active + ): + invalid += 1 + continue + business_unit_id = business.id + + if raw_branch_id: + client_branch = db.get(ClientBranch, int(raw_branch_id)) + if ( + not client_branch + or client_branch.tenant_id != tenant_id + or client_branch.client_id != client.id + or not client_branch.is_active + ): + invalid += 1 + continue + client_branch_id = client_branch.id + business_unit_id = client_branch.business_unit_id + + if default_scope == "primary_business" and not business_unit_id: + business = db.execute( + select(ClientBusinessUnit).where( + ClientBusinessUnit.tenant_id == tenant_id, + ClientBusinessUnit.client_id == client.id, + ClientBusinessUnit.is_active.is_(True), + ).order_by( + ClientBusinessUnit.is_primary.desc(), + ClientBusinessUnit.id, + ) + ).scalars().first() + if not business: + invalid += 1 + continue + business_unit_id = business.id + + if default_scope == "primary_branch" and not client_branch_id: + client_branch = db.execute( + select(ClientBranch).where( + ClientBranch.tenant_id == tenant_id, + ClientBranch.client_id == client.id, + ClientBranch.is_active.is_(True), + ).order_by( + ClientBranch.is_primary.desc(), + ClientBranch.id, + ) + ).scalars().first() + if not client_branch: + invalid += 1 + continue + client_branch_id = client_branch.id + business_unit_id = client_branch.business_unit_id + + existing = db.execute( + select(ClientRegistration).where( + ClientRegistration.tenant_id == tenant_id, + ClientRegistration.registration_type_id == registration_type.id, + ClientRegistration.registration_number == registration_number, + ) + ).scalar_one_or_none() + if existing: + skipped += 1 + continue + + legal_name = str(form.get(f"legal_name_{client_id}", "")).strip() + trade_name = str(form.get(f"trade_name_{client_id}", "")).strip() + row_state = str(form.get(f"state_{client_id}", "")).strip() + + db.add( + ClientRegistration( + tenant_id=tenant_id, + branch_id=client.branch_id, + client_id=client.id, + business_unit_id=business_unit_id, + client_branch_id=client_branch_id, + registration_type_id=registration_type.id, + registration_number=registration_number, + legal_name=legal_name or client.client_name, + trade_name=trade_name or client.trade_name or None, + state=row_state or client.state or None, + jurisdiction=jurisdiction.strip() or row_state or client.state or None, + state_code=state_code.strip().upper() or None, + issue_date=_d(issue_date), + effective_from=_d(effective_from), + valid_until=_d(valid_until), + next_action_date=_d(next_action_date), + lifecycle_rule_id=int(lifecycle_rule_id) if lifecycle_rule_id else None, + responsible_party=responsible_party, + assigned_user_id=client.partner_id, + assigned_consultant_id=( + int(assigned_consultant_id) + if assigned_consultant_id + else None + ), + auto_create_task=auto_create_task, + status="active", + notes=notes.strip() or None, + created_by_user_id=user.id, + ) + ) + + try: + db.flush() + except IntegrityError: + db.rollback() + skipped += 1 + continue + + created += 1 + + db.commit() + run_registration_lifecycle_once(db) + + return RedirectResponse( + f"/registrations/bulk?created={created}&skipped={skipped}&invalid={invalid}", + status_code=303, + ) + @router.get("/clients/{client_id}",response_class=HTMLResponse) def client_register(request:Request,client_id:int): with CommonSessionLocal() as db: diff --git a/app/ui/templates/components/partner_navigation_v2.html b/app/ui/templates/components/partner_navigation_v2.html index 1111e27..9d09165 100644 --- a/app/ui/templates/components/partner_navigation_v2.html +++ b/app/ui/templates/components/partner_navigation_v2.html @@ -70,7 +70,12 @@ { 'label': 'Registrations & Renewals', 'url': '/registrations', - 'active': _partner_path == '/registrations' or (_partner_path.startswith('/registrations/') and not _partner_path.startswith('/registrations/masters')) + 'active': _partner_path == '/registrations' or (_partner_path.startswith('/registrations/') and not _partner_path.startswith('/registrations/masters') and not _partner_path.startswith('/registrations/bulk')) + }, + { + 'label': 'Bulk Registration Addition', + 'url': '/registrations/bulk', + 'active': _partner_path.startswith('/registrations/bulk') }, { 'label': 'Registration Masters',