Align client onboarding documents and engagement AQMM workflow

This commit is contained in:
A R R R Associates
2026-07-11 22:20:26 +05:30
parent a9dda197ad
commit c36f419923
4 changed files with 223 additions and 180 deletions
@@ -12,13 +12,59 @@
</div>
</div>
{% if not request.query_params.get('onboarding') %}
<div class="rounded-2xl border border-sky-200 bg-sky-50 p-4 text-sm text-sky-800">
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.
</div>
{% endif %}
{% if request.query_params.get('onboarding') %}
<section class="rounded-2xl border border-indigo-200 bg-indigo-50 p-5 shadow-soft">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div>
<div class="text-xs font-semibold uppercase tracking-[0.18em] text-indigo-700">Client Onboarding · Permanent Documents</div>
<h3 class="mt-1 text-lg font-semibold text-indigo-950">Complete the legal/KYC document checklist</h3>
<p class="mt-1 max-w-3xl text-sm text-indigo-800">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.</p>
</div>
<div class="shrink-0 rounded-xl bg-white px-4 py-3 text-center shadow-sm">
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Onboarding status</div>
<div class="mt-1 text-sm font-semibold {{ 'text-emerald-700' if onboarding_complete else 'text-amber-700' }}">{{ 'Document checklist complete' if onboarding_complete else 'Documents pending' }}</div>
</div>
</div>
<div class="mt-5 grid gap-3 md:grid-cols-2 xl:grid-cols-3">
{% for item in onboarding_checklist %}
<div class="rounded-xl border px-4 py-3 {{ 'border-emerald-200 bg-emerald-50' if item.complete else 'border-amber-200 bg-white' }}">
<div class="flex items-start gap-3">
<div class="mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-bold {{ 'bg-emerald-600 text-white' if item.complete else 'bg-amber-100 text-amber-800' }}">{{ '✓' if item.complete else '!' }}</div>
<div>
<div class="text-sm font-semibold text-slate-900">{{ item.label }}</div>
<div class="mt-1 text-xs {{ 'text-emerald-700' if item.complete else 'text-amber-700' }}">{{ 'Uploaded' if item.complete else 'Required to complete onboarding' }}</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="mt-5 flex flex-wrap gap-3">
{% if onboarding_complete %}
<a href="/clients/{{ client.id }}" class="rounded-xl bg-emerald-600 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-700">Complete Onboarding</a>
{% else %}
<a href="#permanent-upload-form" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Upload Required Documents</a>
<a href="/clients/{{ client.id }}" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Save and Continue Later</a>
{% endif %}
<a href="/services/engagements/new?client_id={{ client.id }}" class="rounded-xl border border-indigo-300 bg-white px-4 py-2 text-sm font-semibold text-indigo-700 hover:bg-indigo-100">Assign Service / Create Engagement</a>
</div>
</section>
{% endif %}
{% if request.query_params.get('uploaded') %}<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">Permanent document uploaded successfully.</div>{% endif %}
{% if request.query_params.get('deleted') %}<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">Permanent document archived successfully.</div>{% endif %}
{% if request.query_params.get('download_queued') %}<div class="rounded-2xl border border-sky-200 bg-sky-50 p-4 text-sm text-sky-800">The file is stored in branch local storage. Retrieval request #{{ request.query_params.get('download_queued') }} has been queued.</div>{% endif %}
{% if request.query_params.get('error') %}<div class="rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">Action failed. Please check file size, permission and storage path.</div>{% endif %}
{% if can_upload %}
<section class="rounded-2xl bg-white p-5 shadow-soft">
<section id="permanent-upload-form" class="rounded-2xl bg-white p-5 shadow-soft">
<h3 class="text-sm font-semibold text-slate-900">Upload Permanent Document / New Revision</h3>
<form method="post" action="/documents/permanent/clients/{{ client.id }}/upload" enctype="multipart/form-data" class="mt-4 grid gap-4 md:grid-cols-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
+151 -1
View File
@@ -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()