diff --git a/backend/api/v1/employees/views.py b/backend/api/v1/employees/views.py index 0da04250..0adc0f59 100644 --- a/backend/api/v1/employees/views.py +++ b/backend/api/v1/employees/views.py @@ -2765,9 +2765,11 @@ class EmployeeActionViewSet(HistoryActionMixin, viewsets.ModelViewSet): employee=emp ).select_related('department') + departments_processed = False for emp_dept in active_departments: try: svc.remove_member(emp_dept.department, emp) + departments_processed = True except (DirectoryLdapError, DirectoryDbError, DirectoryServiceError) as e: # Логируем ошибку удаления из конкретного отдела import logging @@ -2776,6 +2778,45 @@ class EmployeeActionViewSet(HistoryActionMixin, viewsets.ModelViewSet): f"Failed to remove dismissed employee from department: " f"employee_id={emp.id}, department_id={emp_dept.department.id}, error={e}" ) + + # Если сотрудник не состоял ни в одном отделе, вручную перемещаем в OU=Dismissed + if not departments_processed: + try: + from employees.ldap.utils.ldap_utils import get_base_dn_for_employee + from employees.ldap.infrastructure.connections import _ldap + from employees.ldap.repositories.ldap_repository import ensure_container_exists + from employees.models import LdapSyncState + + sync_state = LdapSyncState.objects.filter( + model='employee', + object_pk=str(emp.pk) + ).first() + + if sync_state and sync_state.ldap_dn: + target_base = get_base_dn_for_employee(emp) + current_dn = sync_state.ldap_dn + + # Проверяем, нужно ли перемещение + if not current_dn.lower().endswith(target_base.lower()): + with _ldap() as conn: + ensure_container_exists(conn, target_base) + new_dn = svc._user_service._move_user_to_base( + conn, current_dn, target_base + ) + sync_state.touch(ldap_dn=new_dn, sync_dir="ldap") + import logging + logger = logging.getLogger(__name__) + logger.info( + f"Dismissed employee without department moved to OU=Dismissed: " + f"employee_id={emp.id}, new_dn={new_dn}" + ) + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.error( + f"Failed to move dismissed employee without department to OU=Dismissed: " + f"employee_id={emp.id}, error={e}" + ) except (DirectoryLdapError, DirectoryDbError, DirectoryServiceError) as e: # Логируем ошибку, но не прерываем процесс # БД-изменения уже применены diff --git a/backend/tests/api/v1/employees/test_employee_actions.py b/backend/tests/api/v1/employees/test_employee_actions.py index 30137e32..187a0122 100644 --- a/backend/tests/api/v1/employees/test_employee_actions.py +++ b/backend/tests/api/v1/employees/test_employee_actions.py @@ -548,3 +548,61 @@ def test_restoration_moves_from_dismissed_to_users( # Проверяем, что был вызван _move_user_to_base (через mock LDAP) # Это косвенная проверка - мы мокнули _ldap(), так что код выполнился + +@pytest.mark.django_db +@patch('api.v1.employees.views._is_ldap_enabled') +@patch('employees.ldap.infrastructure.connections._ldap') +@patch('employees.ldap.directory_service.DirectoryService.update_user') +def test_dismissal_without_department_moves_to_dismissed( + mock_update, mock_ldap_ctx, mock_is_enabled, api_client, settings +): + """Проверяет перемещение в OU=Dismissed при увольнении сотрудника без отдела.""" + from employees.models import LdapSyncState + from unittest.mock import MagicMock + + mock_is_enabled.return_value = True + settings.LDAP_DISMISSED_BASE = "OU=Dismissed,DC=example,DC=com" + settings.LDAP_USERS_BASE = "OU=Users,DC=example,DC=com" + + # Мокаем LDAP соединение + mock_conn = MagicMock() + mock_ldap_ctx.return_value.__enter__.return_value = mock_conn + mock_ldap_ctx.return_value.__exit__.return_value = False + + staff = _user(staff=True) + api_client.force_authenticate(user=staff) + emp = _user() + + # Сотрудник НЕ состоит ни в одном отделе (нет EmployeeDepartment записей) + # Создаём запись LdapSyncState - сотрудник в OU=Users + sync_state = LdapSyncState.objects.create( + model='employee', + object_pk=str(emp.pk), + ldap_dn=f'CN={emp.email},OU=Users,DC=example,DC=com', + last_sync_dir='ldap' + ) + + url = reverse("api:v1:employee-actions-list") + resp = api_client.post( + url, + { + "employee": emp.id, + "action": ACTION_DISMISSED, + "date": timezone.now().isoformat(), + }, + format="json", + ) + + assert resp.status_code == 201 + emp.refresh_from_db() + assert emp.is_active is False + + # Проверяем вызов update_user (деактивация в LDAP) + mock_update.assert_called_once() + call_args = mock_update.call_args + assert call_args[0][0].id == emp.id + assert call_args[1]['changes'] == {'is_active': False} + + # Проверяем, что LDAP соединение было создано (для перемещения в OU=Dismissed) + assert mock_ldap_ctx.called, "LDAP connection should be established for moving to OU=Dismissed" +