Allow staff email correction and sync linked login email
This commit is contained in:
@@ -13,6 +13,7 @@ from sqlalchemy.orm import Session, selectinload
|
|||||||
|
|
||||||
from app.core.security.passwords import hash_password
|
from app.core.security.passwords import hash_password
|
||||||
from app.modules.core.iam.models import User
|
from app.modules.core.iam.models import User
|
||||||
|
from app.modules.core.audit.service import write_audit_log
|
||||||
from app.modules.alerts.service import create_alert
|
from app.modules.alerts.service import create_alert
|
||||||
from app.modules.alerts.workflow_escalations import create_workflow_escalation
|
from app.modules.alerts.workflow_escalations import create_workflow_escalation
|
||||||
from app.modules.core.iam.scope import build_scope, list_visible_branches, list_visible_tenants, validate_branch_matches_tenant
|
from app.modules.core.iam.scope import build_scope, list_visible_branches, list_visible_tenants, validate_branch_matches_tenant
|
||||||
@@ -421,6 +422,7 @@ def update_employee(db: Session, actor: User, emp: Employee, data: dict[str, Any
|
|||||||
raise HTTPException(status_code=400, detail="Employee code and full name are required.")
|
raise HTTPException(status_code=400, detail="Employee code and full name are required.")
|
||||||
|
|
||||||
user_id = cleaned.get("user_id")
|
user_id = cleaned.get("user_id")
|
||||||
|
linked_user: User | None = None
|
||||||
if user_id:
|
if user_id:
|
||||||
linked_user = db.get(User, int(user_id))
|
linked_user = db.get(User, int(user_id))
|
||||||
if not linked_user:
|
if not linked_user:
|
||||||
@@ -433,6 +435,46 @@ def update_employee(db: Session, actor: User, emp: Employee, data: dict[str, Any
|
|||||||
|
|
||||||
_ensure_unique(db, tenant_id=emp.tenant_id, employee_code=employee_code, user_id=user_id, exclude_id=emp.id)
|
_ensure_unique(db, tenant_id=emp.tenant_id, employee_code=employee_code, user_id=user_id, exclude_id=emp.id)
|
||||||
|
|
||||||
|
old_email = (emp.email or "").strip().lower()
|
||||||
|
requested_email = (cleaned.get("email") or "").strip().lower()
|
||||||
|
email_changed = requested_email != old_email
|
||||||
|
|
||||||
|
if email_changed:
|
||||||
|
actor_roles = _role_set(db, actor)
|
||||||
|
if not actor_roles.intersection({"System Admin", "Firm Admin", "Partner"}):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="Only a Firm Admin or Partner can correct an employee email address.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if linked_user is not None and not requested_email:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Email cannot be blank while the employee is linked to a login user.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if requested_email:
|
||||||
|
duplicate_user_stmt = select(User).where(func.lower(User.email) == requested_email)
|
||||||
|
if linked_user is not None:
|
||||||
|
duplicate_user_stmt = duplicate_user_stmt.where(User.id != linked_user.id)
|
||||||
|
duplicate_user = db.execute(duplicate_user_stmt).scalar_one_or_none()
|
||||||
|
if duplicate_user:
|
||||||
|
raise HTTPException(status_code=409, detail="This email address is already used by another login user.")
|
||||||
|
|
||||||
|
duplicate_employee = db.execute(
|
||||||
|
select(Employee).where(
|
||||||
|
Employee.tenant_id == emp.tenant_id,
|
||||||
|
Employee.id != emp.id,
|
||||||
|
func.lower(Employee.email) == requested_email,
|
||||||
|
)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
if duplicate_employee:
|
||||||
|
raise HTTPException(status_code=409, detail="This email address is already used by another employee in this firm.")
|
||||||
|
|
||||||
|
cleaned["email"] = requested_email or None
|
||||||
|
if linked_user is not None:
|
||||||
|
linked_user.email = requested_email
|
||||||
|
|
||||||
update_fields = [
|
update_fields = [
|
||||||
"employee_code", "full_name", "email", "mobile", "alternate_mobile", "date_of_joining", "date_of_leaving",
|
"employee_code", "full_name", "email", "mobile", "alternate_mobile", "date_of_joining", "date_of_leaving",
|
||||||
"employment_type", "status", "is_active", "department", "designation", "reporting_manager_user_id",
|
"employment_type", "status", "is_active", "department", "designation", "reporting_manager_user_id",
|
||||||
@@ -447,7 +489,28 @@ def update_employee(db: Session, actor: User, emp: Employee, data: dict[str, Any
|
|||||||
emp.updated_by_user_id = actor.id
|
emp.updated_by_user_id = actor.id
|
||||||
emp.updated_at_utc = datetime.now(timezone.utc)
|
emp.updated_at_utc = datetime.now(timezone.utc)
|
||||||
_disable_relieved_employee_login(db, emp)
|
_disable_relieved_employee_login(db, emp)
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
if email_changed:
|
||||||
|
write_audit_log(
|
||||||
|
db,
|
||||||
|
action="employee.email.corrected",
|
||||||
|
entity_type="employee",
|
||||||
|
actor=actor,
|
||||||
|
entity_id=emp.id,
|
||||||
|
entity_name=emp.full_name,
|
||||||
|
target_tenant_id=emp.tenant_id,
|
||||||
|
target_branch_id=emp.branch_id,
|
||||||
|
details={
|
||||||
|
"old_email": old_email or None,
|
||||||
|
"new_email": requested_email or None,
|
||||||
|
"linked_user_id": linked_user.id if linked_user is not None else None,
|
||||||
|
"login_email_synchronised": linked_user is not None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
db.refresh(emp)
|
db.refresh(emp)
|
||||||
return emp
|
return emp
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,11 @@
|
|||||||
<div class="grid gap-4 md:grid-cols-2">
|
<div class="grid gap-4 md:grid-cols-2">
|
||||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Employee Code *</label><input name="employee_code" value="{{ val('employee_code') }}" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
<div><label class="mb-1 block text-sm font-medium text-slate-700">Employee Code *</label><input name="employee_code" value="{{ val('employee_code') }}" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Full Name *</label><input name="full_name" value="{{ val('full_name') }}" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
<div><label class="mb-1 block text-sm font-medium text-slate-700">Full Name *</label><input name="full_name" value="{{ val('full_name') }}" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Email</label><input type="email" name="email" value="{{ val('email') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
<div>
|
||||||
|
<label class="mb-1 block text-sm font-medium text-slate-700">Email</label>
|
||||||
|
<input type="email" name="email" value="{{ val('email') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||||
|
{% if is_edit %}<p class="mt-1 text-xs text-slate-500">Firm Admin / Partner can correct this email. If a login user is linked, the same address is applied to that existing login account; user ID, roles and history remain unchanged.</p>{% endif %}
|
||||||
|
</div>
|
||||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Mobile</label><input name="mobile" value="{{ val('mobile') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
<div><label class="mb-1 block text-sm font-medium text-slate-700">Mobile</label><input name="mobile" value="{{ val('mobile') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Alternate Mobile</label><input name="alternate_mobile" value="{{ val('alternate_mobile') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
<div><label class="mb-1 block text-sm font-medium text-slate-700">Alternate Mobile</label><input name="alternate_mobile" value="{{ val('alternate_mobile') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||||
<div><label class="mb-1 block text-sm font-medium text-slate-700">Date of Joining</label><input type="date" name="date_of_joining" value="{{ val('date_of_joining') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
<div><label class="mb-1 block text-sm font-medium text-slate-700">Date of Joining</label><input type="date" name="date_of_joining" value="{{ val('date_of_joining') }}" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user