From c36f419923f42fca326d21364e0ac05ea7f93b21 Mon Sep 17 00:00:00 2001 From: A R R R Associates Date: Sat, 11 Jul 2026 22:20:26 +0530 Subject: [PATCH] Align client onboarding documents and engagement AQMM workflow --- .../clients/templates/clients/detail.html | 185 ++---------------- app/modules/clients/ui.py | 18 +- .../documents/permanent_client_documents.html | 48 ++++- app/modules/documents/ui.py | 152 +++++++++++++- 4 files changed, 223 insertions(+), 180 deletions(-) diff --git a/app/modules/clients/templates/clients/detail.html b/app/modules/clients/templates/clients/detail.html index 07a187b..d97341d 100644 --- a/app/modules/clients/templates/clients/detail.html +++ b/app/modules/clients/templates/clients/detail.html @@ -82,184 +82,29 @@
- {% set acc_status = row.acceptance_status or 'pending_review' %} -
+
-

Client Acceptance Controls

-

Embedded AQMM / peer-review evidence for acceptance, independence, conflict, KYC and engagement letter controls.

+

Client Onboarding & Permanent Documents

+

Client-master onboarding is limited to legal/KYC records and permanent documents. Engagement-specific AQMM formalities are managed from each engagement according to its existing Assurance / Non-Assurance classification.

- {{ acc_status.replace('_',' ').title() }} + + {{ permanent_document_count }} permanent document{{ '' if permanent_document_count == 1 else 's' }} +
-
- {% for label, ok in [ - ('Acceptance Required', row.acceptance_required), - ('Independence Check', row.independence_check_completed), - ('Conflict Check', row.conflict_check_completed), - ('KYC Completed', row.kyc_completed), - ('Engagement Letter Required', row.engagement_letter_required), - ('Engagement Letter Received', row.engagement_letter_received) - ] %} -
-
{{ label }}
-
{{ 'Yes' if ok else 'No' }}
-
- {% endfor %} -
-
Approved By
-
{{ row.acceptance_approved_by_user_id or '-' }}
-
-
-
Approved At
-
{{ row.acceptance_approved_at_utc or '-' }}
-
-
- - {% if row.acceptance_review_notes %} -
-
Review Notes
-
{{ row.acceptance_review_notes }}
-
- {% endif %} - {% if row.acceptance_rejection_reason %} -
-
Rejection / Remediation Notes
-
{{ row.acceptance_rejection_reason }}
-
- {% endif %} - - {% if can_approve_acceptance or can_manage_acceptance %} -
- {% if can_approve_acceptance %} -
- - - -
-
- - - -
- {% endif %} - {% if can_manage_acceptance %} -
- - - -
- {% endif %} -
- {% endif %} -
-
-
-

Digital Acceptance Workflow

-

Declarations, KYC from permanent documents and engagement letter acceptance are captured as audit evidence.

-
- {{ 'Ready for Approval' if workflow_ready else 'Pending Evidence' }} +
+
+

Permanent KYC Documents

+

Upload PAN, registration/constitution records, authorised-person KYC, bank proof and other standing documents. The checklist adapts to the client’s legal constitution.

