2642 lines
109 KiB
Python
2642 lines
109 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from io import BytesIO
|
|
from uuid import uuid4
|
|
|
|
from fastapi import APIRouter, File, Form, Request, UploadFile
|
|
from fastapi.responses import RedirectResponse, StreamingResponse
|
|
|
|
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.core.rbac.permission_guard import require_permission
|
|
from app.modules.core.iam.profile_service import save_user_profile_photo, update_user_public_profile
|
|
from app.modules.employees.import_service import (
|
|
build_template_workbook,
|
|
commit_import,
|
|
preview_import,
|
|
supported_import_types,
|
|
)
|
|
from app.modules.employees.service import (
|
|
EMPLOYEE_ROLE_NAMES,
|
|
EMPLOYEE_STATUS,
|
|
EMPLOYMENT_TYPES,
|
|
build_employee_scope,
|
|
change_employee_status,
|
|
create_employee,
|
|
get_employee_or_404,
|
|
get_employee_user_link_summary,
|
|
link_employee_to_user,
|
|
list_employees,
|
|
list_linkable_users,
|
|
list_reporting_managers,
|
|
parse_date,
|
|
update_employee,
|
|
visible_branches,
|
|
visible_tenants,
|
|
REGISTRATION_STATUS,
|
|
approve_employee_registration_request,
|
|
create_employee_registration_request,
|
|
get_employee_for_user,
|
|
get_employee_dashboard_stats,
|
|
list_employee_work_dashboard,
|
|
list_employee_work_kanban,
|
|
get_employee_engagement_work_board,
|
|
list_employee_work_assignable_users,
|
|
list_visible_work_assignment_dashboard,
|
|
list_engagement_progress_dashboard,
|
|
update_service_task_assignment,
|
|
update_own_service_task_status,
|
|
TASK_COMMUNICATION_TYPES,
|
|
TASK_COMMUNICATION_VISIBILITIES,
|
|
add_work_task_communication,
|
|
get_work_task_with_communications,
|
|
get_employee_registration_request_or_404,
|
|
get_pending_registration_for_user,
|
|
list_employee_registration_requests,
|
|
reject_employee_registration_request,
|
|
update_own_employee_profile,
|
|
ATTENDANCE_APPROVAL_STATUS,
|
|
ATTENDANCE_STATUS,
|
|
create_or_update_manual_attendance,
|
|
get_attendance_or_404,
|
|
get_today_attendance_for_user,
|
|
list_attendance_records,
|
|
list_own_attendance,
|
|
punch_in_attendance,
|
|
punch_out_attendance,
|
|
review_attendance_record,
|
|
LEAVE_REQUEST_STATUS,
|
|
adjust_leave_balance,
|
|
apply_employee_leave,
|
|
cancel_own_leave_request,
|
|
create_leave_type,
|
|
ensure_default_leave_types,
|
|
get_leave_request_or_404,
|
|
get_leave_type_or_404,
|
|
list_leave_balances,
|
|
list_leave_requests,
|
|
list_leave_types,
|
|
list_own_leave_balances,
|
|
list_own_leave_requests,
|
|
review_leave_request,
|
|
update_leave_type,
|
|
DOCUMENT_STATUS,
|
|
DOCUMENT_VISIBILITY,
|
|
archive_employee_document,
|
|
create_document_type,
|
|
create_employee_document_record,
|
|
ensure_default_document_types,
|
|
get_document_type_or_404,
|
|
get_employee_document_or_404,
|
|
list_document_types,
|
|
list_employee_documents,
|
|
list_own_employee_documents,
|
|
review_employee_document,
|
|
update_document_type,
|
|
ONBOARDING_TASK_STATUS,
|
|
OFFBOARDING_REQUEST_STATUS,
|
|
OFFBOARDING_TASK_STATUS,
|
|
create_offboarding_request,
|
|
create_onboarding_checklist_item,
|
|
complete_offboarding_request,
|
|
ensure_default_onboarding_checklist,
|
|
generate_onboarding_tasks_for_employee,
|
|
get_offboarding_request_or_404,
|
|
get_offboarding_task_or_404,
|
|
get_onboarding_checklist_item_or_404,
|
|
get_onboarding_task_or_404,
|
|
list_offboarding_requests,
|
|
list_offboarding_tasks,
|
|
list_onboarding_checklist_items,
|
|
list_onboarding_tasks,
|
|
list_own_offboarding_requests,
|
|
review_offboarding_request,
|
|
update_offboarding_task_status,
|
|
update_onboarding_checklist_item,
|
|
update_onboarding_task_status,
|
|
PAYROLL_RUN_STATUS,
|
|
PAYSLIP_STATUS,
|
|
approve_payroll_run,
|
|
create_payroll_run,
|
|
create_salary_structure,
|
|
generate_payslips_for_run,
|
|
get_payroll_run_or_404,
|
|
get_salary_structure_or_404,
|
|
list_own_payslips,
|
|
list_payroll_runs,
|
|
list_payslips,
|
|
list_salary_structures,
|
|
mark_payroll_run_paid,
|
|
update_salary_structure,
|
|
)
|
|
|
|
router = APIRouter(prefix="/employees", tags=["employees-ui"])
|
|
portal_router = APIRouter(prefix="/employee", tags=["employee-portal-ui"])
|
|
|
|
|
|
def _redirect_login():
|
|
return RedirectResponse(url="/login", status_code=303)
|
|
|
|
|
|
def _redirect_denied():
|
|
return RedirectResponse(url="/system-settings", status_code=303)
|
|
|
|
|
|
def _base_ctx(request: Request, db, 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),
|
|
"employee_statuses": EMPLOYEE_STATUS,
|
|
"employment_types": EMPLOYMENT_TYPES,
|
|
"employee_role_names": EMPLOYEE_ROLE_NAMES,
|
|
"attendance_statuses": ATTENDANCE_STATUS,
|
|
"attendance_approval_statuses": ATTENDANCE_APPROVAL_STATUS,
|
|
"leave_request_statuses": LEAVE_REQUEST_STATUS,
|
|
"document_statuses": DOCUMENT_STATUS,
|
|
"document_visibilities": DOCUMENT_VISIBILITY,
|
|
"onboarding_task_statuses": ONBOARDING_TASK_STATUS,
|
|
"offboarding_request_statuses": OFFBOARDING_REQUEST_STATUS,
|
|
"offboarding_task_statuses": OFFBOARDING_TASK_STATUS,
|
|
"payroll_run_statuses": PAYROLL_RUN_STATUS,
|
|
"payslip_statuses": PAYSLIP_STATUS,
|
|
}
|
|
base.update(ctx)
|
|
return base
|
|
|
|
|
|
def _render(request: Request, template: str, db, current_user, **ctx):
|
|
return templates.TemplateResponse(template, _base_ctx(request, db, current_user, **ctx))
|
|
|
|
|
|
def _client_ip(request: Request) -> str | None:
|
|
forwarded_for = request.headers.get("x-forwarded-for")
|
|
if forwarded_for:
|
|
return forwarded_for.split(",")[0].strip() or None
|
|
real_ip = request.headers.get("x-real-ip")
|
|
if real_ip:
|
|
return real_ip.strip() or None
|
|
return request.client.host if request.client 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 _form_bool(value) -> bool:
|
|
return value in ("1", "true", "True", "on", "yes")
|
|
|
|
|
|
def _form_payload(form, *, include_context: bool = False):
|
|
payload = {
|
|
"user_id": int(form.get("user_id")) if form.get("user_id") not in (None, "", "None") else None,
|
|
"employee_code": form.get("employee_code", ""),
|
|
"full_name": form.get("full_name", ""),
|
|
"email": form.get("email"),
|
|
"mobile": form.get("mobile"),
|
|
"alternate_mobile": form.get("alternate_mobile"),
|
|
"date_of_joining": form.get("date_of_joining"),
|
|
"date_of_leaving": form.get("date_of_leaving"),
|
|
"employment_type": form.get("employment_type") or "full_time",
|
|
"status": form.get("status") or "active",
|
|
"is_active": _form_bool(form.get("is_active")),
|
|
"department": form.get("department"),
|
|
"designation": form.get("designation"),
|
|
"reporting_manager_user_id": int(form.get("reporting_manager_user_id")) if form.get("reporting_manager_user_id") not in (None, "", "None") else None,
|
|
"pan": form.get("pan"),
|
|
"uan": form.get("uan"),
|
|
"esi_no": form.get("esi_no"),
|
|
"pf_no": form.get("pf_no"),
|
|
"aadhaar_last4": form.get("aadhaar_last4"),
|
|
"bank_name": form.get("bank_name"),
|
|
"bank_account_no": form.get("bank_account_no"),
|
|
"bank_ifsc": form.get("bank_ifsc"),
|
|
"address": form.get("address"),
|
|
"emergency_contact_name": form.get("emergency_contact_name"),
|
|
"emergency_contact_mobile": form.get("emergency_contact_mobile"),
|
|
"notes": form.get("notes"),
|
|
"create_login_user": _form_bool(form.get("create_login_user")),
|
|
"login_email": form.get("login_email"),
|
|
"temporary_password": form.get("temporary_password"),
|
|
"employee_role": form.get("employee_role") or "Staff",
|
|
}
|
|
if include_context:
|
|
payload["tenant_id"] = int(form.get("tenant_id")) if form.get("tenant_id") not in (None, "", "None") else None
|
|
payload["branch_id"] = int(form.get("branch_id")) if form.get("branch_id") not in (None, "", "None") else None
|
|
return payload
|
|
|
|
|
|
def _form_options(db, current_user, scope, *, include_user_id: int | None = None):
|
|
return {
|
|
"tenants": visible_tenants(db, current_user),
|
|
"branches": visible_branches(db, current_user, scope.tenant_id),
|
|
"users": list_linkable_users(db, scope, include_user_id=include_user_id),
|
|
"managers": list_reporting_managers(db, scope),
|
|
"scope": scope,
|
|
}
|
|
|
|
|
|
UPLOAD_ROOT = Path("data/uploads/employee_documents")
|
|
ALLOWED_DOCUMENT_EXTENSIONS = {".pdf", ".jpg", ".jpeg", ".png", ".webp", ".doc", ".docx", ".xls", ".xlsx"}
|
|
|
|
|
|
def _document_type_payload(form):
|
|
return {
|
|
"code": form.get("code"),
|
|
"name": form.get("name"),
|
|
"description": form.get("description"),
|
|
"is_mandatory": _form_bool(form.get("is_mandatory")),
|
|
"allow_employee_upload": _form_bool(form.get("allow_employee_upload")),
|
|
"requires_verification": _form_bool(form.get("requires_verification")),
|
|
"is_active": _form_bool(form.get("is_active")),
|
|
}
|
|
|
|
|
|
def _safe_upload_filename(filename: str) -> str:
|
|
base = Path(filename or "document").name.replace(" ", "_")
|
|
keep = []
|
|
for ch in base:
|
|
keep.append(ch if ch.isalnum() or ch in {".", "_", "-"} else "_")
|
|
safe = "".join(keep).strip("._") or "document"
|
|
return safe[:180]
|
|
|
|
|
|
|
|
|
|
def _onboarding_item_payload(form):
|
|
return {
|
|
"code": form.get("code"),
|
|
"title": form.get("title"),
|
|
"description": form.get("description"),
|
|
"stage": form.get("stage") or "joining",
|
|
"default_due_days": int(form.get("default_due_days") or 0),
|
|
"sort_order": int(form.get("sort_order") or 0),
|
|
"is_mandatory": _form_bool(form.get("is_mandatory")),
|
|
"is_active": _form_bool(form.get("is_active")),
|
|
}
|
|
|
|
|
|
def _offboarding_payload(form):
|
|
return {
|
|
"request_type": form.get("request_type") or "resignation",
|
|
"requested_relieving_date": form.get("requested_relieving_date"),
|
|
"reason": form.get("reason"),
|
|
"handover_notes": form.get("handover_notes"),
|
|
}
|
|
|
|
|
|
async def _save_employee_upload(upload: UploadFile, *, tenant_id: int, employee_id: int) -> tuple[str, str, str, int | None, str | None]:
|
|
original = _safe_upload_filename(upload.filename or "document")
|
|
suffix = Path(original).suffix.lower()
|
|
if suffix not in ALLOWED_DOCUMENT_EXTENSIONS:
|
|
raise ValueError("Unsupported file type. Allowed: PDF, images, Word and Excel files.")
|
|
data = await upload.read()
|
|
if not data:
|
|
raise ValueError("Please choose a non-empty file to upload.")
|
|
max_size = 10 * 1024 * 1024
|
|
if len(data) > max_size:
|
|
raise ValueError("File size should not exceed 10 MB.")
|
|
folder = UPLOAD_ROOT / str(tenant_id) / str(employee_id)
|
|
folder.mkdir(parents=True, exist_ok=True)
|
|
stored = f"{uuid4().hex}{suffix}"
|
|
path = folder / stored
|
|
path.write_bytes(data)
|
|
return original, stored, str(path.as_posix()), len(data), upload.content_type
|
|
|
|
|
|
|
|
|
|
@router.get("/dashboard")
|
|
def employee_hr_dashboard(request: Request):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.dashboard.view")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
tenant_id = request.session.get("active_tenant_id")
|
|
branch_id = request.session.get("active_branch_id")
|
|
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
|
|
stats = get_employee_dashboard_stats(db, scope)
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/hr_dashboard.html",
|
|
db,
|
|
current_user,
|
|
title="HR Dashboard",
|
|
stats=stats,
|
|
scope=scope,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("")
|
|
def employees_list(request: Request, q: str = "", include_inactive: str | None = None, link_status: str = "all"):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.view")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
normalized_link_status = link_status if link_status in ("all", "linked", "unlinked") else "all"
|
|
rows = list_employees(
|
|
db,
|
|
scope,
|
|
q=q,
|
|
include_inactive=bool(include_inactive),
|
|
link_status=normalized_link_status,
|
|
)
|
|
link_summary = get_employee_user_link_summary(db, scope)
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/list.html",
|
|
db,
|
|
current_user,
|
|
title="Employees",
|
|
rows=rows,
|
|
q=q,
|
|
include_inactive=bool(include_inactive),
|
|
link_status=normalized_link_status,
|
|
link_summary=link_summary,
|
|
scope=scope,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/new")
|
|
def employee_new(request: Request):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.create")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/form.html",
|
|
db,
|
|
current_user,
|
|
title="Add Employee",
|
|
employee=None,
|
|
errors=[],
|
|
mode="create",
|
|
**_form_options(db, current_user, scope),
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/new")
|
|
async def employee_create_submit(request: Request):
|
|
form = await request.form()
|
|
validate_csrf(request, form.get("csrf_token"))
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.create")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
scope = build_employee_scope(db, current_user, tenant_id=form.get("tenant_id"), branch_id=form.get("branch_id"))
|
|
payload = _form_payload(form, include_context=True)
|
|
try:
|
|
emp = create_employee(db, current_user, scope, payload)
|
|
return RedirectResponse(url=f"/employees/{emp.id}", status_code=303)
|
|
except Exception as exc:
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/form.html",
|
|
db,
|
|
current_user,
|
|
title="Add Employee",
|
|
employee=payload,
|
|
errors=[getattr(exc, "detail", str(exc))],
|
|
mode="create",
|
|
**_form_options(db, current_user, scope),
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/attendance")
|
|
def employees_attendance_list(
|
|
request: Request,
|
|
employee_id: int | None = None,
|
|
from_date: str | None = None,
|
|
to_date: str | None = None,
|
|
status: str | None = None,
|
|
approval_status: str | None = None,
|
|
):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.attendance.view_all")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
rows = list_attendance_records(
|
|
db,
|
|
scope,
|
|
employee_id=employee_id,
|
|
from_date=parse_date(from_date),
|
|
to_date=parse_date(to_date),
|
|
status=status or None,
|
|
approval_status=approval_status or None,
|
|
)
|
|
employees = list_employees(db, scope, include_inactive=True)
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/attendance_list.html",
|
|
db,
|
|
current_user,
|
|
title="Employee Attendance",
|
|
rows=rows,
|
|
employees=employees,
|
|
selected_employee_id=employee_id,
|
|
from_date=from_date or "",
|
|
to_date=to_date or "",
|
|
selected_status=status or "",
|
|
selected_approval_status=approval_status or "",
|
|
scope=scope,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/attendance/manual")
|
|
def employees_attendance_manual_submit(
|
|
request: Request,
|
|
employee_id: int = Form(...),
|
|
attendance_date: str = Form(...),
|
|
status: str = Form(...),
|
|
remarks: str | None = Form(None),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.attendance.approve")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
create_or_update_manual_attendance(
|
|
db,
|
|
current_user,
|
|
scope,
|
|
employee_id=employee_id,
|
|
attendance_date=parse_date(attendance_date),
|
|
status=status,
|
|
remarks=remarks,
|
|
)
|
|
return RedirectResponse(url="/employees/attendance", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/attendance/{attendance_id}/review")
|
|
def employees_attendance_review_submit(
|
|
request: Request,
|
|
attendance_id: int,
|
|
approval_status: str = Form(...),
|
|
review_notes: str | None = Form(None),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.attendance.approve")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
row = get_attendance_or_404(db, attendance_id, scope)
|
|
review_attendance_record(db, current_user, row, approval_status=approval_status, review_notes=review_notes)
|
|
return RedirectResponse(url="/employees/attendance", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/leave-types")
|
|
def employee_leave_types(request: Request, include_inactive: str | None = None):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.leave_type.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
|
|
branch_id = request.session.get("active_branch_id") or current_user.branch_id
|
|
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
|
|
rows = list_leave_types(db, scope, include_inactive=bool(include_inactive))
|
|
return _render(request, "modules/employees/templates/employees/leave_types.html", db, current_user, title="Employee Leave Types", rows=rows, scope=scope, errors=[], include_inactive=bool(include_inactive))
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/leave-types/defaults")
|
|
def employee_leave_type_defaults(request: Request, csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.leave_type.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
|
|
branch_id = request.session.get("active_branch_id") or current_user.branch_id
|
|
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
|
|
ensure_default_leave_types(db, current_user, scope)
|
|
return RedirectResponse(url="/employees/leave-types", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/leave-types/new")
|
|
def employee_leave_type_create(request: Request, code: str = Form(...), name: str = Form(...), description: str | None = Form(None), annual_quota_days: int = Form(0), carry_forward_allowed: str | None = Form(None), allow_negative_balance: str | None = Form(None), requires_approval: str | None = Form(None), is_paid: str | None = Form(None), is_active: str | None = Form(None), csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.leave_type.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
|
|
branch_id = request.session.get("active_branch_id") or current_user.branch_id
|
|
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
|
|
create_leave_type(db, current_user, scope, {"code": code, "name": name, "description": description, "annual_quota_days": annual_quota_days, "carry_forward_allowed": _form_bool(carry_forward_allowed), "allow_negative_balance": _form_bool(allow_negative_balance), "requires_approval": _form_bool(requires_approval), "is_paid": _form_bool(is_paid), "is_active": _form_bool(is_active)})
|
|
return RedirectResponse(url="/employees/leave-types", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/leave-types/{leave_type_id}/edit")
|
|
def employee_leave_type_edit(request: Request, leave_type_id: int, name: str = Form(...), description: str | None = Form(None), annual_quota_days: int = Form(0), carry_forward_allowed: str | None = Form(None), allow_negative_balance: str | None = Form(None), requires_approval: str | None = Form(None), is_paid: str | None = Form(None), is_active: str | None = Form(None), csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.leave_type.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
|
|
branch_id = request.session.get("active_branch_id") or current_user.branch_id
|
|
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
|
|
row = get_leave_type_or_404(db, leave_type_id, scope)
|
|
update_leave_type(db, current_user, row, {"name": name, "description": description, "annual_quota_days": annual_quota_days, "carry_forward_allowed": _form_bool(carry_forward_allowed), "allow_negative_balance": _form_bool(allow_negative_balance), "requires_approval": _form_bool(requires_approval), "is_paid": _form_bool(is_paid), "is_active": _form_bool(is_active)})
|
|
return RedirectResponse(url="/employees/leave-types", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/leave-balances")
|
|
def employee_leave_balances(request: Request, employee_id: int | None = None):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.leave_balance.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
rows = list_leave_balances(db, scope, employee_id=employee_id)
|
|
employees = list_employees(db, scope, include_inactive=True)
|
|
leave_types = list_leave_types(db, scope, include_inactive=False)
|
|
return _render(request, "modules/employees/templates/employees/leave_balances.html", db, current_user, title="Employee Leave Balances", rows=rows, employees=employees, leave_types=leave_types, selected_employee_id=employee_id, scope=scope)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/leave-balances/adjust")
|
|
def employee_leave_balance_adjust(request: Request, employee_id: int = Form(...), leave_type_id: int = Form(...), adjusted_days: int = Form(0), notes: str | None = Form(None), csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.leave_balance.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
adjust_leave_balance(db, current_user, scope, employee_id=employee_id, leave_type_id=leave_type_id, adjusted_days=adjusted_days, notes=notes)
|
|
return RedirectResponse(url=f"/employees/leave-balances?employee_id={employee_id}", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/leave")
|
|
def employee_leave_requests(request: Request, employee_id: int | None = None, status: str = "pending"):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.leave.view_all")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
rows = list_leave_requests(db, scope, employee_id=employee_id, status=status if status != "all" else None)
|
|
employees = list_employees(db, scope, include_inactive=True)
|
|
return _render(request, "modules/employees/templates/employees/leave_requests.html", db, current_user, title="Employee Leave Requests", rows=rows, employees=employees, status=status, selected_employee_id=employee_id, statuses=LEAVE_REQUEST_STATUS, scope=scope)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/leave/{request_id}/review")
|
|
def employee_leave_review_submit(request: Request, request_id: int, status: str = Form(...), review_notes: str | None = Form(None), csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.leave.approve")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
row = get_leave_request_or_404(db, request_id, scope)
|
|
review_leave_request(db, current_user, row, status=status, review_notes=review_notes)
|
|
return RedirectResponse(url="/employees/leave", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/registration-requests")
|
|
def employee_registration_requests(request: Request, status: str = "pending"):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.registration.approve")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
rows = list_employee_registration_requests(db, scope, status=status if status != "all" else None)
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/registration_requests.html",
|
|
db,
|
|
current_user,
|
|
title="Employee Registration Requests",
|
|
rows=rows,
|
|
status=status,
|
|
statuses=REGISTRATION_STATUS,
|
|
scope=scope,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/registration-requests/{request_id}")
|
|
def employee_registration_request_detail(request: Request, request_id: int):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.registration.approve")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
req = get_employee_registration_request_or_404(db, request_id, scope)
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/registration_request_detail.html",
|
|
db,
|
|
current_user,
|
|
title=f"Employee Registration #{req.id}",
|
|
item=req,
|
|
error=None,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/registration-requests/{request_id}/approve")
|
|
def employee_registration_request_approve(
|
|
request: Request,
|
|
request_id: int,
|
|
employee_code: str | None = Form(None),
|
|
review_notes: str | None = Form(None),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.registration.approve")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
req = get_employee_registration_request_or_404(db, request_id, scope)
|
|
emp = approve_employee_registration_request(db, current_user, req, employee_code=employee_code, notes=review_notes)
|
|
return RedirectResponse(url=f"/employees/{emp.id}", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/registration-requests/{request_id}/reject")
|
|
def employee_registration_request_reject(
|
|
request: Request,
|
|
request_id: int,
|
|
review_notes: str | None = Form(None),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.registration.approve")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
req = get_employee_registration_request_or_404(db, request_id, scope)
|
|
reject_employee_registration_request(db, current_user, req, notes=review_notes)
|
|
return RedirectResponse(url="/employees/registration-requests", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/onboarding-checklist")
|
|
def employee_onboarding_checklist(request: Request):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.onboarding.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
|
|
branch_id = request.session.get("active_branch_id") or current_user.branch_id
|
|
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
|
|
return _render(request, "modules/employees/templates/employees/onboarding_checklist.html", db, current_user, title="Onboarding Checklist", rows=list_onboarding_checklist_items(db, scope), errors=[], **_form_options(db, current_user, scope))
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/onboarding-checklist/defaults")
|
|
def employee_onboarding_checklist_defaults(request: Request, csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.onboarding.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
|
|
branch_id = request.session.get("active_branch_id") or current_user.branch_id
|
|
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
|
|
ensure_default_onboarding_checklist(db, current_user, scope)
|
|
return RedirectResponse(url="/employees/onboarding-checklist", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/onboarding-checklist")
|
|
async def employee_onboarding_checklist_create(request: Request):
|
|
form = await request.form()
|
|
validate_csrf(request, form.get("csrf_token"))
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.onboarding.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
|
|
branch_id = request.session.get("active_branch_id") or current_user.branch_id
|
|
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
|
|
create_onboarding_checklist_item(db, current_user, scope, _onboarding_item_payload(form))
|
|
return RedirectResponse(url="/employees/onboarding-checklist", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/onboarding-checklist/{item_id}/edit")
|
|
async def employee_onboarding_checklist_edit(request: Request, item_id: int):
|
|
form = await request.form()
|
|
validate_csrf(request, form.get("csrf_token"))
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.onboarding.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
tenant_id = request.session.get("active_tenant_id") or current_user.tenant_id
|
|
branch_id = request.session.get("active_branch_id") or current_user.branch_id
|
|
scope = build_employee_scope(db, current_user, tenant_id=tenant_id, branch_id=branch_id)
|
|
item = get_onboarding_checklist_item_or_404(db, item_id, scope)
|
|
update_onboarding_checklist_item(db, current_user, item, _onboarding_item_payload(form))
|
|
return RedirectResponse(url="/employees/onboarding-checklist", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/onboarding")
|
|
def employee_onboarding_tasks(request: Request, employee_id: int | None = None):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.onboarding.view")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
return _render(request, "modules/employees/templates/employees/onboarding_tasks.html", db, current_user, title="Employee Onboarding", rows=list_onboarding_tasks(db, scope, employee_id=employee_id), employees=list_employees(db, scope, include_inactive=True), selected_employee_id=employee_id, errors=[])
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/{employee_id}/onboarding/generate")
|
|
def employee_generate_onboarding(request: Request, employee_id: int, csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.onboarding.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
employee = get_employee_or_404(db, employee_id, scope)
|
|
generate_onboarding_tasks_for_employee(db, current_user, employee, scope)
|
|
return RedirectResponse(url=f"/employees/onboarding?employee_id={employee_id}", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/onboarding/{task_id}/status")
|
|
def employee_onboarding_task_status(request: Request, task_id: int, status: str = Form(...), review_notes: str | None = Form(None), csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.onboarding.approve")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
task = get_onboarding_task_or_404(db, task_id, scope)
|
|
update_onboarding_task_status(db, current_user, task, status, review_notes)
|
|
return RedirectResponse(url=f"/employees/onboarding?employee_id={task.employee_id}", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/offboarding")
|
|
def employee_offboarding_requests(request: Request, status: str | None = None):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.offboarding.view")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
rows = list_offboarding_requests(db, scope, status=status if status else None)
|
|
return _render(request, "modules/employees/templates/employees/offboarding_requests.html", db, current_user, title="Employee Offboarding", rows=rows, employees=list_employees(db, scope, include_inactive=True), selected_status=status, errors=[])
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/{employee_id}/offboarding/initiate")
|
|
async def employee_offboarding_initiate(request: Request, employee_id: int):
|
|
form = await request.form()
|
|
validate_csrf(request, form.get("csrf_token"))
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.offboarding.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
employee = get_employee_or_404(db, employee_id, scope)
|
|
create_offboarding_request(db, current_user, employee, _offboarding_payload(form), source="admin")
|
|
return RedirectResponse(url="/employees/offboarding", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/offboarding/{request_id}/review")
|
|
def employee_offboarding_review(request: Request, request_id: int, status: str = Form(...), approved_relieving_date: str | None = Form(None), review_notes: str | None = Form(None), csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.offboarding.approve")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
req = get_offboarding_request_or_404(db, request_id, scope)
|
|
review_offboarding_request(db, current_user, req, status=status, approved_relieving_date=parse_date(approved_relieving_date), review_notes=review_notes)
|
|
return RedirectResponse(url="/employees/offboarding", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/offboarding/tasks/{task_id}/status")
|
|
def employee_offboarding_task_status(request: Request, task_id: int, status: str = Form(...), review_notes: str | None = Form(None), csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.offboarding.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
task = get_offboarding_task_or_404(db, task_id, scope)
|
|
update_offboarding_task_status(db, current_user, task, status, review_notes)
|
|
return RedirectResponse(url="/employees/offboarding", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/offboarding/{request_id}/complete")
|
|
def employee_offboarding_complete(request: Request, request_id: int, csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.offboarding.approve")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
req = get_offboarding_request_or_404(db, request_id, scope)
|
|
complete_offboarding_request(db, current_user, req)
|
|
return RedirectResponse(url="/employees/offboarding", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/document-types")
|
|
def employee_document_types(request: Request, include_inactive: str | None = None):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.document_type.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
rows = list_document_types(db, scope, include_inactive=bool(include_inactive))
|
|
return _render(request, "modules/employees/templates/employees/document_types.html", db, current_user, title="Employee Document Types", rows=rows, scope=scope, include_inactive=bool(include_inactive), editing=None, errors=[])
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/document-types/defaults")
|
|
def employee_document_types_defaults(request: Request, csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.document_type.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
ensure_default_document_types(db, current_user, scope)
|
|
return RedirectResponse(url="/employees/document-types", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/document-types")
|
|
async def employee_document_type_create(request: Request):
|
|
form = await request.form()
|
|
validate_csrf(request, form.get("csrf_token"))
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.document_type.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
try:
|
|
create_document_type(db, current_user, scope, _document_type_payload(form))
|
|
return RedirectResponse(url="/employees/document-types", status_code=303)
|
|
except Exception as exc:
|
|
return _render(request, "modules/employees/templates/employees/document_types.html", db, current_user, title="Employee Document Types", rows=list_document_types(db, scope, include_inactive=True), scope=scope, include_inactive=True, editing=None, errors=[getattr(exc, "detail", str(exc))])
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/document-types/{type_id}/edit")
|
|
async def employee_document_type_update(request: Request, type_id: int):
|
|
form = await request.form()
|
|
validate_csrf(request, form.get("csrf_token"))
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.document_type.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
row = get_document_type_or_404(db, type_id, scope)
|
|
update_document_type(db, current_user, row, _document_type_payload(form))
|
|
return RedirectResponse(url="/employees/document-types", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/documents")
|
|
def employee_documents(request: Request, employee_id: int | None = None, status: str | None = None):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.documents.view_all")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
return _render(request, "modules/employees/templates/employees/documents.html", db, current_user, title="Employee Documents", rows=list_employee_documents(db, scope, employee_id=employee_id, status=status), employees=list_employees(db, scope, include_inactive=True), document_types=list_document_types(db, scope), scope=scope, selected_employee_id=employee_id, selected_status=status, errors=[])
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/{employee_id}/documents/upload")
|
|
async def employee_document_upload(request: Request, employee_id: int, document_file: UploadFile = File(...)):
|
|
form = await request.form()
|
|
validate_csrf(request, form.get("csrf_token"))
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.documents.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
employee = get_employee_or_404(db, employee_id, scope)
|
|
try:
|
|
original, stored, path, size, content_type = await _save_employee_upload(document_file, tenant_id=employee.tenant_id, employee_id=employee.id)
|
|
create_employee_document_record(
|
|
db,
|
|
current_user,
|
|
employee,
|
|
document_type_id=int(form.get("document_type_id")) if form.get("document_type_id") not in (None, "", "None") else None,
|
|
title=form.get("title") or original,
|
|
document_no=form.get("document_no"),
|
|
issue_date=parse_date(form.get("issue_date")),
|
|
expiry_date=parse_date(form.get("expiry_date")),
|
|
original_filename=original,
|
|
stored_filename=stored,
|
|
storage_path=path,
|
|
content_type=content_type,
|
|
file_size_bytes=size,
|
|
remarks=form.get("remarks"),
|
|
visibility=form.get("visibility") or "employee_and_hr",
|
|
)
|
|
return RedirectResponse(url=f"/employees/documents?employee_id={employee.id}", status_code=303)
|
|
except Exception as exc:
|
|
return _render(request, "modules/employees/templates/employees/documents.html", db, current_user, title="Employee Documents", rows=list_employee_documents(db, scope, employee_id=employee.id), employees=list_employees(db, scope, include_inactive=True), document_types=list_document_types(db, scope), scope=scope, selected_employee_id=employee.id, selected_status=None, errors=[getattr(exc, "detail", str(exc))])
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/documents/{document_id}/review")
|
|
def employee_document_review(request: Request, document_id: int, status: str = Form(...), verification_notes: str | None = Form(None), csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.documents.verify")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
row = get_employee_document_or_404(db, document_id, scope)
|
|
review_employee_document(db, current_user, row, status=status, verification_notes=verification_notes)
|
|
return RedirectResponse(url="/employees/documents", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/documents/{document_id}/archive")
|
|
def employee_document_archive(request: Request, document_id: int, notes: str | None = Form(None), csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.documents.delete")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
row = get_employee_document_or_404(db, document_id, scope)
|
|
archive_employee_document(db, current_user, row, notes=notes)
|
|
return RedirectResponse(url="/employees/documents", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/documents")
|
|
def employee_self_documents(request: Request):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.documents.view_self")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
employee = get_employee_for_user(db, current_user)
|
|
document_types = []
|
|
if employee:
|
|
scope = build_employee_scope(db, current_user, tenant_id=employee.tenant_id, branch_id=employee.branch_id)
|
|
document_types = [dt for dt in list_document_types(db, scope) if dt.allow_employee_upload]
|
|
return _render(request, "modules/employees/templates/employees/self_documents.html", db, current_user, title="My Documents", employee=employee, rows=list_own_employee_documents(db, current_user), document_types=document_types, errors=[])
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.post("/documents/upload")
|
|
async def employee_self_document_upload(request: Request, document_file: UploadFile = File(...)):
|
|
form = await request.form()
|
|
validate_csrf(request, form.get("csrf_token"))
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.documents.upload_self")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
employee = get_employee_for_user(db, current_user)
|
|
if not employee:
|
|
return RedirectResponse(url="/employee/register", status_code=303)
|
|
try:
|
|
original, stored, path, size, content_type = await _save_employee_upload(document_file, tenant_id=employee.tenant_id, employee_id=employee.id)
|
|
create_employee_document_record(
|
|
db,
|
|
current_user,
|
|
employee,
|
|
document_type_id=int(form.get("document_type_id")) if form.get("document_type_id") not in (None, "", "None") else None,
|
|
title=form.get("title") or original,
|
|
document_no=form.get("document_no"),
|
|
issue_date=parse_date(form.get("issue_date")),
|
|
expiry_date=parse_date(form.get("expiry_date")),
|
|
original_filename=original,
|
|
stored_filename=stored,
|
|
storage_path=path,
|
|
content_type=content_type,
|
|
file_size_bytes=size,
|
|
remarks=form.get("remarks"),
|
|
visibility="employee_and_hr",
|
|
)
|
|
return RedirectResponse(url="/employee/documents", status_code=303)
|
|
except Exception as exc:
|
|
scope = build_employee_scope(db, current_user, tenant_id=employee.tenant_id, branch_id=employee.branch_id)
|
|
return _render(request, "modules/employees/templates/employees/self_documents.html", db, current_user, title="My Documents", employee=employee, rows=list_own_employee_documents(db, current_user), document_types=[dt for dt in list_document_types(db, scope) if dt.allow_employee_upload], errors=[getattr(exc, "detail", str(exc))])
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
|
|
@router.get("/payroll/structures")
|
|
def employee_salary_structures(request: Request, employee_id: int | None = None):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.payroll.structure.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/payroll_structures.html",
|
|
db,
|
|
current_user,
|
|
title="Employee Salary Structures",
|
|
rows=list_salary_structures(db, scope, employee_id=employee_id),
|
|
employees=list_employees(db, scope, include_inactive=True),
|
|
selected_employee_id=employee_id,
|
|
scope=scope,
|
|
errors=[],
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/payroll/structures")
|
|
async def employee_salary_structure_submit(request: Request):
|
|
form = await request.form()
|
|
validate_csrf(request, form.get("csrf_token"))
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.payroll.structure.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
payload = dict(form)
|
|
payload["is_active"] = _form_bool(form.get("is_active"))
|
|
try:
|
|
create_salary_structure(db, current_user, scope, payload)
|
|
return RedirectResponse(url="/employees/payroll/structures", status_code=303)
|
|
except Exception as exc:
|
|
return _render(request, "modules/employees/templates/employees/payroll_structures.html", db, current_user, title="Employee Salary Structures", rows=list_salary_structures(db, scope), employees=list_employees(db, scope, include_inactive=True), selected_employee_id=None, scope=scope, errors=[getattr(exc, "detail", str(exc))], form=payload)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/payroll/structures/{structure_id}/update")
|
|
async def employee_salary_structure_update(request: Request, structure_id: int):
|
|
form = await request.form()
|
|
validate_csrf(request, form.get("csrf_token"))
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.payroll.structure.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
row = get_salary_structure_or_404(db, structure_id, scope)
|
|
payload = dict(form)
|
|
payload["is_active"] = _form_bool(form.get("is_active"))
|
|
update_salary_structure(db, current_user, row, payload)
|
|
return RedirectResponse(url="/employees/payroll/structures", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/payroll/runs")
|
|
def employee_payroll_runs(request: Request):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.payroll.run")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
return _render(request, "modules/employees/templates/employees/payroll_runs.html", db, current_user, title="Payroll Runs", rows=list_payroll_runs(db, scope), scope=scope, errors=[])
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/payroll/runs")
|
|
def employee_payroll_run_create(request: Request, pay_year: int = Form(...), pay_month: int = Form(...), notes: str | None = Form(None), csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.payroll.run")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
create_payroll_run(db, current_user, scope, pay_year=pay_year, pay_month=pay_month, notes=notes)
|
|
return RedirectResponse(url="/employees/payroll/runs", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/payroll/runs/{run_id}/generate")
|
|
def employee_payroll_run_generate(request: Request, run_id: int, csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.payroll.run")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
run = get_payroll_run_or_404(db, run_id, scope)
|
|
generate_payslips_for_run(db, current_user, scope, run)
|
|
return RedirectResponse(url=f"/employees/payroll/payslips?payroll_run_id={run_id}", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/payroll/runs/{run_id}/approve")
|
|
def employee_payroll_run_approve(request: Request, run_id: int, csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.payroll.payout")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
run = get_payroll_run_or_404(db, run_id, scope)
|
|
approve_payroll_run(db, current_user, run)
|
|
return RedirectResponse(url="/employees/payroll/runs", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/payroll/runs/{run_id}/paid")
|
|
def employee_payroll_run_paid(request: Request, run_id: int, csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.payroll.payout")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
run = get_payroll_run_or_404(db, run_id, scope)
|
|
mark_payroll_run_paid(db, current_user, run)
|
|
return RedirectResponse(url="/employees/payroll/runs", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/payroll/payslips")
|
|
def employee_payslips(request: Request, payroll_run_id: int | None = None, employee_id: int | None = None):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.payroll.view")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
return _render(request, "modules/employees/templates/employees/payslips.html", db, current_user, title="Employee Payslips", rows=list_payslips(db, scope, payroll_run_id=payroll_run_id, employee_id=employee_id), employees=list_employees(db, scope, include_inactive=True), runs=list_payroll_runs(db, scope), selected_run_id=payroll_run_id, selected_employee_id=employee_id, scope=scope, errors=[])
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/progress")
|
|
def employee_engagement_progress_dashboard(
|
|
request: Request,
|
|
q: str = "",
|
|
status: str = "open",
|
|
assigned_to_user_id: int = 0,
|
|
):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.progress.view")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
assignee_id = int(assigned_to_user_id or 0) or None
|
|
financial_year = _active_financial_year(request)
|
|
progress_payload = list_engagement_progress_dashboard(db, scope, q=q, status=status, assigned_to_user_id=assignee_id, financial_year=financial_year)
|
|
assignable_users = list_employee_work_assignable_users(db, scope)
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/progress_dashboard.html",
|
|
db,
|
|
current_user,
|
|
progress_payload=progress_payload,
|
|
assignable_users=assignable_users,
|
|
q=q,
|
|
status=status,
|
|
selected_assigned_to_user_id=assignee_id,
|
|
page_title="Engagement Progress",
|
|
financial_year=financial_year,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/work")
|
|
def employee_work_assignment_dashboard(
|
|
request: Request,
|
|
q: str = "",
|
|
status: str = "open",
|
|
assigned_to_user_id: int = 0,
|
|
):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.work.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
assignee_id = int(assigned_to_user_id or 0) or None
|
|
financial_year = _active_financial_year(request)
|
|
work_payload = list_visible_work_assignment_dashboard(db, scope, q=q, status=status, assigned_to_user_id=assignee_id, financial_year=financial_year)
|
|
assignable_users = list_employee_work_assignable_users(db, scope)
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/work_dashboard.html",
|
|
db,
|
|
current_user,
|
|
title="Employee Work Allocation",
|
|
work_payload=work_payload,
|
|
assignable_users=assignable_users,
|
|
q=q,
|
|
status=status,
|
|
selected_assigned_to_user_id=assignee_id,
|
|
scope=scope,
|
|
errors=[],
|
|
financial_year=financial_year,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/work/tasks/{task_id}/assign")
|
|
def employee_work_task_assign(
|
|
request: Request,
|
|
task_id: int,
|
|
assigned_to_user_id: str = Form(""),
|
|
status: str = Form("pending"),
|
|
priority: str = Form("normal"),
|
|
internal_target_date: str = Form(""),
|
|
remarks: str = Form(""),
|
|
return_url: str = Form(""),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.work.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
validate_csrf(request, csrf_token)
|
|
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=tenant_id, branch_id=branch_id)
|
|
update_service_task_assignment(
|
|
db,
|
|
scope,
|
|
task_id,
|
|
assigned_to_user_id=int(assigned_to_user_id) if assigned_to_user_id else None,
|
|
status=status,
|
|
priority=priority,
|
|
internal_target_date=parse_date(internal_target_date),
|
|
remarks=remarks,
|
|
actor_user_id=current_user.id,
|
|
financial_year=_active_financial_year(request),
|
|
)
|
|
safe_return_url = return_url if return_url.startswith(("/employees/work", "/manager/work", "/manager/dashboard")) else "/employees/work"
|
|
return RedirectResponse(url=safe_return_url, status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.post("/work/tasks/{task_id}/status")
|
|
def employee_my_work_task_status(
|
|
request: Request,
|
|
task_id: int,
|
|
status: str = Form(...),
|
|
remarks: str = Form(""),
|
|
return_url: str = Form(""),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.work.view_self")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
validate_csrf(request, csrf_token)
|
|
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=tenant_id, branch_id=branch_id)
|
|
update_own_service_task_status(
|
|
db,
|
|
scope,
|
|
task_id,
|
|
status=status,
|
|
remarks=remarks,
|
|
actor_user_id=current_user.id,
|
|
financial_year=_active_financial_year(request),
|
|
)
|
|
safe_return_url = return_url if return_url.startswith("/employee/work") else "/employee/work"
|
|
return RedirectResponse(url=safe_return_url, status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/work")
|
|
def employee_my_work(request: Request, q: str = "", status: str = "open"):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.work.view_self")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
employee = get_employee_for_user(db, current_user)
|
|
financial_year = _active_financial_year(request)
|
|
work_payload = list_employee_work_kanban(db, scope, q=q, status=status, financial_year=financial_year)
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/self_work.html",
|
|
db,
|
|
current_user,
|
|
title="My Work",
|
|
employee=employee,
|
|
work_payload=work_payload,
|
|
q=q,
|
|
status=status,
|
|
errors=[],
|
|
financial_year=financial_year,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/work/engagements/{engagement_id}")
|
|
def employee_my_work_engagement_board(request: Request, engagement_id: int):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.work.view_self")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
employee = get_employee_for_user(db, current_user)
|
|
financial_year = _active_financial_year(request)
|
|
board = get_employee_engagement_work_board(db, scope, engagement_id, financial_year=financial_year)
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/work_engagement_board.html",
|
|
db,
|
|
current_user,
|
|
title="Engagement Work Board",
|
|
employee=employee,
|
|
board=board,
|
|
errors=[],
|
|
financial_year=financial_year,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/work/tasks/{task_id}/communication")
|
|
def employee_work_task_communication(
|
|
request: Request,
|
|
task_id: int,
|
|
):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.work.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
financial_year = _active_financial_year(request)
|
|
task = get_work_task_with_communications(db, scope, task_id, financial_year=financial_year)
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/work_task_communication.html",
|
|
db,
|
|
current_user,
|
|
title="Task Communication",
|
|
task=task,
|
|
communication_types=TASK_COMMUNICATION_TYPES,
|
|
communication_visibilities=TASK_COMMUNICATION_VISIBILITIES,
|
|
post_url=f"/employees/work/tasks/{task_id}/communication",
|
|
back_url="/employees/work",
|
|
is_employee_self=False,
|
|
errors=[],
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/work/tasks/{task_id}/communication")
|
|
def employee_work_task_add_communication(
|
|
request: Request,
|
|
task_id: int,
|
|
comment_type: str = Form("internal_note"),
|
|
visibility: str = Form("internal"),
|
|
message: str = Form(...),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.work.manage")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
validate_csrf(request, csrf_token)
|
|
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=tenant_id, branch_id=branch_id)
|
|
add_work_task_communication(
|
|
db,
|
|
scope,
|
|
task_id,
|
|
comment_type=comment_type,
|
|
visibility=visibility,
|
|
message=message,
|
|
actor_user_id=current_user.id,
|
|
financial_year=_active_financial_year(request),
|
|
)
|
|
return RedirectResponse(url=f"/employees/work/tasks/{task_id}/communication", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/work/tasks/{task_id}/communication")
|
|
def employee_my_work_task_communication(
|
|
request: Request,
|
|
task_id: int,
|
|
):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.work.view_self")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
financial_year = _active_financial_year(request)
|
|
task = get_work_task_with_communications(db, scope, task_id, assigned_only=True, financial_year=financial_year)
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/work_task_communication.html",
|
|
db,
|
|
current_user,
|
|
title="My Task Communication",
|
|
task=task,
|
|
communication_types=TASK_COMMUNICATION_TYPES,
|
|
communication_visibilities=TASK_COMMUNICATION_VISIBILITIES,
|
|
post_url=f"/employee/work/tasks/{task_id}/communication",
|
|
back_url="/employee/work",
|
|
is_employee_self=True,
|
|
errors=[],
|
|
financial_year=financial_year,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.post("/work/tasks/{task_id}/communication")
|
|
def employee_my_work_task_add_communication(
|
|
request: Request,
|
|
task_id: int,
|
|
comment_type: str = Form("internal_note"),
|
|
visibility: str = Form("internal"),
|
|
message: str = Form(...),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.work.view_self")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
validate_csrf(request, csrf_token)
|
|
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=tenant_id, branch_id=branch_id)
|
|
add_work_task_communication(
|
|
db,
|
|
scope,
|
|
task_id,
|
|
comment_type=comment_type,
|
|
visibility=visibility,
|
|
message=message,
|
|
actor_user_id=current_user.id,
|
|
assigned_only=True,
|
|
financial_year=_active_financial_year(request),
|
|
)
|
|
return RedirectResponse(url=f"/employee/work/tasks/{task_id}/communication", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/payslips")
|
|
def employee_self_payslips(request: Request):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.payroll.view_self")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
employee = get_employee_for_user(db, current_user)
|
|
return _render(request, "modules/employees/templates/employees/self_payslips.html", db, current_user, title="My Payslips", employee=employee, rows=list_own_payslips(db, current_user), errors=[])
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
# -------------------------
|
|
# Phase 6J HR Excel imports
|
|
# -------------------------
|
|
|
|
@router.get("/imports")
|
|
def employee_imports_page(request: Request, message: str | None = None):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.import")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/imports.html",
|
|
db,
|
|
current_user,
|
|
title="HR Excel Imports",
|
|
import_types=supported_import_types(),
|
|
message=message,
|
|
errors=[],
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/imports/template/{import_type}")
|
|
def employee_import_template(request: Request, import_type: str):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.import")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
content = build_template_workbook(import_type)
|
|
filename = f"hr_{import_type}_template.xlsx"
|
|
return StreamingResponse(
|
|
BytesIO(content),
|
|
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/imports/preview")
|
|
async def employee_import_preview(request: Request, import_type: str = Form(...), import_file: UploadFile = File(...)):
|
|
form = await request.form()
|
|
validate_csrf(request, form.get("csrf_token"))
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.import")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
try:
|
|
content = await import_file.read()
|
|
preview = preview_import(db, scope, import_type, content)
|
|
request.session["employee_import_preview"] = preview
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/import_preview.html",
|
|
db,
|
|
current_user,
|
|
title="HR Import Preview",
|
|
preview=preview,
|
|
errors=[],
|
|
)
|
|
except Exception as exc:
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/imports.html",
|
|
db,
|
|
current_user,
|
|
title="HR Excel Imports",
|
|
import_types=supported_import_types(),
|
|
message=None,
|
|
errors=[getattr(exc, "detail", str(exc))],
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/imports/commit")
|
|
def employee_import_commit(request: Request, csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.import")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
preview = request.session.get("employee_import_preview")
|
|
if not preview:
|
|
return _render(request, "modules/employees/templates/employees/imports.html", db, current_user, title="HR Excel Imports", import_types=supported_import_types(), message=None, errors=["No pending import preview found. Please upload and preview again."])
|
|
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=tenant_id, branch_id=branch_id)
|
|
result = commit_import(db, current_user, scope, preview)
|
|
request.session.pop("employee_import_preview", None)
|
|
message = f"Import completed. Created: {result['created']}, Updated: {result['updated']}, Skipped: {result['skipped']}, Failed: {result['failed']}."
|
|
errors = result.get("errors") or []
|
|
return _render(request, "modules/employees/templates/employees/imports.html", db, current_user, title="HR Excel Imports", import_types=supported_import_types(), message=message, errors=errors)
|
|
finally:
|
|
db.close()
|
|
|
|
@router.get("/{employee_id}")
|
|
def employee_detail(request: Request, employee_id: int, link_error: str | None = None):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.view")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
try:
|
|
employee = get_employee_or_404(db, employee_id, scope)
|
|
except Exception:
|
|
return _redirect_denied()
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/detail.html",
|
|
db,
|
|
current_user,
|
|
title=f"Employee - {employee.full_name}",
|
|
employee=employee,
|
|
linkable_users=list_linkable_users(db, scope, include_user_id=employee.user_id),
|
|
link_error=bool(link_error),
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/{employee_id}/edit")
|
|
def employee_edit(request: Request, employee_id: int):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.edit")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
try:
|
|
employee = get_employee_or_404(db, employee_id, scope)
|
|
except Exception:
|
|
return _redirect_denied()
|
|
form_scope = build_employee_scope(db, current_user, tenant_id=employee.tenant_id, branch_id=employee.branch_id)
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/form.html",
|
|
db,
|
|
current_user,
|
|
title="Edit Employee",
|
|
employee=employee,
|
|
errors=[],
|
|
mode="edit",
|
|
**_form_options(db, current_user, form_scope, include_user_id=employee.user_id),
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/{employee_id}/edit")
|
|
async def employee_edit_submit(request: Request, employee_id: int):
|
|
form = await request.form()
|
|
validate_csrf(request, form.get("csrf_token"))
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.edit")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
employee = get_employee_or_404(db, employee_id, scope)
|
|
form_scope = build_employee_scope(db, current_user, tenant_id=employee.tenant_id, branch_id=employee.branch_id)
|
|
payload = _form_payload(form)
|
|
try:
|
|
employee = update_employee(db, current_user, employee, payload)
|
|
return RedirectResponse(url=f"/employees/{employee.id}", status_code=303)
|
|
except Exception as exc:
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/form.html",
|
|
db,
|
|
current_user,
|
|
title="Edit Employee",
|
|
employee=payload | {"id": employee.id, "tenant_id": employee.tenant_id, "branch_id": employee.branch_id},
|
|
errors=[getattr(exc, "detail", str(exc))],
|
|
mode="edit",
|
|
**_form_options(db, current_user, form_scope, include_user_id=employee.user_id),
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/{employee_id}/link-user")
|
|
def employee_link_user_submit(request: Request, employee_id: int, user_id: str | None = Form(None), csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.edit")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
employee = get_employee_or_404(db, employee_id, scope)
|
|
try:
|
|
resolved_user_id = int(user_id) if user_id not in (None, "", "None") else None
|
|
link_employee_to_user(db, current_user, employee, resolved_user_id)
|
|
return RedirectResponse(url=f"/employees/{employee.id}", status_code=303)
|
|
except Exception:
|
|
return RedirectResponse(url=f"/employees/{employee.id}?link_error=1", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/{employee_id}/status")
|
|
def employee_status_submit(
|
|
request: Request,
|
|
employee_id: int,
|
|
status: str = Form(...),
|
|
date_of_leaving: str | None = Form(None),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.status")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
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=tenant_id, branch_id=branch_id)
|
|
employee = get_employee_or_404(db, employee_id, scope)
|
|
change_employee_status(db, current_user, employee, status, parse_date(date_of_leaving))
|
|
return RedirectResponse(url=f"/employees/{employee_id}", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/dashboard")
|
|
def employee_portal_dashboard(request: Request):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.ess.view")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
employee = get_employee_for_user(db, current_user)
|
|
pending_request = None if employee else get_pending_registration_for_user(db, current_user)
|
|
work_payload = None
|
|
if employee:
|
|
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"))
|
|
work_payload = list_employee_work_kanban(db, scope, q="", status="open", financial_year=_active_financial_year(request))
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/portal_dashboard.html",
|
|
db,
|
|
current_user,
|
|
title="Employee Portal",
|
|
employee=employee,
|
|
pending_request=pending_request,
|
|
today_attendance=get_today_attendance_for_user(db, current_user),
|
|
leave_balances=list_own_leave_balances(db, current_user),
|
|
leave_requests=list_own_leave_requests(db, current_user)[:5],
|
|
offboarding_requests=list_own_offboarding_requests(db, current_user)[:5],
|
|
payslips=list_own_payslips(db, current_user)[:5],
|
|
work_payload=work_payload,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/attendance")
|
|
def employee_self_attendance(request: Request):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.attendance.view_self")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
employee = get_employee_for_user(db, current_user)
|
|
rows = list_own_attendance(db, current_user)
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/self_attendance.html",
|
|
db,
|
|
current_user,
|
|
title="My Attendance",
|
|
employee=employee,
|
|
rows=rows,
|
|
today_attendance=get_today_attendance_for_user(db, current_user),
|
|
errors=[],
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.post("/attendance/punch-in")
|
|
def employee_punch_in_submit(
|
|
request: Request,
|
|
remarks: str | None = Form(None),
|
|
latitude: str | None = Form(None),
|
|
longitude: str | None = Form(None),
|
|
accuracy_meters: str | None = Form(None),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.attendance.punch")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
punch_in_attendance(
|
|
db,
|
|
current_user,
|
|
remarks=remarks,
|
|
latitude=latitude,
|
|
longitude=longitude,
|
|
accuracy_meters=accuracy_meters,
|
|
client_ip=_client_ip(request),
|
|
)
|
|
return RedirectResponse(url="/employee/attendance", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.post("/attendance/punch-out")
|
|
def employee_punch_out_submit(
|
|
request: Request,
|
|
remarks: str | None = Form(None),
|
|
latitude: str | None = Form(None),
|
|
longitude: str | None = Form(None),
|
|
accuracy_meters: str | None = Form(None),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.attendance.punch")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
punch_out_attendance(
|
|
db,
|
|
current_user,
|
|
remarks=remarks,
|
|
latitude=latitude,
|
|
longitude=longitude,
|
|
accuracy_meters=accuracy_meters,
|
|
client_ip=_client_ip(request),
|
|
)
|
|
return RedirectResponse(url="/employee/attendance", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/leave")
|
|
def employee_self_leave(request: Request):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.leave.view_self")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
employee = get_employee_for_user(db, current_user)
|
|
leave_types = []
|
|
if employee:
|
|
scope = build_employee_scope(db, current_user, tenant_id=employee.tenant_id, branch_id=employee.branch_id)
|
|
leave_types = list_leave_types(db, scope, include_inactive=False)
|
|
return _render(request, "modules/employees/templates/employees/self_leave.html", db, current_user, title="My Leave", employee=employee, rows=list_own_leave_requests(db, current_user), balances=list_own_leave_balances(db, current_user), leave_types=leave_types, errors=[])
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.post("/leave/apply")
|
|
def employee_self_leave_apply(request: Request, leave_type_id: int = Form(...), from_date: str = Form(...), to_date: str = Form(...), reason: str | None = Form(None), csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.leave.apply")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
apply_employee_leave(db, current_user, leave_type_id=leave_type_id, from_date=parse_date(from_date), to_date=parse_date(to_date), reason=reason)
|
|
return RedirectResponse(url="/employee/leave", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.post("/leave/{request_id}/cancel")
|
|
def employee_self_leave_cancel(request: Request, request_id: int, csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.leave.apply")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
cancel_own_leave_request(db, current_user, request_id)
|
|
return RedirectResponse(url="/employee/leave", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
|
|
|
|
@portal_router.get("/offboarding")
|
|
def employee_self_offboarding(request: Request):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.offboarding.request_self")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
employee = get_employee_for_user(db, current_user)
|
|
return _render(request, "modules/employees/templates/employees/self_offboarding.html", db, current_user, title="My Offboarding", employee=employee, rows=list_own_offboarding_requests(db, current_user), errors=[])
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.post("/offboarding/request")
|
|
async def employee_self_offboarding_request(request: Request):
|
|
form = await request.form()
|
|
validate_csrf(request, form.get("csrf_token"))
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.offboarding.request_self")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
employee = get_employee_for_user(db, current_user)
|
|
if not employee:
|
|
return RedirectResponse(url="/employee/register", status_code=303)
|
|
create_offboarding_request(db, current_user, employee, _offboarding_payload(form), source="employee")
|
|
return RedirectResponse(url="/employee/offboarding", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
|
|
|
|
@portal_router.get("/profile")
|
|
def employee_self_profile(request: Request):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.ess.view")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
employee = get_employee_for_user(db, current_user)
|
|
if not employee:
|
|
return RedirectResponse(url="/employee/register", status_code=303)
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/self_profile.html",
|
|
db,
|
|
current_user,
|
|
title="My Employee Profile",
|
|
employee=employee,
|
|
errors=[],
|
|
saved=False,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.post("/profile")
|
|
async def employee_self_profile_submit(request: Request):
|
|
form = await request.form()
|
|
validate_csrf(request, form.get("csrf_token"))
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.ess.profile.edit")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
employee = get_employee_for_user(db, current_user)
|
|
if not employee:
|
|
return RedirectResponse(url="/employee/register", status_code=303)
|
|
payload = {
|
|
"mobile": form.get("mobile"),
|
|
"alternate_mobile": form.get("alternate_mobile"),
|
|
"address": form.get("address"),
|
|
"emergency_contact_name": form.get("emergency_contact_name"),
|
|
"emergency_contact_mobile": form.get("emergency_contact_mobile"),
|
|
"bank_name": form.get("bank_name"),
|
|
"bank_account_no": form.get("bank_account_no"),
|
|
"bank_ifsc": form.get("bank_ifsc"),
|
|
}
|
|
try:
|
|
profile_photo_path = await save_user_profile_photo(current_user, form.get("profile_photo"))
|
|
update_user_public_profile(
|
|
db,
|
|
current_user,
|
|
qualification=form.get("qualification"),
|
|
designation=form.get("public_designation") or employee.designation,
|
|
mobile=form.get("mobile"),
|
|
bio=form.get("bio"),
|
|
profile_photo_path=profile_photo_path,
|
|
)
|
|
employee = update_own_employee_profile(db, current_user, employee, payload)
|
|
db.commit()
|
|
db.refresh(current_user)
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/self_profile.html",
|
|
db,
|
|
current_user,
|
|
title="My Employee Profile",
|
|
employee=employee,
|
|
errors=[],
|
|
saved=True,
|
|
)
|
|
except Exception as exc:
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/self_profile.html",
|
|
db,
|
|
current_user,
|
|
title="My Employee Profile",
|
|
employee=employee,
|
|
errors=[getattr(exc, "detail", str(exc))],
|
|
saved=False,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.get("/register")
|
|
def employee_registration_form(request: Request):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.registration.request")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
employee = get_employee_for_user(db, current_user)
|
|
pending_request = get_pending_registration_for_user(db, current_user)
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/registration_form.html",
|
|
db,
|
|
current_user,
|
|
title="Request Employee Profile",
|
|
employee=employee,
|
|
pending_request=pending_request,
|
|
errors=[],
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@portal_router.post("/register")
|
|
async def employee_registration_submit(request: Request):
|
|
form = await request.form()
|
|
validate_csrf(request, form.get("csrf_token"))
|
|
db = CommonSessionLocal()
|
|
try:
|
|
current_user = get_current_user(request, db=db)
|
|
if not current_user:
|
|
return _redirect_login()
|
|
try:
|
|
require_permission(db, current_user, "employees.registration.request")
|
|
except Exception:
|
|
return _redirect_denied()
|
|
payload = {
|
|
"requested_employee_code": form.get("requested_employee_code"),
|
|
"full_name": form.get("full_name"),
|
|
"email": form.get("email"),
|
|
"mobile": form.get("mobile"),
|
|
"department": form.get("department"),
|
|
"designation": form.get("designation"),
|
|
"date_of_joining": form.get("date_of_joining"),
|
|
"remarks": form.get("remarks"),
|
|
}
|
|
try:
|
|
create_employee_registration_request(db, current_user, payload)
|
|
return RedirectResponse(url="/employee/dashboard", status_code=303)
|
|
except Exception as exc:
|
|
return _render(
|
|
request,
|
|
"modules/employees/templates/employees/registration_form.html",
|
|
db,
|
|
current_user,
|
|
title="Request Employee Profile",
|
|
employee=get_employee_for_user(db, current_user),
|
|
pending_request=get_pending_registration_for_user(db, current_user),
|
|
errors=[getattr(exc, "detail", str(exc))],
|
|
form=payload,
|
|
)
|
|
finally:
|
|
db.close()
|