Prepare ERP source for Gitea deployment
This commit is contained in:
@@ -0,0 +1,532 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Form, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sqlalchemy import select
|
||||
|
||||
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.services.models import ClientServiceSubscription, ClientServiceTaskInstance
|
||||
from app.modules.services.due_dates import apply_due_date_rule_to_subscription
|
||||
from app.modules.clients.models import Client
|
||||
from app.modules.services.client_services import (
|
||||
SUBSCRIPTION_STATUSES,
|
||||
assessment_year_from_financial_year,
|
||||
current_financial_year,
|
||||
get_enabled_firm_service,
|
||||
get_existing_subscription,
|
||||
get_subscription,
|
||||
normalize_financial_year,
|
||||
list_assignable_users,
|
||||
list_clients_for_assignment,
|
||||
list_enabled_services_for_assignment,
|
||||
list_subscription_payload,
|
||||
list_review_partners,
|
||||
parse_date,
|
||||
review_partner_required_for_engagement,
|
||||
)
|
||||
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
|
||||
from app.modules.core.rbac.permission_guard import require_permission
|
||||
from app.modules.core.tenancy.year_control import redirect_if_financial_year_locked, is_row_financial_year_locked
|
||||
|
||||
router = APIRouter(prefix="/services/engagements", tags=["services-engagements-ui"])
|
||||
|
||||
|
||||
def _base_ctx(request: Request, user, db, **ctx):
|
||||
base = {
|
||||
"request": request,
|
||||
"current_user": user,
|
||||
"current_user_roles": get_user_roles(db, user.id),
|
||||
"current_user_permissions": get_user_permissions(db, user.id),
|
||||
"csrf_token": get_or_create_csrf_token(request),
|
||||
"subscription_statuses": SUBSCRIPTION_STATUSES,
|
||||
}
|
||||
base.update(ctx)
|
||||
return base
|
||||
|
||||
|
||||
def _render(request: Request, template: str, db, user, **ctx):
|
||||
return templates.TemplateResponse(template, _base_ctx(request, user, db, **ctx))
|
||||
|
||||
|
||||
def _redirect_denied():
|
||||
return RedirectResponse(url="/system-settings", status_code=303)
|
||||
|
||||
|
||||
def _has_perm(db, user, code: str) -> bool:
|
||||
try:
|
||||
require_permission(db, user, code)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _active_tenant_id(request: Request, user) -> int:
|
||||
return int(
|
||||
request.session.get("active_tenant_id")
|
||||
or request.session.get("selected_tenant_id")
|
||||
or request.session.get("tenant_id")
|
||||
or user.tenant_id
|
||||
)
|
||||
|
||||
|
||||
def _active_branch_id(request: Request, user, db) -> int | None:
|
||||
value = request.session.get("active_branch_id")
|
||||
if value in (None, "", 0, "0"):
|
||||
if _has_perm(db, user, "clients.cross_branch"):
|
||||
return None
|
||||
return int(getattr(user, "branch_id", 0) or 0) or None
|
||||
return int(value)
|
||||
|
||||
|
||||
def _active_financial_year(request: Request) -> str:
|
||||
return normalize_financial_year(
|
||||
request.session.get("active_financial_year")
|
||||
or getattr(request.state, "year_code", None)
|
||||
)
|
||||
|
||||
|
||||
def _locked_partner_id(db, user) -> int | None:
|
||||
return int(user.id) if _has_perm(db, user, "clients.view.own_only") else None
|
||||
|
||||
|
||||
def _can_manage_client_services(db, user) -> bool:
|
||||
return _has_perm(db, user, "clients.edit")
|
||||
|
||||
|
||||
def _can_lock_engagements(db, user) -> bool:
|
||||
roles = set(get_user_roles(db, user.id))
|
||||
return bool(roles.intersection({"Firm Admin", "Partner"}))
|
||||
|
||||
|
||||
def _user_can_lock_subscription(db, user, row: ClientServiceSubscription) -> bool:
|
||||
roles = set(get_user_roles(db, user.id))
|
||||
if "Firm Admin" in roles:
|
||||
return True
|
||||
if "Partner" in roles:
|
||||
client = getattr(row, "client", None)
|
||||
return (
|
||||
getattr(row, "assigned_partner_user_id", None) == user.id
|
||||
or getattr(client, "partner_id", None) == user.id
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def _lock_subscription_row(row: ClientServiceSubscription, user) -> bool:
|
||||
if getattr(row, "is_locked", False):
|
||||
return False
|
||||
from datetime import datetime, timezone
|
||||
|
||||
row.is_locked = True
|
||||
row.status = "completed" if row.status == "active" else row.status
|
||||
row.locked_at_utc = datetime.now(timezone.utc)
|
||||
row.locked_by_user_id = user.id
|
||||
row.updated_by_user_id = user.id
|
||||
return True
|
||||
|
||||
|
||||
@router.get("")
|
||||
def subscription_list(request: Request, q: str = "", financial_year: str = "", include_inactive: bool = True, locked: int = 0, skipped: int = 0):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
if not user:
|
||||
return RedirectResponse(url="/login", status_code=303)
|
||||
try:
|
||||
require_permission(db, user, "clients.view")
|
||||
except Exception:
|
||||
return _redirect_denied()
|
||||
|
||||
tenant_id = _active_tenant_id(request, user)
|
||||
branch_id = _active_branch_id(request, user, db)
|
||||
selected_financial_year = normalize_financial_year(financial_year or _active_financial_year(request))
|
||||
rows = list_subscription_payload(
|
||||
db,
|
||||
tenant_id=tenant_id,
|
||||
branch_id=branch_id,
|
||||
financial_year=selected_financial_year,
|
||||
q=q,
|
||||
include_inactive=include_inactive,
|
||||
)
|
||||
return _render(
|
||||
request,
|
||||
"modules/services/templates/services/engagements/list.html",
|
||||
db,
|
||||
user,
|
||||
title="Engagement Subscriptions",
|
||||
rows=rows,
|
||||
q=q,
|
||||
financial_year=selected_financial_year,
|
||||
include_inactive=include_inactive,
|
||||
locked_count=locked,
|
||||
skipped_count=skipped,
|
||||
can_manage=_can_manage_client_services(db, user),
|
||||
can_lock_engagements=_can_lock_engagements(db, user),
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/new")
|
||||
def subscription_create_page(request: Request, client_id: int | None = None):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
if not user:
|
||||
return RedirectResponse(url="/login", status_code=303)
|
||||
try:
|
||||
require_permission(db, user, "clients.edit")
|
||||
except Exception:
|
||||
return _redirect_denied()
|
||||
|
||||
tenant_id = _active_tenant_id(request, user)
|
||||
branch_id = _active_branch_id(request, user, db)
|
||||
clients = list_clients_for_assignment(
|
||||
db,
|
||||
tenant_id=tenant_id,
|
||||
branch_id=branch_id,
|
||||
partner_id=_locked_partner_id(db, user),
|
||||
)
|
||||
enabled_services = list_enabled_services_for_assignment(db, tenant_id=tenant_id)
|
||||
assignable_users = list_assignable_users(db, tenant_id=tenant_id, branch_id=branch_id)
|
||||
review_partners = list_review_partners(db, tenant_id=tenant_id, branch_id=branch_id)
|
||||
|
||||
return _render(
|
||||
request,
|
||||
"modules/services/templates/services/engagements/form.html",
|
||||
db,
|
||||
user,
|
||||
title="Assign Service to Client",
|
||||
mode="create",
|
||||
subscription=None,
|
||||
clients=clients,
|
||||
enabled_services=enabled_services,
|
||||
assignable_users=assignable_users,
|
||||
review_partners=review_partners,
|
||||
selected_client_id=client_id,
|
||||
financial_year=_active_financial_year(request),
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/new")
|
||||
def subscription_create_submit(
|
||||
request: Request,
|
||||
client_id: int = Form(...),
|
||||
service_catalogue_id: int = Form(...),
|
||||
assigned_partner_user_id: str = Form(""),
|
||||
assigned_manager_user_id: str = Form(""),
|
||||
assigned_staff_user_id: str = Form(""),
|
||||
review_partner_user_id: str = Form(""),
|
||||
financial_year: str = Form(""),
|
||||
start_date: str = Form(""),
|
||||
end_date: str = Form(""),
|
||||
expiry_date: str = Form(""),
|
||||
status: str = Form("active"),
|
||||
remarks: str = Form(""),
|
||||
is_active: str | None = Form(None),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
validate_csrf(request, csrf_token)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
if not user:
|
||||
return RedirectResponse(url="/login", status_code=303)
|
||||
try:
|
||||
require_permission(db, user, "clients.edit")
|
||||
except Exception:
|
||||
return _redirect_denied()
|
||||
|
||||
tenant_id = _active_tenant_id(request, user)
|
||||
selected_financial_year = normalize_financial_year(financial_year or _active_financial_year(request))
|
||||
locked_response = redirect_if_financial_year_locked(db, tenant_id=tenant_id, year_code=selected_financial_year, redirect_url=f"/services/engagements?financial_year={selected_financial_year}")
|
||||
if locked_response:
|
||||
return locked_response
|
||||
firm_selection = get_enabled_firm_service(db, tenant_id=tenant_id, service_catalogue_id=service_catalogue_id)
|
||||
if not firm_selection:
|
||||
return RedirectResponse(url="/services/engagements/new", status_code=303)
|
||||
|
||||
existing = get_existing_subscription(
|
||||
db,
|
||||
tenant_id=tenant_id,
|
||||
client_id=client_id,
|
||||
service_catalogue_id=service_catalogue_id,
|
||||
financial_year=selected_financial_year,
|
||||
)
|
||||
client = db.get(Client, client_id)
|
||||
if not client or client.tenant_id != tenant_id:
|
||||
return RedirectResponse(url="/services/engagements/new", status_code=303)
|
||||
|
||||
if existing:
|
||||
row = existing
|
||||
else:
|
||||
row = ClientServiceSubscription(
|
||||
tenant_id=tenant_id,
|
||||
client_id=client_id,
|
||||
service_catalogue_id=service_catalogue_id,
|
||||
financial_year=selected_financial_year,
|
||||
assessment_year=assessment_year_from_financial_year(selected_financial_year),
|
||||
engagement_type=getattr(firm_selection.catalogue, "engagement_type", "non_assurance") or "non_assurance",
|
||||
created_by_user_id=user.id,
|
||||
)
|
||||
db.add(row)
|
||||
|
||||
if getattr(row, "is_locked", False) or is_row_financial_year_locked(db, row):
|
||||
return RedirectResponse(url=f"/services/engagements/{row.id}?year_locked=1", status_code=303)
|
||||
|
||||
row.financial_year = selected_financial_year
|
||||
row.assessment_year = assessment_year_from_financial_year(selected_financial_year)
|
||||
if not getattr(row, "engagement_type", None):
|
||||
row.engagement_type = getattr(firm_selection.catalogue, "engagement_type", "non_assurance") or "non_assurance"
|
||||
row.branch_id = firm_selection.default_branch_id or getattr(user, "branch_id", None)
|
||||
row.firm_service_selection_id = firm_selection.id
|
||||
row.assigned_partner_user_id = int(assigned_partner_user_id) if assigned_partner_user_id.strip() else None
|
||||
row.assigned_manager_user_id = int(assigned_manager_user_id) if assigned_manager_user_id.strip() else None
|
||||
row.assigned_staff_user_id = int(assigned_staff_user_id) if assigned_staff_user_id.strip() else None
|
||||
if review_partner_required_for_engagement(db, tenant_id=tenant_id, engagement_type=row.engagement_type):
|
||||
row.review_partner_user_id = int(review_partner_user_id) if review_partner_user_id.strip() else getattr(client, "default_review_partner_user_id", None)
|
||||
else:
|
||||
row.review_partner_user_id = None
|
||||
row.start_date = parse_date(start_date)
|
||||
row.end_date = parse_date(end_date)
|
||||
row.expiry_date = parse_date(expiry_date)
|
||||
row.status = status or "active"
|
||||
row.remarks = remarks.strip() or None
|
||||
row.is_active = is_active is not None
|
||||
row.updated_by_user_id = user.id
|
||||
apply_due_date_rule_to_subscription(db, row)
|
||||
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
return RedirectResponse(url=f"/services/engagements/{row.id}", status_code=303)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/bulk-lock")
|
||||
def subscription_bulk_lock(
|
||||
request: Request,
|
||||
subscription_ids: list[int] = Form([]),
|
||||
financial_year: str = Form(""),
|
||||
q: str = Form(""),
|
||||
include_inactive: str | None = Form(None),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
validate_csrf(request, csrf_token)
|
||||
db = CommonSessionLocal()
|
||||
locked_count = 0
|
||||
skipped_count = 0
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
if not user:
|
||||
return RedirectResponse(url="/login", status_code=303)
|
||||
if not _can_lock_engagements(db, user):
|
||||
return _redirect_denied()
|
||||
|
||||
tenant_id = _active_tenant_id(request, user)
|
||||
selected_ids = [int(value) for value in subscription_ids if value]
|
||||
for subscription_id in selected_ids:
|
||||
row = get_subscription(db, subscription_id=subscription_id, tenant_id=tenant_id)
|
||||
if not row or not _user_can_lock_subscription(db, user, row):
|
||||
skipped_count += 1
|
||||
continue
|
||||
if _lock_subscription_row(row, user):
|
||||
locked_count += 1
|
||||
else:
|
||||
skipped_count += 1
|
||||
|
||||
if locked_count:
|
||||
db.commit()
|
||||
|
||||
params = []
|
||||
fy = normalize_financial_year(financial_year or _active_financial_year(request))
|
||||
if fy:
|
||||
params.append(f"financial_year={fy}")
|
||||
if q.strip():
|
||||
from urllib.parse import quote_plus
|
||||
params.append(f"q={quote_plus(q.strip())}")
|
||||
if include_inactive:
|
||||
params.append("include_inactive=true")
|
||||
params.append(f"locked={locked_count}")
|
||||
params.append(f"skipped={skipped_count}")
|
||||
suffix = "?" + "&".join(params) if params else ""
|
||||
return RedirectResponse(url=f"/services/engagements{suffix}", status_code=303)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/{subscription_id}")
|
||||
def subscription_detail(request: Request, subscription_id: int):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
if not user:
|
||||
return RedirectResponse(url="/login", status_code=303)
|
||||
try:
|
||||
require_permission(db, user, "clients.view")
|
||||
except Exception:
|
||||
return _redirect_denied()
|
||||
|
||||
tenant_id = _active_tenant_id(request, user)
|
||||
row = get_subscription(db, subscription_id=subscription_id, tenant_id=tenant_id)
|
||||
if not row:
|
||||
return RedirectResponse(url="/services/engagements", status_code=303)
|
||||
active_fy = _active_financial_year(request)
|
||||
if row.financial_year != active_fy:
|
||||
return RedirectResponse(url=f"/services/engagements?financial_year={active_fy}", status_code=303)
|
||||
|
||||
tasks = db.execute(
|
||||
select(ClientServiceTaskInstance)
|
||||
.where(ClientServiceTaskInstance.subscription_id == row.id)
|
||||
.order_by(ClientServiceTaskInstance.sequence_no.asc(), ClientServiceTaskInstance.id.asc())
|
||||
).scalars().all()
|
||||
|
||||
return _render(
|
||||
request,
|
||||
"modules/services/templates/services/engagements/detail.html",
|
||||
db,
|
||||
user,
|
||||
title="Engagement Subscription",
|
||||
row=row,
|
||||
tasks=tasks,
|
||||
can_manage=_can_manage_client_services(db, user),
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/{subscription_id}/edit")
|
||||
def subscription_edit_page(request: Request, subscription_id: int):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
if not user:
|
||||
return RedirectResponse(url="/login", status_code=303)
|
||||
try:
|
||||
require_permission(db, user, "clients.edit")
|
||||
except Exception:
|
||||
return _redirect_denied()
|
||||
|
||||
tenant_id = _active_tenant_id(request, user)
|
||||
branch_id = _active_branch_id(request, user, db)
|
||||
row = get_subscription(db, subscription_id=subscription_id, tenant_id=tenant_id)
|
||||
if not row:
|
||||
return RedirectResponse(url="/services/engagements", status_code=303)
|
||||
active_fy = _active_financial_year(request)
|
||||
if row.financial_year != active_fy:
|
||||
return RedirectResponse(url=f"/services/engagements?financial_year={active_fy}", status_code=303)
|
||||
if getattr(row, "is_locked", False) or is_row_financial_year_locked(db, row):
|
||||
return RedirectResponse(url=f"/services/engagements/{row.id}?year_locked=1", status_code=303)
|
||||
|
||||
clients = list_clients_for_assignment(
|
||||
db,
|
||||
tenant_id=tenant_id,
|
||||
branch_id=branch_id,
|
||||
partner_id=_locked_partner_id(db, user),
|
||||
)
|
||||
enabled_services = list_enabled_services_for_assignment(db, tenant_id=tenant_id)
|
||||
assignable_users = list_assignable_users(db, tenant_id=tenant_id, branch_id=branch_id)
|
||||
review_partners = list_review_partners(db, tenant_id=tenant_id, branch_id=branch_id)
|
||||
return _render(
|
||||
request,
|
||||
"modules/services/templates/services/engagements/form.html",
|
||||
db,
|
||||
user,
|
||||
title="Edit Engagement Subscription",
|
||||
mode="edit",
|
||||
subscription=row,
|
||||
clients=clients,
|
||||
enabled_services=enabled_services,
|
||||
assignable_users=assignable_users,
|
||||
review_partners=review_partners,
|
||||
selected_client_id=row.client_id,
|
||||
financial_year=row.financial_year,
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/{subscription_id}/edit")
|
||||
def subscription_edit_submit(
|
||||
request: Request,
|
||||
subscription_id: int,
|
||||
assigned_partner_user_id: str = Form(""),
|
||||
assigned_manager_user_id: str = Form(""),
|
||||
assigned_staff_user_id: str = Form(""),
|
||||
review_partner_user_id: str = Form(""),
|
||||
start_date: str = Form(""),
|
||||
end_date: str = Form(""),
|
||||
expiry_date: str = Form(""),
|
||||
status: str = Form("active"),
|
||||
remarks: str = Form(""),
|
||||
is_active: str | None = Form(None),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
validate_csrf(request, csrf_token)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
if not user:
|
||||
return RedirectResponse(url="/login", status_code=303)
|
||||
try:
|
||||
require_permission(db, user, "clients.edit")
|
||||
except Exception:
|
||||
return _redirect_denied()
|
||||
|
||||
tenant_id = _active_tenant_id(request, user)
|
||||
row = get_subscription(db, subscription_id=subscription_id, tenant_id=tenant_id)
|
||||
if not row:
|
||||
return RedirectResponse(url="/services/engagements", status_code=303)
|
||||
active_fy = _active_financial_year(request)
|
||||
if row.financial_year != active_fy:
|
||||
return RedirectResponse(url=f"/services/engagements?financial_year={active_fy}", status_code=303)
|
||||
if getattr(row, "is_locked", False) or is_row_financial_year_locked(db, row):
|
||||
return RedirectResponse(url=f"/services/engagements/{row.id}?year_locked=1", status_code=303)
|
||||
|
||||
row.assigned_partner_user_id = int(assigned_partner_user_id) if assigned_partner_user_id.strip() else None
|
||||
row.assigned_manager_user_id = int(assigned_manager_user_id) if assigned_manager_user_id.strip() else None
|
||||
row.assigned_staff_user_id = int(assigned_staff_user_id) if assigned_staff_user_id.strip() else None
|
||||
if review_partner_required_for_engagement(db, tenant_id=tenant_id, engagement_type=row.engagement_type):
|
||||
row.review_partner_user_id = int(review_partner_user_id) if review_partner_user_id.strip() else getattr(row.client, "default_review_partner_user_id", None)
|
||||
else:
|
||||
row.review_partner_user_id = None
|
||||
row.start_date = parse_date(start_date)
|
||||
row.end_date = parse_date(end_date)
|
||||
row.expiry_date = parse_date(expiry_date)
|
||||
row.status = status or "active"
|
||||
row.remarks = remarks.strip() or None
|
||||
row.is_active = is_active is not None
|
||||
row.updated_by_user_id = user.id
|
||||
apply_due_date_rule_to_subscription(db, row)
|
||||
db.commit()
|
||||
return RedirectResponse(url=f"/services/engagements/{row.id}", status_code=303)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/{subscription_id}/lock")
|
||||
def subscription_lock(request: Request, subscription_id: int, csrf_token: str = Form(...)):
|
||||
validate_csrf(request, csrf_token)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
if not user:
|
||||
return RedirectResponse(url="/login", status_code=303)
|
||||
if not _can_lock_engagements(db, user):
|
||||
return _redirect_denied()
|
||||
tenant_id = _active_tenant_id(request, user)
|
||||
row = get_subscription(db, subscription_id=subscription_id, tenant_id=tenant_id)
|
||||
if row and row.financial_year != _active_financial_year(request):
|
||||
return RedirectResponse(url=f"/services/engagements?financial_year={_active_financial_year(request)}", status_code=303)
|
||||
if row and is_row_financial_year_locked(db, row):
|
||||
return RedirectResponse(url=f"/services/engagements/{row.id}?year_locked=1", status_code=303)
|
||||
if row and _user_can_lock_subscription(db, user, row):
|
||||
if _lock_subscription_row(row, user):
|
||||
db.commit()
|
||||
return RedirectResponse(url=f"/services/engagements/{subscription_id}", status_code=303)
|
||||
finally:
|
||||
db.close()
|
||||
Reference in New Issue
Block a user