diff --git a/apps/employees/admin.py b/apps/employees/admin.py index e6140b4..f9efa08 100644 --- a/apps/employees/admin.py +++ b/apps/employees/admin.py @@ -144,10 +144,13 @@ class WorkScheduleInline(TabularInline): model = WorkSchedule fk_name = 'employee' extra = 0 + tab = True fields = ('schedule_template', 'location', 'start_date', 'end_date', 'is_primary', 'is_active') readonly_fields = ('is_active',) ordering = ('-start_date',) show_change_link = True + verbose_name = _('График') + verbose_name_plural = _('График работы') def has_add_permission(self, request, obj=None): return False @@ -156,10 +159,11 @@ class WorkScheduleInline(TabularInline): class EmployeePermissionOverrideInline(TabularInline): model = EmployeePermissionOverride extra = 0 + tab = True fields = ('permission', 'override_type', 'valid_from', 'valid_until', 'is_active', 'comment') autocomplete_fields = [] - verbose_name = _('Доп. право на раздел портала') - verbose_name_plural = _('Доп. права на разделы портала') + verbose_name = _('Доп. право') + verbose_name_plural = _('Доп. права на разделы') def formfield_for_foreignkey(self, db_field, request, **kwargs): if db_field.name == 'permission': @@ -183,10 +187,13 @@ class AttendanceLogInline(TabularInline): fk_name = 'employee' extra = 0 max_num = 0 + tab = True fields = ('timestamp', 'direction', 'location', 'device_id', 'synced') readonly_fields = ('timestamp', 'direction', 'location', 'device_id', 'synced') ordering = ('-timestamp',) show_change_link = True + verbose_name = _('Отметка') + verbose_name_plural = _('Посещаемость') def has_add_permission(self, request, obj=None): return False @@ -199,6 +206,10 @@ class AttendanceLogInline(TabularInline): class EmployeeAdmin(ModelAdmin): form = EmployeeAdminForm inlines = [EmployeePermissionOverrideInline, WorkScheduleInline, AttendanceLogInline] + filter_horizontal = ('groups', 'user_permissions') + autocomplete_fields = ( + 'manager', 'user_pharmacy', 'user_location', 'user_organization', 'post', + ) class Media: css = {'all': ('users/css/employee_admin.css',)} @@ -215,7 +226,12 @@ class EmployeeAdmin(ModelAdmin): ] search_fields = ['username', 'email', 'last_name', 'first_name', 'middle_name'] list_select_related = ['user_pharmacy', 'department', 'post'] - readonly_fields = ('photo_preview', 'effective_access_summary') + readonly_fields = ( + 'photo_preview', 'effective_access_summary', 'date_joined', 'last_seen', + 'mattermost_sync_status', 'mattermost_last_sync', + 'ad_sync_status', 'ad_sync_error', 'ad_last_sync', + 'totp_enabled', + ) actions = ['reset_2fa'] actions_list = ['password_invites_action'] @@ -315,63 +331,71 @@ class EmployeeAdmin(ModelAdmin): self.message_user(request, f'2FA сброшена у {n} сотрудник(ов).') fieldsets = ( - (_('Аутентификация'), { - 'fields': ( - 'username', 'email', 'groups', 'is_active', - 'new_password', 'new_password_confirm', 'sync_ad_password', - ), + (_('Основное'), { 'classes': ('tab',), + 'fields': ( + ('last_name', 'first_name', 'middle_name'), + ('username', 'email'), + 'is_active', + 'groups', + ('photo', 'photo_preview'), + ), }), - (_('Доступ (сводка)'), { + (_('Работа'), { + 'classes': ('tab',), + 'fields': ( + 'user_location_type', + ('user_organization', 'user_pharmacy', 'user_location'), + ('department', 'post', 'manager'), + ('hire_date', 'probation_end_date', 'termination_date'), + 'it_work_channel', + 'it_responsibility_zone', + ), + }), + (_('Контакты и личное'), { + 'classes': ('tab',), + 'fields': ( + ('phone', 'internal_number'), + ('personal_phone', 'personal_email'), + ('emergency_contact_name', 'emergency_contact_phone', 'emergency_contact_relation'), + ('birth_date', 'inn_code'), + ), + }), + (_('Пароль и 2FA'), { + 'classes': ('tab',), + 'fields': ( + 'new_password', 'new_password_confirm', 'sync_ad_password', + 'totp_enabled', + ), + 'description': _( + 'Оставьте пароль пустым, чтобы не менять. ' + '2FA сбрасывается действием в списке сотрудников.' + ), + }), + (_('Доступ к порталу'), { + 'classes': ('tab',), 'fields': ('effective_access_summary',), + 'description': _( + 'Итог по разделам: группы + индивидуальные доп. права. ' + 'Группы — на вкладке «Основное»; точечные права — вкладка «Доп. права».' + ), + }), + (_('Интеграции'), { 'classes': ('tab',), 'description': _( - 'Итоговые разрешения на разделы портала (группы + индивидуальные ' - 'override). Изменить: поле «Группы» выше или блок «Доп. права на ' - 'разделы портала» ниже.' + 'Mattermost: автосвязка только по email. Чтобы привязать уже ' + 'существующую учётку — укажите её User ID и логин вручную, ' + 'лишнюю новую учётку деактивируйте в Mattermost.' ), - }), - (_('ФИО'), { - 'fields': ('last_name', 'first_name', 'middle_name'), - 'classes': ('tab',), - }), - (_('Рабочая информация'), { 'fields': ( - 'user_location_type', 'user_organization', - 'user_pharmacy', 'user_location', - 'department', 'post', 'manager', - 'it_work_channel', 'it_responsibility_zone', + 'mattermost_sync_enabled', + ('mattermost_user_id', 'mattermost_username'), + ('mattermost_sync_status', 'mattermost_last_sync'), + 'ad_sync_enabled', + 'ad_user_dn', + ('ad_sync_status', 'ad_last_sync'), + 'ad_sync_error', ), - 'classes': ('tab',), - }), - (_('Даты'), { - 'fields': ('hire_date', 'termination_date', 'probation_end_date', 'date_joined'), - 'classes': ('tab',), - }), - (_('Личные данные'), { - 'fields': ('birth_date', 'inn_code', 'photo', 'photo_preview'), - 'classes': ('tab',), - }), - (_('Контакты'), { - 'fields': ( - 'phone', 'internal_number', 'personal_phone', 'personal_email', - 'emergency_contact_name', 'emergency_contact_phone', 'emergency_contact_relation', - ), - 'classes': ('tab',), - }), - ('Mattermost', { - 'fields': ('mattermost_user_id', 'mattermost_username', 'mattermost_sync_status', - 'mattermost_last_sync', 'mattermost_sync_enabled'), - 'classes': ('tab', 'collapse'), - }), - ('Active Directory', { - 'fields': ('ad_user_dn', 'ad_sync_status', 'ad_sync_error', 'ad_last_sync', 'ad_sync_enabled'), - 'classes': ('tab', 'collapse'), - }), - (_('Графік та відвідуваність'), { - 'fields': (), - 'classes': ('tab',), - 'description': _('Графіки роботи та записи відвідуваності відображаються нижче.'), }), ) @@ -379,11 +403,14 @@ class EmployeeAdmin(ModelAdmin): fieldsets = list(super().get_fieldsets(request, obj)) if request.user.is_superuser: fieldsets.append(( - _('Системный доступ'), + _('Система'), { - 'fields': ('is_staff', 'user_permissions'), - 'classes': ('tab', 'collapse'), - 'description': _('Только для IT-администраторов. is_staff — вход в Django admin.'), + 'fields': ( + 'is_staff', 'user_permissions', + ('date_joined', 'last_seen'), + ), + 'classes': ('tab',), + 'description': _('Только для IT: вход в Django admin и сырые права.'), }, )) return fieldsets @@ -398,6 +425,17 @@ class EmployeeAdmin(ModelAdmin): ]) return readonly + def formfield_for_dbfield(self, db_field, request, **kwargs): + formfield = super().formfield_for_dbfield(db_field, request, **kwargs) + if db_field.name == 'mattermost_user_id' and formfield is not None: + formfield.help_text = _( + 'ID пользователя в Mattermost (System Console → Users). ' + 'Нужен для ручной привязки существующей учётки.' + ) + if db_field.name == 'mattermost_username' and formfield is not None: + formfield.help_text = _('Логин в Mattermost (без @).') + return formfield + def save_model(self, request, obj, form, change): if not request.user.is_superuser and change: previous = Employee.objects.filter(pk=obj.pk).prefetch_related('groups').first()