Add engagement financial year correction workflow
This commit is contained in:
@@ -54,6 +54,10 @@ from app.modules.services.client_services import (
|
||||
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
|
||||
from app.modules.services.engagement_fy_correction import (
|
||||
correct_engagement_financial_year,
|
||||
validate_financial_year,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/services/engagements", tags=["services-engagements-ui"])
|
||||
|
||||
@@ -126,6 +130,25 @@ def _can_lock_engagements(db, user) -> bool:
|
||||
return bool(roles.intersection({"Firm Admin", "Partner"}))
|
||||
|
||||
|
||||
def _can_correct_engagement_financial_year(db, user) -> bool:
|
||||
roles = set(get_user_roles(db, user.id))
|
||||
return bool(roles.intersection({"Firm Admin", "Partner"}))
|
||||
|
||||
|
||||
def _user_can_correct_engagement_financial_year(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 bool(
|
||||
getattr(row, "assigned_partner_user_id", None) == user.id
|
||||
or getattr(row, "performing_partner_user_id", None) == user.id
|
||||
or getattr(client, "partner_id", None) == user.id
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def _user_can_lock_subscription(db, user, row: ClientServiceSubscription) -> bool:
|
||||
roles = set(get_user_roles(db, user.id))
|
||||
if "Firm Admin" in roles:
|
||||
@@ -217,6 +240,9 @@ def subscription_list(
|
||||
bulk_created: int = 0,
|
||||
bulk_existing: int = 0,
|
||||
bulk_skipped: int = 0,
|
||||
fy_corrected: int = 0,
|
||||
fy_skipped: int = 0,
|
||||
fy_error: str = "",
|
||||
):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
@@ -263,6 +289,10 @@ def subscription_list(
|
||||
bulk_skipped_count=bulk_skipped,
|
||||
can_manage=_can_manage_client_services(db, user),
|
||||
can_lock_engagements=_can_lock_engagements(db, user),
|
||||
can_correct_engagement_fy=_can_correct_engagement_financial_year(db, user),
|
||||
fy_corrected_count=fy_corrected,
|
||||
fy_skipped_count=fy_skipped,
|
||||
fy_error=fy_error,
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
@@ -773,6 +803,83 @@ def subscription_bulk_lock(
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/bulk-correct-financial-year")
|
||||
def subscription_bulk_correct_financial_year(
|
||||
request: Request,
|
||||
subscription_ids: list[int] = Form([]),
|
||||
target_financial_year: str = Form(...),
|
||||
correction_reason: str = 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()
|
||||
corrected = 0
|
||||
skipped = 0
|
||||
first_error = ""
|
||||
target_fy = (target_financial_year or "").strip()
|
||||
try:
|
||||
user = get_current_user(request, db=db)
|
||||
if not user:
|
||||
return RedirectResponse(url="/login", status_code=303)
|
||||
if not _can_correct_engagement_financial_year(db, user):
|
||||
return _redirect_denied()
|
||||
|
||||
tenant_id = _active_tenant_id(request, user)
|
||||
try:
|
||||
target_fy = validate_financial_year(target_fy)
|
||||
except ValueError as exc:
|
||||
from urllib.parse import quote_plus
|
||||
source_fy = normalize_financial_year(financial_year or _active_financial_year(request))
|
||||
return RedirectResponse(
|
||||
url=f"/services/engagements?financial_year={source_fy}&fy_error={quote_plus(str(exc))}",
|
||||
status_code=303,
|
||||
)
|
||||
|
||||
for subscription_id in [int(value) for value in subscription_ids if value]:
|
||||
row = get_subscription(db, subscription_id=subscription_id, tenant_id=tenant_id)
|
||||
if not row or not _user_can_correct_engagement_financial_year(db, user, row):
|
||||
skipped += 1
|
||||
if not first_error:
|
||||
first_error = "One or more selected engagements were unavailable or not permitted."
|
||||
continue
|
||||
try:
|
||||
with db.begin_nested():
|
||||
correct_engagement_financial_year(
|
||||
db,
|
||||
row=row,
|
||||
target_financial_year=target_fy,
|
||||
actor=user,
|
||||
reason=correction_reason,
|
||||
request=request,
|
||||
)
|
||||
corrected += 1
|
||||
except ValueError as exc:
|
||||
skipped += 1
|
||||
if not first_error:
|
||||
first_error = str(exc)
|
||||
|
||||
db.commit()
|
||||
from urllib.parse import quote_plus
|
||||
source_fy = normalize_financial_year(financial_year or _active_financial_year(request))
|
||||
params = [
|
||||
f"financial_year={source_fy}",
|
||||
f"fy_corrected={corrected}",
|
||||
f"fy_skipped={skipped}",
|
||||
]
|
||||
if q.strip():
|
||||
params.append(f"q={quote_plus(q.strip())}")
|
||||
if include_inactive:
|
||||
params.append("include_inactive=true")
|
||||
if first_error:
|
||||
params.append(f"fy_error={quote_plus(first_error)}")
|
||||
return RedirectResponse(url=f"/services/engagements?{'&'.join(params)}", status_code=303)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/{subscription_id}")
|
||||
def subscription_detail(request: Request, subscription_id: int):
|
||||
db = CommonSessionLocal()
|
||||
@@ -828,6 +935,10 @@ def subscription_detail(request: Request, subscription_id: int):
|
||||
closure_checklist=closure_checklist,
|
||||
closure_summary=closure_summary,
|
||||
can_manage=_can_manage_client_services(db, user),
|
||||
can_correct_engagement_fy=(
|
||||
_can_correct_engagement_financial_year(db, user)
|
||||
and _user_can_correct_engagement_financial_year(db, user, row)
|
||||
),
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
@@ -1101,6 +1212,63 @@ def subscription_closure_reopen(
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/{subscription_id}/correct-financial-year")
|
||||
def subscription_correct_financial_year(
|
||||
request: Request,
|
||||
subscription_id: int,
|
||||
target_financial_year: str = Form(...),
|
||||
correction_reason: str = Form(...),
|
||||
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_correct_engagement_financial_year(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 not row:
|
||||
return RedirectResponse(url="/services/engagements", status_code=303)
|
||||
if not _user_can_correct_engagement_financial_year(db, user, row):
|
||||
return _redirect_denied()
|
||||
|
||||
old_fy = row.financial_year
|
||||
try:
|
||||
result = correct_engagement_financial_year(
|
||||
db,
|
||||
row=row,
|
||||
target_financial_year=target_financial_year,
|
||||
actor=user,
|
||||
reason=correction_reason,
|
||||
request=request,
|
||||
)
|
||||
db.commit()
|
||||
except ValueError as exc:
|
||||
db.rollback()
|
||||
from urllib.parse import quote_plus
|
||||
return RedirectResponse(
|
||||
url=f"/services/engagements/{subscription_id}?fy_error={quote_plus(str(exc))}",
|
||||
status_code=303,
|
||||
)
|
||||
|
||||
# The active session FY may still be the old year. Return to the corrected
|
||||
# FY list rather than silently changing the user's global year context.
|
||||
from urllib.parse import quote_plus
|
||||
return RedirectResponse(
|
||||
url=(
|
||||
f"/services/engagements?financial_year={result.new_financial_year}"
|
||||
f"&fy_corrected=1&fy_from={quote_plus(old_fy)}"
|
||||
),
|
||||
status_code=303,
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/{subscription_id}/edit")
|
||||
def subscription_edit_page(request: Request, subscription_id: int):
|
||||
db = CommonSessionLocal()
|
||||
|
||||
Reference in New Issue
Block a user