Prepare ERP source for Gitea deployment

This commit is contained in:
A R R R Associates
2026-06-20 15:01:44 +05:30
commit 5c75eb6bd9
450 changed files with 67698 additions and 0 deletions
+408
View File
@@ -0,0 +1,408 @@
from __future__ import annotations
from collections import defaultdict
from datetime import date, datetime, timezone
from typing import Any
from fastapi import APIRouter, Form, Request
from fastapi.responses import RedirectResponse
from sqlalchemy import or_, select
from sqlalchemy.orm import Session, selectinload
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.core.rbac.deps import get_user_permissions, get_user_roles
from app.modules.documents.models import EngagementDocument
from app.modules.clients.models import Client
from app.modules.services.execution import CLOSED_TASK_STATUSES, TASK_PRIORITIES, TASK_STATUSES
from app.modules.services.models import (
ClientServiceSubscription,
ClientServiceTaskInstance,
ServiceCatalogue,
ServiceTaskComment,
)
router = APIRouter(prefix="/partner", tags=["partner-workspace-ui"])
REVIEW_ACTIONS = {
"approve": ("completed", "partner_review", "partner_review"),
"send_rework": ("pending", "partner_review_note", "partner_review"),
"clarification": ("blocked", "client_clarification", "internal"),
}
def _redirect_login():
return RedirectResponse(url="/login", status_code=303)
def _redirect_denied():
return RedirectResponse(url="/employee/dashboard", status_code=303)
def _is_partner_user(db: Session, current_user) -> bool:
roles = set(get_user_roles(db, current_user.id))
return bool(roles.intersection({"Partner", "Firm Admin", "System Admin"}))
def _base_ctx(request: Request, db: Session, current_user, **ctx):
base = {
"request": request,
"current_user": current_user,
"current_user_roles": get_user_roles(db, current_user.id),
"current_user_permissions": get_user_permissions(db, current_user.id),
"csrf_token": get_or_create_csrf_token(request),
"task_statuses": TASK_STATUSES,
"task_priorities": TASK_PRIORITIES,
}
base.update(ctx)
return base
def _render(request: Request, template_name: str, db: Session, current_user, **ctx):
return templates.TemplateResponse(template_name, _base_ctx(request, db, current_user, **ctx))
def _active_tenant_branch(request: Request, current_user, roles: set[str]) -> tuple[int, int | None]:
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id")
if "System Admin" not in roles:
tenant_id = current_user.tenant_id
if not roles.intersection({"System Admin", "Firm Admin"}):
branch_id = current_user.branch_id
if branch_id in (0, "0", "", None):
branch_id = None
return int(tenant_id), int(branch_id) if branch_id is not None else None
def _active_financial_year(request: Request) -> str | None:
value = request.session.get("active_financial_year") or getattr(request.state, "year_code", None)
value = (value or "").strip()
return value or None
def _subscription_scope_filter(stmt, tenant_id: int, branch_id: int | None, current_user, roles: set[str], financial_year: str | None = None):
stmt = stmt.where(ClientServiceSubscription.tenant_id == tenant_id, ClientServiceSubscription.is_active.is_(True))
if branch_id is not None:
stmt = stmt.where(ClientServiceSubscription.branch_id == branch_id)
if financial_year:
stmt = stmt.where(ClientServiceSubscription.financial_year == financial_year.strip())
if not roles.intersection({"System Admin", "Firm Admin"}):
stmt = stmt.where(
or_(
ClientServiceSubscription.assigned_partner_user_id == current_user.id,
ClientServiceSubscription.review_partner_user_id == current_user.id,
)
)
return stmt
def _task_scope_filter(stmt, tenant_id: int, branch_id: int | None, current_user, roles: set[str], financial_year: str | None = None):
stmt = stmt.where(ClientServiceTaskInstance.tenant_id == tenant_id, ClientServiceTaskInstance.is_active.is_(True))
if branch_id is not None:
stmt = stmt.where(ClientServiceTaskInstance.branch_id == branch_id)
if financial_year:
stmt = stmt.where(ClientServiceTaskInstance.financial_year == financial_year.strip())
if not roles.intersection({"System Admin", "Firm Admin"}):
stmt = stmt.where(
ClientServiceTaskInstance.subscription.has(
or_(
ClientServiceSubscription.assigned_partner_user_id == current_user.id,
ClientServiceSubscription.review_partner_user_id == current_user.id,
)
)
)
return stmt
def _task_status_label(task: ClientServiceTaskInstance) -> str:
return dict(TASK_STATUSES).get(getattr(task, "status", ""), (getattr(task, "status", "") or "-").replace("_", " ").title())
def _task_priority_label(task: ClientServiceTaskInstance) -> str:
return dict(TASK_PRIORITIES).get(getattr(task, "priority", ""), (getattr(task, "priority", "") or "normal").replace("_", " ").title())
def _engagement_label(subscription: ClientServiceSubscription | None, task: ClientServiceTaskInstance | None = None) -> str:
catalogue = getattr(subscription, "catalogue", None) if subscription else getattr(task, "catalogue", None)
service_name = getattr(catalogue, "service_name", None) or getattr(catalogue, "name", None) or "Engagement"
fy = getattr(subscription, "financial_year", None) or getattr(task, "financial_year", None)
return f"{service_name} · FY {fy}" if fy else str(service_name)
def _decorate_task(task: ClientServiceTaskInstance, today: date) -> ClientServiceTaskInstance:
client = getattr(task, "client", None)
assignee = getattr(task, "assigned_to", None)
subscription = getattr(task, "subscription", None)
target = getattr(task, "internal_target_date", None)
status = (task.status or "pending").strip().lower()
is_closed = status in CLOSED_TASK_STATUSES
task.status_label = _task_status_label(task)
task.priority_label = _task_priority_label(task)
task.client_display = getattr(client, "client_name", None) or "Unlinked Client"
task.client_code_display = getattr(client, "client_code", None) or ""
task.assignee_display = getattr(assignee, "full_name", None) or getattr(assignee, "email", None) or "Unassigned"
task.engagement_label = _engagement_label(subscription, task)
task.is_overdue = bool(target and target < today and not is_closed)
task.is_due_today = bool(target and target == today and not is_closed)
task.comment_count = len([c for c in getattr(task, "comments", []) if not getattr(c, "is_deleted", False)])
return task
def _task_bucket(task: ClientServiceTaskInstance) -> str:
status = (task.status or "pending").strip().lower()
if status == "blocked":
return "clarification_required"
if status == "completed":
return "pending_review"
if status in CLOSED_TASK_STATUSES:
return "completed"
if status in {"pending", "rework", "rework_required"}:
return "rework_sent"
return "approved"
def build_partner_payload(db: Session, request: Request, current_user, *, q: str = "") -> dict[str, Any]:
roles = set(get_user_roles(db, current_user.id))
tenant_id, branch_id = _active_tenant_branch(request, current_user, roles)
financial_year = _active_financial_year(request)
today = date.today()
task_stmt = (
select(ClientServiceTaskInstance)
.options(
selectinload(ClientServiceTaskInstance.client),
selectinload(ClientServiceTaskInstance.catalogue),
selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.catalogue),
selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.assigned_manager),
selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.assigned_staff),
selectinload(ClientServiceTaskInstance.assigned_to),
selectinload(ClientServiceTaskInstance.comments).selectinload(ServiceTaskComment.created_by),
)
)
task_stmt = _task_scope_filter(task_stmt, tenant_id, branch_id, current_user, roles, financial_year=financial_year)
if q.strip():
like = f"%{q.strip()}%"
task_stmt = task_stmt.where(
or_(
ClientServiceTaskInstance.task_name.ilike(like),
ClientServiceTaskInstance.description.ilike(like),
ClientServiceTaskInstance.client.has(or_(Client.client_name.ilike(like), Client.client_code.ilike(like))),
ClientServiceTaskInstance.catalogue.has(or_(ServiceCatalogue.service_name.ilike(like), ServiceCatalogue.service_code.ilike(like))),
)
)
tasks = db.execute(
task_stmt.order_by(
ClientServiceTaskInstance.internal_target_date.is_(None),
ClientServiceTaskInstance.internal_target_date.asc(),
ClientServiceTaskInstance.priority.desc(),
ClientServiceTaskInstance.id.desc(),
)
).scalars().all()
columns = [
{"code": "pending_review", "label": "Pending Review", "hint": "Completed tasks waiting for partner review", "tasks": []},
{"code": "clarification_required", "label": "Clarification Required", "hint": "Blocked tasks needing partner attention", "tasks": []},
{"code": "rework_sent", "label": "Rework Sent", "hint": "Pending/reopened after review notes", "tasks": []},
{"code": "approved", "label": "In Progress", "hint": "Work currently moving with the team", "tasks": []},
{"code": "completed", "label": "Completed", "hint": "Closed tasks", "tasks": []},
]
lookup = {c["code"]: c for c in columns}
summary = {"total": len(tasks), "pending_review": 0, "blocked": 0, "overdue": 0, "due_today": 0, "completed": 0, "clients": set(), "engagements": set()}
for task in tasks:
_decorate_task(task, today)
status = (task.status or "pending").lower()
if getattr(task, "client_id", None):
summary["clients"].add(task.client_id)
if getattr(task, "subscription_id", None):
summary["engagements"].add(task.subscription_id)
if status == "completed":
summary["pending_review"] += 1
if status == "blocked":
summary["blocked"] += 1
if getattr(task, "is_overdue", False):
summary["overdue"] += 1
if getattr(task, "is_due_today", False):
summary["due_today"] += 1
if status in CLOSED_TASK_STATUSES:
summary["completed"] += 1
lookup[_task_bucket(task)]["tasks"].append(task)
summary["clients"] = len(summary["clients"])
summary["engagements"] = len(summary["engagements"])
for col in columns:
col["count"] = len(col["tasks"])
engagement_stmt = (
select(ClientServiceSubscription)
.options(
selectinload(ClientServiceSubscription.client),
selectinload(ClientServiceSubscription.catalogue),
selectinload(ClientServiceSubscription.assigned_manager),
selectinload(ClientServiceSubscription.assigned_staff),
)
)
engagement_stmt = _subscription_scope_filter(engagement_stmt, tenant_id, branch_id, current_user, roles, financial_year=financial_year)
engagements = db.execute(
engagement_stmt.order_by(ClientServiceSubscription.current_due_date.is_(None), ClientServiceSubscription.current_due_date.asc(), ClientServiceSubscription.id.desc()).limit(25)
).scalars().all()
task_counts_by_subscription: dict[int, dict[str, int]] = defaultdict(lambda: {"total": 0, "completed": 0, "open": 0})
for task in tasks:
bucket = task_counts_by_subscription[int(task.subscription_id)]
bucket["total"] += 1
if (task.status or "").lower() in CLOSED_TASK_STATUSES:
bucket["completed"] += 1
else:
bucket["open"] += 1
for engagement in engagements:
engagement.display_label = _engagement_label(engagement)
engagement.client_display = getattr(getattr(engagement, "client", None), "client_name", None) or "Unlinked Client"
engagement.task_counts = task_counts_by_subscription.get(int(engagement.id), {"total": 0, "completed": 0, "open": 0})
due = getattr(engagement, "current_due_date", None)
engagement.is_overdue = bool(due and due < today and (engagement.status or "").lower() not in {"completed", "closed", "locked"})
return {"summary": summary, "columns": columns, "engagements": engagements, "q": q, "today": today, "financial_year": financial_year}
def _get_partner_task_or_redirect(db: Session, request: Request, current_user, task_id: int) -> ClientServiceTaskInstance | None:
roles = set(get_user_roles(db, current_user.id))
tenant_id, branch_id = _active_tenant_branch(request, current_user, roles)
financial_year = _active_financial_year(request)
stmt = select(ClientServiceTaskInstance).options(selectinload(ClientServiceTaskInstance.subscription)).where(ClientServiceTaskInstance.id == int(task_id))
stmt = _task_scope_filter(stmt, tenant_id, branch_id, current_user, roles, financial_year=financial_year)
return db.execute(stmt).scalar_one_or_none()
@router.get("/dashboard")
def partner_dashboard(request: Request, q: str = ""):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
if not _is_partner_user(db, current_user):
return _redirect_denied()
payload = build_partner_payload(db, request, current_user, q=q)
return _render(request, "modules/partners/templates/partners/dashboard.html", db, current_user, title="Partner Workspace", payload=payload, q=q, errors=[])
finally:
db.close()
@router.get("/reviews")
def partner_review_board(request: Request, q: str = ""):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
if not _is_partner_user(db, current_user):
return _redirect_denied()
payload = build_partner_payload(db, request, current_user, q=q)
return _render(request, "modules/partners/templates/partners/review_board.html", db, current_user, title="Partner Review Board", payload=payload, q=q, errors=[])
finally:
db.close()
@router.get("/clients")
def partner_clients(request: Request, q: str = ""):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
if not _is_partner_user(db, current_user):
return _redirect_denied()
payload = build_partner_payload(db, request, current_user, q=q)
return _render(request, "modules/partners/templates/partners/clients.html", db, current_user, title="My Client Portfolio", payload=payload, q=q, errors=[])
finally:
db.close()
@router.get("/engagements/{engagement_id}")
def partner_engagement_detail(request: Request, engagement_id: int):
db = CommonSessionLocal()
try:
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
if not _is_partner_user(db, current_user):
return _redirect_denied()
roles = set(get_user_roles(db, current_user.id))
tenant_id, branch_id = _active_tenant_branch(request, current_user, roles)
financial_year = _active_financial_year(request)
engagement_stmt = select(ClientServiceSubscription).options(
selectinload(ClientServiceSubscription.client),
selectinload(ClientServiceSubscription.catalogue),
selectinload(ClientServiceSubscription.assigned_manager),
selectinload(ClientServiceSubscription.assigned_staff),
).where(ClientServiceSubscription.id == int(engagement_id))
engagement_stmt = _subscription_scope_filter(engagement_stmt, tenant_id, branch_id, current_user, roles, financial_year=financial_year)
engagement = db.execute(engagement_stmt).scalar_one_or_none()
if not engagement:
return _redirect_denied()
task_stmt = select(ClientServiceTaskInstance).options(
selectinload(ClientServiceTaskInstance.client),
selectinload(ClientServiceTaskInstance.catalogue),
selectinload(ClientServiceTaskInstance.subscription).selectinload(ClientServiceSubscription.catalogue),
selectinload(ClientServiceTaskInstance.assigned_to),
selectinload(ClientServiceTaskInstance.comments).selectinload(ServiceTaskComment.created_by),
).where(ClientServiceTaskInstance.subscription_id == engagement.id, ClientServiceTaskInstance.is_active.is_(True))
tasks = db.execute(task_stmt.order_by(ClientServiceTaskInstance.sequence_no.asc(), ClientServiceTaskInstance.id.asc())).scalars().all()
today = date.today()
for task in tasks:
_decorate_task(task, today)
documents = db.execute(
select(EngagementDocument)
.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)
finally:
db.close()
@router.post("/tasks/{task_id}/review")
def partner_review_task(request: Request, task_id: int, action: str = Form(...), message: str = Form(""), csrf_token: str = Form(...)):
db = CommonSessionLocal()
try:
validate_csrf(request, csrf_token)
current_user = get_current_user(request, db=db)
if not current_user:
return _redirect_login()
if not _is_partner_user(db, current_user):
return _redirect_denied()
task = _get_partner_task_or_redirect(db, request, current_user, task_id)
if not task:
return _redirect_denied()
new_status, comment_type, visibility = REVIEW_ACTIONS.get(action, REVIEW_ACTIONS["approve"])
task.status = new_status
task.updated_by_user_id = current_user.id
task.updated_at_utc = datetime.now(timezone.utc)
note = (message or "").strip()
if not note:
note = {
"approve": "Approved by partner.",
"send_rework": "Sent back for rework by partner.",
"clarification": "Clarification requested by partner.",
}.get(action, "Partner review updated.")
db.add(ServiceTaskComment(
tenant_id=task.tenant_id,
branch_id=task.branch_id,
subscription_id=task.subscription_id,
task_instance_id=task.id,
comment_type=comment_type,
visibility=visibility,
message=note,
created_by_user_id=current_user.id,
))
db.commit()
return RedirectResponse(url=f"/partner/engagements/{task.subscription_id}", status_code=303)
finally:
db.close()