+ Open Onboarding Documents
-
-
-
-
Independence / Conflict Declarations
- {% if can_manage_acceptance %} -
- - -
- {% endif %} -
-
- {% if acceptance_declarations %} - {% for d in acceptance_declarations %} -
-
{{ d.declaration_type.replace('_',' ').title() }} · User #{{ d.assigned_user_id }}
-
{{ d.status.replace('_',' ').title() }}
- {% if d.assigned_user_id == current_user.id and d.status == 'pending' %} -
- - - - - -
- {% endif %} -
- {% endfor %} - {% else %} -
No declaration requests have been created yet.
- {% endif %} -
-
- -
-
-
KYC from Permanent Documents
- {% if can_manage_acceptance %} -
- - -
- {% endif %} -
-
Permanent documents found: {{ permanent_document_count }}
-
{{ permanent_document_summary or 'No permanent document summary available. Click Sync after client uploads permanent documents.' }}
- {% if kyc_verification %} -
KYC Status: {{ kyc_verification.status.replace('_',' ').title() }}
- {% endif %} - {% if can_manage_acceptance or can_approve_acceptance %} -
-
- - - -
-
- - - -
-
- {% endif %} -
- -
-
Engagement Letter
- {% if engagement_letter %} -
Version {{ engagement_letter.version_no }} · {{ engagement_letter.status.replace('_',' ').title() }}
- View / Print PDF - {% if can_approve_acceptance and engagement_letter.status == 'draft' %} -
- - -
- {% endif %} - {% if (can_manage_acceptance or can_approve_acceptance) and engagement_letter.status == 'manual_uploaded' %} -
- - -
- {% endif %} - {% else %} -
No engagement letter drafted yet.
- {% endif %} - {% if can_manage_acceptance %} -
- - - - -
- {% endif %} -
+
+

AQMM at Engagement Level

+

Create or open an engagement to complete the applicable acceptance, independence, conflict, engagement-letter, evidence, review and closure requirements. The existing engagement classification remains the single source of truth.

