Allow staff work assignments across financial years

This commit is contained in:
A R R R Associates
2026-08-26 17:27:32 +05:30
parent 33410ea141
commit 436a0e0766
3 changed files with 51 additions and 20 deletions
+25 -1
View File
@@ -3158,6 +3158,28 @@ def list_employee_work_kanban(
if page_size not in {10, 25, 50}: if page_size not in {10, 25, 50}:
page_size = 10 page_size = 10
# Staff work is cross-FY by default. The global ERP FY must not silently
# hide an engagement assigned to this employee. financial_year is therefore
# an explicit optional filter only.
fy_stmt = (
select(ClientServiceTaskInstance.financial_year)
.where(
ClientServiceTaskInstance.tenant_id == scope.tenant_id,
ClientServiceTaskInstance.is_active.is_(True),
_employee_owned_task_condition(scope),
)
)
if scope.branch_id is not None:
fy_stmt = fy_stmt.where(ClientServiceTaskInstance.branch_id == scope.branch_id)
financial_year_options = sorted(
{
str(value).strip()
for value in db.execute(fy_stmt).scalars().all()
if str(value or "").strip()
},
reverse=True,
)
stmt = _employee_work_task_query( stmt = _employee_work_task_query(
db, db,
scope, scope,
@@ -3309,7 +3331,7 @@ def list_employee_work_kanban(
if not is_closed and not card["next_task_name"]: if not is_closed and not card["next_task_name"]:
card["next_task_name"] = task.task_name card["next_task_name"] = task.task_name
# Options are derived from the employee's assigned engagements in the active FY. # Options are derived from the employee's assigned engagements in the selected FY, or all assigned years when no FY filter is selected.
period_options = sorted({str(card.get("period_label") or "").strip() for card in engagement_lookup.values() if str(card.get("period_label") or "").strip()}) period_options = sorted({str(card.get("period_label") or "").strip() for card in engagement_lookup.values() if str(card.get("period_label") or "").strip()})
service_options = sorted({str(card.get("service_name") or "").strip() for card in engagement_lookup.values() if str(card.get("service_name") or "").strip()}, key=str.casefold) service_options = sorted({str(card.get("service_name") or "").strip() for card in engagement_lookup.values() if str(card.get("service_name") or "").strip()}, key=str.casefold)
@@ -3493,6 +3515,7 @@ def list_employee_work_kanban(
"status": status_filter, "status": status_filter,
"today": today, "today": today,
"filters": { "filters": {
"financial_year": (financial_year or "").strip(),
"period": period_filter, "period": period_filter,
"service": service_filter, "service": service_filter,
"due": due_filter, "due": due_filter,
@@ -3500,6 +3523,7 @@ def list_employee_work_kanban(
"page_size": page_size, "page_size": page_size,
}, },
"filter_options": { "filter_options": {
"financial_years": financial_year_options,
"periods": period_options, "periods": period_options,
"services": service_options, "services": service_options,
}, },
@@ -31,7 +31,7 @@
</div> </div>
<form method="get" action="/employee/work" class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft"> <form method="get" action="/employee/work" class="rounded-2xl border border-slate-200 bg-white p-4 shadow-soft">
<div class="grid gap-3 lg:grid-cols-4 xl:grid-cols-8"> <div class="grid gap-3 lg:grid-cols-4 xl:grid-cols-9">
<input type="search" name="q" value="{{ q or '' }}" placeholder="Search business, client, GSTIN, task or service" class="lg:col-span-2 rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-100"> <input type="search" name="q" value="{{ q or '' }}" placeholder="Search business, client, GSTIN, task or service" class="lg:col-span-2 rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-100">
<select name="status" class="rounded-xl border border-slate-300 px-3 py-2 text-sm"> <select name="status" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
<option value="open" {% if status == 'open' %}selected{% endif %}>Open engagements</option> <option value="open" {% if status == 'open' %}selected{% endif %}>Open engagements</option>
@@ -41,6 +41,12 @@
<option value="completed" {% if status == 'completed' %}selected{% endif %}>Completed</option> <option value="completed" {% if status == 'completed' %}selected{% endif %}>Completed</option>
<option value="closed" {% if status == 'closed' %}selected{% endif %}>Closed engagements</option> <option value="closed" {% if status == 'closed' %}selected{% endif %}>Closed engagements</option>
</select> </select>
<select name="financial_year" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
<option value="" {% if not financial_year %}selected{% endif %}>All assigned FYs</option>
{% for value in work_payload.filter_options.financial_years %}
<option value="{{ value }}" {% if financial_year == value %}selected{% endif %}>FY {{ value }}</option>
{% endfor %}
</select>
<select name="period" class="rounded-xl border border-slate-300 px-3 py-2 text-sm"> <select name="period" class="rounded-xl border border-slate-300 px-3 py-2 text-sm">
<option value="">All periods</option> <option value="">All periods</option>
{% for value in work_payload.filter_options.periods %}<option value="{{ value }}" {% if period == value %}selected{% endif %}>{{ value }}</option>{% endfor %} {% for value in work_payload.filter_options.periods %}<option value="{{ value }}" {% if period == value %}selected{% endif %}>{{ value }}</option>{% endfor %}
@@ -71,7 +77,7 @@
</select> </select>
</div> </div>
<div class="mt-3 flex flex-wrap items-center justify-between gap-3"> <div class="mt-3 flex flex-wrap items-center justify-between gap-3">
<div class="text-xs text-slate-500">FY {{ financial_year }} · Default ordering remains earliest due date first.</div> <div class="text-xs text-slate-500">{% if financial_year %}Filtered to FY {{ financial_year }}{% else %}All assigned financial years{% endif %} · Default ordering remains earliest due date first.</div>
<div class="flex gap-2"> <div class="flex gap-2">
<a href="/employee/work" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Clear</a> <a href="/employee/work" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Clear</a>
<button type="submit" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Apply</button> <button type="submit" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Apply</button>
@@ -141,14 +147,14 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
{% if column.pagination.has_previous %} {% if column.pagination.has_previous %}
<form method="get" action="/employee/work"> <form method="get" action="/employee/work">
<input type="hidden" name="q" value="{{ q or '' }}"><input type="hidden" name="status" value="{{ status }}"><input type="hidden" name="period" value="{{ period or '' }}"><input type="hidden" name="service" value="{{ service or '' }}"><input type="hidden" name="due" value="{{ due or '' }}"><input type="hidden" name="sort" value="{{ sort or 'due_asc' }}"><input type="hidden" name="page_size" value="{{ page_size }}"> <input type="hidden" name="q" value="{{ q or '' }}"><input type="hidden" name="status" value="{{ status }}"><input type="hidden" name="financial_year" value="{{ financial_year or '' }}"><input type="hidden" name="period" value="{{ period or '' }}"><input type="hidden" name="service" value="{{ service or '' }}"><input type="hidden" name="due" value="{{ due or '' }}"><input type="hidden" name="sort" value="{{ sort or 'due_asc' }}"><input type="hidden" name="page_size" value="{{ page_size }}">
<input type="hidden" name="in_progress_page" value="{{ column.pagination.previous_page if column.code == 'in_progress' else work_payload.paginations.in_progress.page }}"><input type="hidden" name="pending_page" value="{{ column.pagination.previous_page if column.code == 'pending' else work_payload.paginations.pending.page }}"><input type="hidden" name="blocked_page" value="{{ column.pagination.previous_page if column.code == 'blocked' else work_payload.paginations.blocked.page }}"><input type="hidden" name="completed_page" value="{{ column.pagination.previous_page if column.code == 'completed' else work_payload.paginations.completed.page }}"> <input type="hidden" name="in_progress_page" value="{{ column.pagination.previous_page if column.code == 'in_progress' else work_payload.paginations.in_progress.page }}"><input type="hidden" name="pending_page" value="{{ column.pagination.previous_page if column.code == 'pending' else work_payload.paginations.pending.page }}"><input type="hidden" name="blocked_page" value="{{ column.pagination.previous_page if column.code == 'blocked' else work_payload.paginations.blocked.page }}"><input type="hidden" name="completed_page" value="{{ column.pagination.previous_page if column.code == 'completed' else work_payload.paginations.completed.page }}">
<button type="submit" class="rounded-lg border border-slate-300 bg-white px-3 py-1.5 text-xs font-semibold text-slate-700 hover:bg-slate-50">Previous</button> <button type="submit" class="rounded-lg border border-slate-300 bg-white px-3 py-1.5 text-xs font-semibold text-slate-700 hover:bg-slate-50">Previous</button>
</form>{% endif %} </form>{% endif %}
<span class="rounded-lg bg-slate-100 px-3 py-1.5 text-xs font-semibold text-slate-700">Page {{ column.pagination.page }} of {{ column.pagination.pages }}</span> <span class="rounded-lg bg-slate-100 px-3 py-1.5 text-xs font-semibold text-slate-700">Page {{ column.pagination.page }} of {{ column.pagination.pages }}</span>
{% if column.pagination.has_next %} {% if column.pagination.has_next %}
<form method="get" action="/employee/work"> <form method="get" action="/employee/work">
<input type="hidden" name="q" value="{{ q or '' }}"><input type="hidden" name="status" value="{{ status }}"><input type="hidden" name="period" value="{{ period or '' }}"><input type="hidden" name="service" value="{{ service or '' }}"><input type="hidden" name="due" value="{{ due or '' }}"><input type="hidden" name="sort" value="{{ sort or 'due_asc' }}"><input type="hidden" name="page_size" value="{{ page_size }}"> <input type="hidden" name="q" value="{{ q or '' }}"><input type="hidden" name="status" value="{{ status }}"><input type="hidden" name="financial_year" value="{{ financial_year or '' }}"><input type="hidden" name="period" value="{{ period or '' }}"><input type="hidden" name="service" value="{{ service or '' }}"><input type="hidden" name="due" value="{{ due or '' }}"><input type="hidden" name="sort" value="{{ sort or 'due_asc' }}"><input type="hidden" name="page_size" value="{{ page_size }}">
<input type="hidden" name="in_progress_page" value="{{ column.pagination.next_page if column.code == 'in_progress' else work_payload.paginations.in_progress.page }}"><input type="hidden" name="pending_page" value="{{ column.pagination.next_page if column.code == 'pending' else work_payload.paginations.pending.page }}"><input type="hidden" name="blocked_page" value="{{ column.pagination.next_page if column.code == 'blocked' else work_payload.paginations.blocked.page }}"><input type="hidden" name="completed_page" value="{{ column.pagination.next_page if column.code == 'completed' else work_payload.paginations.completed.page }}"> <input type="hidden" name="in_progress_page" value="{{ column.pagination.next_page if column.code == 'in_progress' else work_payload.paginations.in_progress.page }}"><input type="hidden" name="pending_page" value="{{ column.pagination.next_page if column.code == 'pending' else work_payload.paginations.pending.page }}"><input type="hidden" name="blocked_page" value="{{ column.pagination.next_page if column.code == 'blocked' else work_payload.paginations.blocked.page }}"><input type="hidden" name="completed_page" value="{{ column.pagination.next_page if column.code == 'completed' else work_payload.paginations.completed.page }}">
<button type="submit" class="rounded-lg border border-slate-300 bg-white px-3 py-1.5 text-xs font-semibold text-slate-700 hover:bg-slate-50">Next</button> <button type="submit" class="rounded-lg border border-slate-300 bg-white px-3 py-1.5 text-xs font-semibold text-slate-700 hover:bg-slate-50">Next</button>
</form>{% endif %} </form>{% endif %}
+16 -15
View File
@@ -1934,7 +1934,7 @@ def employee_work_assignment_dashboard(
selected_assigned_to_user_id=assignee_id, selected_assigned_to_user_id=assignee_id,
scope=scope, scope=scope,
errors=[], errors=[],
financial_year=financial_year, financial_year=selected_financial_year or "",
) )
finally: finally:
db.close() db.close()
@@ -2018,7 +2018,7 @@ def employee_my_work_task_status(
status=status, status=status,
remarks=remarks, remarks=remarks,
actor_user_id=current_user.id, actor_user_id=current_user.id,
financial_year=_active_financial_year(request), financial_year=None,
) )
safe_return_url = return_url if return_url.startswith("/employee/work") else "/employee/work" safe_return_url = return_url if return_url.startswith("/employee/work") else "/employee/work"
return RedirectResponse(url=safe_return_url, status_code=303) return RedirectResponse(url=safe_return_url, status_code=303)
@@ -2031,6 +2031,7 @@ def employee_my_work(
request: Request, request: Request,
q: str = "", q: str = "",
status: str = "open", status: str = "open",
financial_year: str = "",
period: str = "", period: str = "",
service: str = "", service: str = "",
due: str = "", due: str = "",
@@ -2054,13 +2055,13 @@ def employee_my_work(
branch_id = request.session.get("active_branch_id") branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id) scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
employee = get_employee_for_user(db, current_user) employee = get_employee_for_user(db, current_user)
financial_year = _active_financial_year(request) selected_financial_year = (financial_year or "").strip() or None
work_payload = list_employee_work_kanban( work_payload = list_employee_work_kanban(
db, db,
scope, scope,
q=q, q=q,
status=status, status=status,
financial_year=financial_year, financial_year=selected_financial_year,
period=period, period=period,
service=service, service=service,
due_bucket=due, due_bucket=due,
@@ -2125,7 +2126,7 @@ def employee_my_work_engagement_start(
scope, scope,
engagement_id, engagement_id,
actor_user_id=current_user.id, actor_user_id=current_user.id,
financial_year=_active_financial_year(request), financial_year=None,
) )
return RedirectResponse( return RedirectResponse(
url=f"/employee/work/engagements/{engagement_id}", url=f"/employee/work/engagements/{engagement_id}",
@@ -2150,7 +2151,7 @@ def employee_my_work_engagement_board(request: Request, engagement_id: int, task
branch_id = request.session.get("active_branch_id") branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id) scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
employee = get_employee_for_user(db, current_user) employee = get_employee_for_user(db, current_user)
financial_year = _active_financial_year(request) financial_year = None
board = get_employee_engagement_work_board( board = get_employee_engagement_work_board(
db, db,
scope, scope,
@@ -2185,7 +2186,7 @@ def employee_workflow_pause(request: Request, engagement_id: int, reason: str =
except PermissionError: return _csrf_rejected(request) except PermissionError: return _csrf_rejected(request)
scope = build_employee_scope(db, current_user, tenant_id=request.session.get("active_tenant_id") or current_user.tenant_id, branch_id=request.session.get("active_branch_id")) scope = build_employee_scope(db, current_user, tenant_id=request.session.get("active_tenant_id") or current_user.tenant_id, branch_id=request.session.get("active_branch_id"))
try: try:
pause_employee_engagement_workflow(db, scope, engagement_id, reason=reason, notes=notes, follow_up_date=follow_up_date, actor_user_id=current_user.id, financial_year=_active_financial_year(request)) pause_employee_engagement_workflow(db, scope, engagement_id, reason=reason, notes=notes, follow_up_date=follow_up_date, actor_user_id=current_user.id, financial_year=None)
except ValueError: except ValueError:
db.rollback(); return RedirectResponse(url=f"/employee/work/engagements/{engagement_id}?pause_error=1", status_code=303) db.rollback(); return RedirectResponse(url=f"/employee/work/engagements/{engagement_id}?pause_error=1", status_code=303)
return RedirectResponse(url=f"/employee/work/engagements/{engagement_id}?paused=1", status_code=303) return RedirectResponse(url=f"/employee/work/engagements/{engagement_id}?paused=1", status_code=303)
@@ -2204,7 +2205,7 @@ def employee_workflow_resume(request: Request, engagement_id: int, csrf_token: s
except PermissionError: return _csrf_rejected(request) except PermissionError: return _csrf_rejected(request)
scope = build_employee_scope(db, current_user, tenant_id=request.session.get("active_tenant_id") or current_user.tenant_id, branch_id=request.session.get("active_branch_id")) scope = build_employee_scope(db, current_user, tenant_id=request.session.get("active_tenant_id") or current_user.tenant_id, branch_id=request.session.get("active_branch_id"))
try: try:
resume_employee_engagement_workflow(db, scope, engagement_id, actor_user_id=current_user.id, financial_year=_active_financial_year(request)) resume_employee_engagement_workflow(db, scope, engagement_id, actor_user_id=current_user.id, financial_year=None)
except ValueError: except ValueError:
db.rollback(); return RedirectResponse(url=f"/employee/work/engagements/{engagement_id}?resume_error=1", status_code=303) db.rollback(); return RedirectResponse(url=f"/employee/work/engagements/{engagement_id}?resume_error=1", status_code=303)
return RedirectResponse(url=f"/employee/work/engagements/{engagement_id}?resumed=1", status_code=303) return RedirectResponse(url=f"/employee/work/engagements/{engagement_id}?resumed=1", status_code=303)
@@ -2234,7 +2235,7 @@ def employee_workflow_escalate(
escalate_employee_engagement_workflow( escalate_employee_engagement_workflow(
db, scope, engagement_id, level=level, message=message, db, scope, engagement_id, level=level, message=message,
actor_user_id=current_user.id, actor_user_id=current_user.id,
financial_year=request.session.get("active_financial_year"), financial_year=None,
) )
except ValueError: except ValueError:
db.rollback() db.rollback()
@@ -2277,7 +2278,7 @@ def employee_workflow_task_quick_action(
action=action, action=action,
reason=reason, reason=reason,
actor_user_id=current_user.id, actor_user_id=current_user.id,
financial_year=_active_financial_year(request), financial_year=None,
) )
except ValueError as exc: except ValueError as exc:
db.rollback() db.rollback()
@@ -2341,7 +2342,7 @@ def employee_workflow_task_save(
checklist_date_response=checklist_date_response, checklist_date_response=checklist_date_response,
checklist_remarks=checklist_remarks, checklist_remarks=checklist_remarks,
actor_user_id=current_user.id, actor_user_id=current_user.id,
financial_year=_active_financial_year(request), financial_year=None,
) )
except ValueError: except ValueError:
db.rollback() db.rollback()
@@ -2431,7 +2432,7 @@ def employee_work_task_add_communication(
visibility=visibility, visibility=visibility,
message=message, message=message,
actor_user_id=current_user.id, actor_user_id=current_user.id,
financial_year=_active_financial_year(request), financial_year=None,
) )
return RedirectResponse(url=_safe_employee_return_url(return_to, f"/employees/work/tasks/{task_id}/communication"), status_code=303) return RedirectResponse(url=_safe_employee_return_url(return_to, f"/employees/work/tasks/{task_id}/communication"), status_code=303)
finally: finally:
@@ -2455,7 +2456,7 @@ def employee_my_work_task_communication(
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
branch_id = request.session.get("active_branch_id") branch_id = request.session.get("active_branch_id")
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id) scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
financial_year = _active_financial_year(request) financial_year = None
task = get_work_task_with_communications(db, scope, task_id, assigned_only=True, financial_year=financial_year) task = get_work_task_with_communications(db, scope, task_id, assigned_only=True, financial_year=financial_year)
return _render( return _render(
request, request,
@@ -2512,7 +2513,7 @@ def employee_my_work_task_add_communication(
message=message, message=message,
actor_user_id=current_user.id, actor_user_id=current_user.id,
assigned_only=True, assigned_only=True,
financial_year=_active_financial_year(request), financial_year=None,
) )
return RedirectResponse(url=_safe_employee_return_url(return_to, f"/employee/work/tasks/{task_id}/communication"), status_code=303) return RedirectResponse(url=_safe_employee_return_url(return_to, f"/employee/work/tasks/{task_id}/communication"), status_code=303)
finally: finally:
@@ -2870,7 +2871,7 @@ def _employee_portal_context(request: Request, db, current_user, *, active_tab:
scope, scope,
q="", q="",
status="open", status="open",
financial_year=_active_financial_year(request), financial_year=None,
) )
return { return {
"title": "Employee Portal", "title": "Employee Portal",