From 2a4fd2bfdd963aed5aa04f9a0c8a65581602aeb8 Mon Sep 17 00:00:00 2001 From: A R R R Associates Date: Wed, 2 Sep 2026 12:31:27 +0530 Subject: [PATCH] Improve employee and partner engagement workspace --- app/modules/employees/service.py | 4 + .../employees/work_engagement_board.html | 5 + .../templates/partners/engagement_detail.html | 4 + app/modules/partners/ui.py | 5 +- app/modules/services/engagement_resources.py | 85 ++++++++++ .../engagements/_engagement_resources.html | 157 ++++++++++++++++++ 6 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 app/modules/services/engagement_resources.py create mode 100644 app/modules/services/templates/services/engagements/_engagement_resources.html diff --git a/app/modules/employees/service.py b/app/modules/employees/service.py index b5fcd1a..0380cde 100644 --- a/app/modules/employees/service.py +++ b/app/modules/employees/service.py @@ -25,6 +25,7 @@ from app.modules.clients.models import Client, ClientBusinessUnit, ClientBranch from app.modules.documents.models import EngagementDocument from app.modules.services.models import ClientServiceTaskInstance, ClientServiceSubscription, ServiceCatalogue, ServiceTaskComment, FirmTaskDocumentRequirement from app.modules.registrations.models import ClientRegistration, RegistrationType +from app.modules.services.engagement_resources import build_engagement_resource_context from app.modules.services.execution import ( CLOSED_TASK_STATUSES, TASK_PRIORITIES, @@ -4164,6 +4165,8 @@ def get_employee_engagement_work_board( and int(task.firm_task_template_id) in mandatory_requirement_template_ids ) + resource_context = build_engagement_resource_context(db, subscription, tasks, engagement_documents) if subscription is not None else {} + scope_display = _employee_work_scope_display_map( db, tenant_id=scope.tenant_id, @@ -4191,6 +4194,7 @@ def get_employee_engagement_work_board( "active_category": selected_category, "next_task": next_task, "documents": engagement_documents, + "resources": resource_context, "scope_display": scope_display, "i1_enabled": True, "communication_types": TASK_COMMUNICATION_TYPES, diff --git a/app/modules/employees/templates/employees/work_engagement_board.html b/app/modules/employees/templates/employees/work_engagement_board.html index a07fade..b2196c4 100644 --- a/app/modules/employees/templates/employees/work_engagement_board.html +++ b/app/modules/employees/templates/employees/work_engagement_board.html @@ -66,6 +66,11 @@ + {% set resources = board.resources or {} %} + {% set engagement_id = board.engagement_id %} + {% set return_to = '/employee/work/engagements/' ~ board.engagement_id %} + {% include "modules/services/templates/services/engagements/_engagement_resources.html" %} +
Progress
diff --git a/app/modules/partners/templates/partners/engagement_detail.html b/app/modules/partners/templates/partners/engagement_detail.html index 9a928a2..143d0f6 100644 --- a/app/modules/partners/templates/partners/engagement_detail.html +++ b/app/modules/partners/templates/partners/engagement_detail.html @@ -14,6 +14,10 @@
+ {% set engagement_id = engagement.id %} + {% set return_to = '/partner/engagements/' ~ engagement.id %} + {% include "modules/services/templates/services/engagements/_engagement_resources.html" %} +
{% for task in tasks %} diff --git a/app/modules/partners/ui.py b/app/modules/partners/ui.py index 50e9242..78f5472 100644 --- a/app/modules/partners/ui.py +++ b/app/modules/partners/ui.py @@ -19,6 +19,7 @@ from app.modules.documents.models import EngagementDocument from app.modules.clients.models import Client, ClientBranch, ClientBusinessUnit from app.modules.registrations.models import ClientRegistration, RegistrationType from app.modules.services.execution import CLOSED_TASK_STATUSES, TASK_PRIORITIES, TASK_STATUSES +from app.modules.services.engagement_resources import build_engagement_resource_context from app.modules.services.models import ( ClientServiceSubscription, ClientServiceTaskInstance, @@ -709,11 +710,13 @@ def partner_engagement_detail(request: Request, engagement_id: int): _decorate_task(task, today) documents = db.execute( select(EngagementDocument) + .options(selectinload(EngagementDocument.versions)) .where(EngagementDocument.engagement_id == engagement.id, EngagementDocument.is_deleted.is_(False)) .order_by(EngagementDocument.updated_at_utc.desc(), EngagementDocument.id.desc()) ).scalars().all() engagement.display_label = _engagement_label(engagement) - return _render(request, "modules/partners/templates/partners/engagement_detail.html", db, current_user, title="Partner Engagement Review", engagement=engagement, tasks=tasks, documents=documents, errors=[], financial_year=financial_year) + resources = build_engagement_resource_context(db, engagement, tasks, documents) + return _render(request, "modules/partners/templates/partners/engagement_detail.html", db, current_user, title="Partner Engagement Review", engagement=engagement, tasks=tasks, documents=documents, resources=resources, errors=[], financial_year=financial_year) finally: db.close() diff --git a/app/modules/services/engagement_resources.py b/app/modules/services/engagement_resources.py new file mode 100644 index 0000000..da50a1e --- /dev/null +++ b/app/modules/services/engagement_resources.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +import re +from pathlib import Path +from typing import Any, Iterable + +from app.modules.documents.services import client_folder_parts, get_active_storage_node_for_branch, sanitize_segment + + +def _join_local_path(root: str, relative: str) -> str: + root = (root or "").strip().rstrip("\\/") + if not root: + return "" + if re.match(r"^[A-Za-z]:", root) or "\\" in root: + return root + "\\" + relative.replace("/", "\\") + return root + "/" + relative.lstrip("/") + + +def build_engagement_resource_context(db, engagement, tasks: Iterable[Any], documents: Iterable[Any]) -> dict[str, Any]: + """Presentation-only engagement resource data using existing storage/task models. + + No new persistence is introduced here. Accounting paths follow the same FY/client + folder convention already used by the ERP Local Agent. Task-category uploads map + to an existing task so evidence/AQMM/document controls remain authoritative. + """ + tasks = list(tasks or []) + documents = list(documents or []) + client = getattr(engagement, "client", None) + client_id = int(getattr(engagement, "client_id", 0) or 0) + fy = str(getattr(engagement, "financial_year", "") or "").strip() + + accounting_relative_dir = "" + accounting_relative_path = "" + accounting_local_path = "" + storage_root_path = "" + storage_node_name = "" + if client and client_id and fy: + fy_folder = sanitize_segment(f"FY{fy}", "FY") + letter, client_folder = client_folder_parts(client, client_id) + relative_dir = Path(fy_folder) / "Clients" / letter / client_folder / "Accounting" + accounting_relative_dir = relative_dir.as_posix() + accounting_relative_path = (relative_dir / f"client_{client_id:08d}.act").as_posix() + node = get_active_storage_node_for_branch( + db, + int(getattr(engagement, "tenant_id", 0) or 0), + getattr(engagement, "branch_id", None), + ) + if node: + storage_root_path = str(getattr(node, "storage_root_path", "") or "").strip() + storage_node_name = str(getattr(node, "node_name", "") or "").strip() + accounting_local_path = _join_local_path(storage_root_path, accounting_relative_path) + + category_map: dict[str, int] = {} + for task in tasks: + category = str(getattr(task, "task_category", "") or "").strip() or "General Workflow" + task_id = int(getattr(task, "id", 0) or 0) + if task_id and category not in category_map: + category_map[category] = task_id + categories = [{"name": name, "task_id": task_id} for name, task_id in category_map.items()] + + document_options = [] + for doc in documents: + versions = list(getattr(doc, "versions", None) or []) + latest = versions[0] if versions else None + document_options.append({ + "id": int(getattr(doc, "id", 0) or 0), + "title": str(getattr(doc, "title", "") or "Document"), + "document_type": str(getattr(doc, "document_type", "GENERAL") or "GENERAL"), + "version_no": int(getattr(doc, "current_version_no", 0) or 0), + "task_instance_id": getattr(doc, "task_instance_id", None), + "latest_filename": str(getattr(latest, "original_filename", "") or "") if latest else "", + "latest_relative_path": str(getattr(latest, "local_relative_path", "") or "") if latest else "", + "storage_status": str(getattr(latest, "storage_status", "") or "") if latest else "", + }) + + return { + "accounting_relative_dir": accounting_relative_dir, + "accounting_relative_path": accounting_relative_path, + "accounting_local_path": accounting_local_path, + "storage_root_path": storage_root_path, + "storage_node_name": storage_node_name, + "task_categories": categories, + "default_upload_task_id": categories[0]["task_id"] if categories else None, + "document_options": document_options, + } diff --git a/app/modules/services/templates/services/engagements/_engagement_resources.html b/app/modules/services/templates/services/engagements/_engagement_resources.html new file mode 100644 index 0000000..b6121ea --- /dev/null +++ b/app/modules/services/templates/services/engagements/_engagement_resources.html @@ -0,0 +1,157 @@ +{% set resources = resources or {} %} +
+
+
+
+

Engagement Data & Documents

+

Tally/accounting storage and engagement files are available here before the task checklist.

+
+ Open Full Document Register +
+
+ +
+
+
+
+
+
Tally / Accounting Data
+
Client accounting .act path
+
+ {% if resources.storage_node_name %}{{ resources.storage_node_name }}{% endif %} +
+ {% if resources.accounting_local_path %} +
+ + +
+

This follows the existing branch Storage Node → FY → Client → Accounting convention used by the Local Agent.

+ {% elif resources.accounting_relative_path %} +
{{ resources.accounting_relative_path }}
+

The relative accounting path is known, but this branch does not currently expose a configured local storage root.

+ {% else %} +

Accounting path is unavailable for this engagement.

+ {% endif %} +
+ +
+
+
Current Engagement Files
+ {{ resources.document_options|length }} +
+
+ {% for doc in resources.document_options %} +
+
+
+
{{ doc.title }}
+
{{ doc.document_type|replace('_',' ')|title }} · v{{ doc.version_no }}{% if doc.latest_filename %} · {{ doc.latest_filename }}{% endif %}
+
+ {% if doc.storage_status %}{{ doc.storage_status|replace('_',' ')|title }}{% endif %} +
+ {% if doc.latest_relative_path %}
{{ doc.latest_relative_path }}
{% endif %} +
+ {% else %} +
No engagement documents uploaded yet.
+ {% endfor %} +
+
+
+ +
+
Upload from Engagement Page
+

Choose the task category so the file remains linked to the existing task/evidence workflow.

+ {% if resources.default_upload_task_id %} +
+ + +
+ + +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+ +
+ {% else %} +
No task is available to receive an engagement upload.
+ {% endif %} +
+
+
+ +