+ Assign Service / Create Engagement
-
diff --git a/app/modules/clients/ui.py b/app/modules/clients/ui.py index 7d90d74..1804801 100644 --- a/app/modules/clients/ui.py +++ b/app/modules/clients/ui.py @@ -3,6 +3,7 @@ from __future__ import annotations from fastapi import APIRouter, File, Form, Request, UploadFile from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, RedirectResponse, Response from pydantic import ValidationError +from sqlalchemy import func, select from app.core.db.common import CommonSessionLocal from app.core.security.csrf import get_or_create_csrf_token, validate_csrf @@ -74,6 +75,7 @@ from app.modules.billing.client_portal_service import ( ) from app.modules.billing.services import build_invoice_print_context, create_cashfree_transaction, create_payumoney_transaction, process_cashfree_return, process_cashfree_webhook, process_payumoney_response from app.modules.email_integration.services import send_auth_otp_email +from app.modules.documents.models import PermanentClientDocument from app.modules.documents.services import ( get_permanent_version, get_version, @@ -623,7 +625,7 @@ async def client_create(request: Request): portal_password=(form.get("portal_password") or "").strip() or None, portal_password_confirm=(form.get("portal_password_confirm") or "").strip() or None, ) - return RedirectResponse(url=f"/clients/{row.id}", status_code=303) + return RedirectResponse(url=f"/documents/permanent/clients/{row.id}?onboarding=1", status_code=303) except Exception as exc: return _render( request, @@ -662,7 +664,12 @@ def client_detail(request: Request, client_id: int): return _redirect_denied() audit_logs = list_client_audit_logs(db, row=type("Tmp", (), {"id": row["id"]})(), limit=10) if has("clients.audit_log.view") else [] - workflow = build_client_acceptance_workflow_payload(db, client_id=client_id) + permanent_document_count = db.execute( + select(func.count(PermanentClientDocument.id)).where( + PermanentClientDocument.client_id == client_id, + PermanentClientDocument.is_deleted.is_(False), + ) + ).scalar_one() return _render( request, "modules/clients/templates/clients/detail.html", @@ -671,12 +678,7 @@ def client_detail(request: Request, client_id: int): title=f"Client • {row['client_name']}", row=row, audit_logs=audit_logs, - acceptance_declarations=workflow.get("acceptance_declarations", []), - kyc_verification=workflow.get("kyc_verification"), - engagement_letter=workflow.get("engagement_letter"), - permanent_document_summary=workflow.get("permanent_document_summary"), - permanent_document_count=workflow.get("permanent_document_count", 0), - workflow_ready=workflow.get("workflow_ready", False), + permanent_document_count=permanent_document_count, scope=scope, can_edit=has("clients.edit"), can_deactivate=has("clients.deactivate"), diff --git a/app/modules/documents/templates/documents/permanent_client_documents.html b/app/modules/documents/templates/documents/permanent_client_documents.html index 1a035c2..64faf42 100644 --- a/app/modules/documents/templates/documents/permanent_client_documents.html +++ b/app/modules/documents/templates/documents/permanent_client_documents.html @@ -12,13 +12,59 @@ + {% if not request.query_params.get('onboarding') %} +
+ Permanent documents belong to the client master. Acceptance, independence, conflict, engagement-letter and other AQMM requirements are completed from the applicable engagement according to its Assurance / Non-Assurance classification. +
+ {% endif %} + + {% if request.query_params.get('onboarding') %} +
+
+
+
Client Onboarding · Permanent Documents
+

Complete the legal/KYC document checklist

+

The client master has been created. Upload the standing documents below using the existing permanent-document, versioning and branch-local-storage workflow. Engagement-specific AQMM formalities are intentionally completed only inside the relevant Assurance / Non-Assurance engagement.

+
+
+
Onboarding status
+
{{ 'Document checklist complete' if onboarding_complete else 'Documents pending' }}
+
+
+ +
+ {% for item in onboarding_checklist %} +
+
+
{{ '✓' if item.complete else '!' }}
+
+
{{ item.label }}
+
{{ 'Uploaded' if item.complete else 'Required to complete onboarding' }}
+
+
+
+ {% endfor %} +
+ +
+ {% if onboarding_complete %} + Complete Onboarding + {% else %} + Upload Required Documents + Save and Continue Later + {% endif %} + Assign Service / Create Engagement +
+
+ {% endif %} + {% if request.query_params.get('uploaded') %}
Permanent document uploaded successfully.
{% endif %} {% if request.query_params.get('deleted') %}
Permanent document archived successfully.
{% endif %} {% if request.query_params.get('download_queued') %}
The file is stored in branch local storage. Retrieval request #{{ request.query_params.get('download_queued') }} has been queued.
{% endif %} {% if request.query_params.get('error') %}
Action failed. Please check file size, permission and storage path.
{% endif %} {% if can_upload %} -
+

Upload Permanent Document / New Revision

diff --git a/app/modules/documents/ui.py b/app/modules/documents/ui.py index c220287..da47f06 100644 --- a/app/modules/documents/ui.py +++ b/app/modules/documents/ui.py @@ -95,6 +95,144 @@ router = APIRouter(prefix="/documents", tags=["documents-ui"]) logger = logging.getLogger("audit_storage_agent.documents_ui") +def _normalise_document_text(value) -> str: + return re.sub(r"[^a-z0-9]+", " ", str(value or "").lower()).strip() + + +def _permanent_onboarding_requirements(client) -> list[dict[str, object]]: + """Return the client-master KYC checklist without creating a second document system.""" + constitution = _normalise_document_text(getattr(client, "client_type", "")) + requirements: list[dict[str, object]] = [ + { + "code": "pan", + "label": "PAN / Income-tax identity proof", + "categories": {"income tax", "kyc"}, + "keywords": {"pan", "income tax"}, + }, + { + "code": "kyc", + "label": "Authorised person / proprietor KYC", + "categories": {"kyc"}, + "keywords": {"kyc", "aadhaar", "aadhar", "identity", "authorised signatory", "authorized signatory"}, + }, + { + "code": "bank", + "label": "Bank account proof", + "categories": {"bank"}, + "keywords": {"bank", "cancelled cheque", "canceled cheque"}, + }, + ] + + if getattr(client, "gstin", None): + requirements.append({ + "code": "gst", + "label": "GST registration certificate", + "categories": {"gst registration"}, + "keywords": {"gst", "gstin"}, + }) + + if "partnership" in constitution: + requirements.append({ + "code": "constitution", + "label": "Partnership deed", + "categories": {"agreements", "company registration"}, + "keywords": {"partnership deed", "deed"}, + }) + elif constitution == "llp" or "limited liability partnership" in constitution: + requirements.extend([ + { + "code": "registration", + "label": "LLP incorporation / registration certificate", + "categories": {"company registration", "roc master data"}, + "keywords": {"llp incorporation", "certificate of incorporation", "incorporation"}, + }, + { + "code": "constitution", + "label": "LLP agreement", + "categories": {"agreements"}, + "keywords": {"llp agreement", "agreement"}, + }, + ]) + elif "limited company" in constitution or constitution in {"private limited company", "public limited company"}: + requirements.extend([ + { + "code": "registration", + "label": "Certificate of Incorporation", + "categories": {"company registration", "roc master data"}, + "keywords": {"certificate of incorporation", "incorporation"}, + }, + { + "code": "constitution", + "label": "MOA / AOA", + "categories": {"company registration", "agreements", "roc master data"}, + "keywords": {"moa", "aoa", "memorandum", "articles of association"}, + }, + ]) + elif "trust" in constitution: + requirements.append({ + "code": "constitution", + "label": "Trust deed / registration document", + "categories": {"agreements", "company registration", "licenses"}, + "keywords": {"trust deed", "trust registration"}, + }) + elif "society" in constitution: + requirements.extend([ + { + "code": "registration", + "label": "Society registration certificate", + "categories": {"company registration", "licenses"}, + "keywords": {"society registration", "registration certificate"}, + }, + { + "code": "constitution", + "label": "Bye-laws / governing document", + "categories": {"agreements"}, + "keywords": {"bye laws", "byelaws", "governing document"}, + }, + ]) + elif "proprietorship" in constitution: + requirements.append({ + "code": "constitution", + "label": "Business constitution / registration proof", + "categories": {"licenses", "company registration", "gst registration"}, + "keywords": {"proprietorship", "shop act", "udyam", "business registration"}, + }) + elif constitution in {"aop", "huf"}: + requirements.append({ + "code": "constitution", + "label": "Constitution / formation document", + "categories": {"agreements", "kyc", "company registration"}, + "keywords": {"formation", "constitution", "deed", "huf"}, + }) + + return requirements + + +def _build_permanent_onboarding_checklist(client, documents) -> tuple[list[dict[str, object]], bool]: + haystacks = [] + for document in documents or []: + haystacks.append({ + "category": _normalise_document_text(getattr(document, "category", "")), + "text": _normalise_document_text(" ".join([ + str(getattr(document, "title", "") or ""), + str(getattr(document, "description", "") or ""), + str(getattr(document, "document_code", "") or ""), + ])), + }) + + checklist = [] + for requirement in _permanent_onboarding_requirements(client): + category_tokens = {_normalise_document_text(v) for v in requirement["categories"]} + keyword_tokens = {_normalise_document_text(v) for v in requirement["keywords"]} + complete = any( + item["category"] in category_tokens + or any(keyword and keyword in item["text"] for keyword in keyword_tokens) + for item in haystacks + ) + checklist.append({**requirement, "complete": complete}) + return checklist, all(item["complete"] for item in checklist) + + def _base_ctx(request: Request, user, db, **ctx): base = { "request": request, @@ -723,7 +861,19 @@ def permanent_client_documents(request: Request, client_id: int): if not client or not user_can_view_client_documents(db, user, client, scope): return _redirect_denied() documents = list_permanent_documents_for_client(db, client_id) - return _render(request, "modules/documents/templates/documents/permanent_client_documents.html", db, user, title="Permanent Client Documents", client=client, documents=documents, can_upload=user_can_upload_client_documents(db, user, client, scope)) + onboarding_checklist, onboarding_complete = _build_permanent_onboarding_checklist(client, documents) + return _render( + request, + "modules/documents/templates/documents/permanent_client_documents.html", + db, + user, + title="Permanent Client Documents", + client=client, + documents=documents, + can_upload=user_can_upload_client_documents(db, user, client, scope), + onboarding_checklist=onboarding_checklist, + onboarding_complete=onboarding_complete, + ) finally: db.close()