Add RRO, portal section perms, dislocation presets, and hiring intern flow.

Ship cash-register registry, section-based portal access, configurable dislocation columns, and trial-hire intern accounts with credentials PDF.
This commit is contained in:
2026-08-09 14:58:17 +03:00
parent 237275abd0
commit ae5c1f1848
63 changed files with 5987 additions and 2217 deletions
+133 -4
View File
@@ -1,14 +1,22 @@
from django import forms
from django.contrib import admin
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from unfold.admin import ModelAdmin, TabularInline
from core.models import Directory
from .models import Employee, PermissionGroup
from core.models import Directory, PortalAccess
from .models import Employee, PermissionGroup, EmployeeDefaults, EmployeePermissionOverride
from apps.visit_manager.models import WorkSchedule, AttendanceLog
def _portal_permissions_qs():
ct = ContentType.objects.get_for_model(PortalAccess, for_concrete_model=False)
return Permission.objects.filter(content_type=ct, codename__startswith='access_').order_by('codename')
class HierarchicalDepartmentChoiceField(forms.ModelChoiceField):
"""Поле выбора отдела с отступами по иерархии в выпадающем списке."""
@@ -78,6 +86,20 @@ class WorkScheduleInline(TabularInline):
return False
class EmployeePermissionOverrideInline(TabularInline):
model = EmployeePermissionOverride
extra = 0
fields = ('permission', 'override_type', 'valid_from', 'valid_until', 'is_active', 'comment')
autocomplete_fields = []
verbose_name = _('Доп. право на раздел портала')
verbose_name_plural = _('Доп. права на разделы портала')
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == 'permission':
kwargs['queryset'] = _portal_permissions_qs()
return super().formfield_for_foreignkey(db_field, request, **kwargs)
class AttendanceLogInline(TabularInline):
model = AttendanceLog
fk_name = 'employee'
@@ -98,7 +120,7 @@ class AttendanceLogInline(TabularInline):
@admin.register(Employee)
class EmployeeAdmin(ModelAdmin):
form = EmployeeAdminForm
inlines = [WorkScheduleInline, AttendanceLogInline]
inlines = [EmployeePermissionOverrideInline, WorkScheduleInline, AttendanceLogInline]
class Media:
css = {'all': ('users/css/employee_admin.css',)}
@@ -256,4 +278,111 @@ class EmployeeAdmin(ModelAdmin):
)
except Exception:
return obj.department.name or '—'
department_display.short_description = _('Отдел')
department_display.short_description = _('Отдел')
@admin.register(EmployeeDefaults)
class EmployeeDefaultsAdmin(ModelAdmin):
"""Единственная запись настроек по умолчанию для сотрудников."""
fieldsets = (
(_('Офисные сотрудники'), {
'fields': ('office_email_domain',),
'description': _(
'При утверждении заявки на найм офисному сотруднику '
'назначается рабочий email: логин@домен.'
),
}),
)
def has_add_permission(self, request):
return not EmployeeDefaults.objects.exists()
def has_delete_permission(self, request, obj=None):
return False
class PermissionGroupAdminForm(forms.ModelForm):
portal_permissions = forms.ModelMultipleChoiceField(
label=_('Разделы портала'),
queryset=Permission.objects.none(),
required=False,
widget=admin.widgets.FilteredSelectMultiple(_('разделы'), is_stacked=False),
help_text=_('Базовый набор доступов к разделам для этой роли. Доп. права сотруднику — через overrides.'),
)
class Meta:
model = PermissionGroup
fields = ('group', 'description', 'is_active')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['portal_permissions'].queryset = _portal_permissions_qs()
if self.instance and self.instance.pk and self.instance.group_id:
portal_pks = set(_portal_permissions_qs().values_list('pk', flat=True))
self.fields['portal_permissions'].initial = [
p for p in self.instance.group.permissions.all() if p.pk in portal_pks
]
def save_portal_permissions(self, obj):
portal_qs = list(_portal_permissions_qs())
selected = self.cleaned_data.get('portal_permissions')
if selected is None:
return
group = obj.group
group.permissions.remove(*portal_qs)
group.permissions.add(*selected)
@admin.register(PermissionGroup)
class PermissionGroupAdmin(ModelAdmin):
form = PermissionGroupAdminForm
list_display = ('group', 'is_active', 'description')
list_filter = ('is_active',)
search_fields = ('group__name', 'description')
fieldsets = (
(None, {
'fields': ('group', 'description', 'is_active', 'portal_permissions'),
}),
)
def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)
form.save_portal_permissions(obj)
@admin.register(EmployeePermissionOverride)
class EmployeePermissionOverrideAdmin(ModelAdmin):
list_display = (
'employee', 'permission', 'override_type',
'valid_from', 'valid_until', 'is_active',
)
list_filter = ('override_type', 'is_active')
search_fields = (
'employee__username', 'employee__last_name', 'employee__first_name',
'permission__codename', 'comment',
)
list_select_related = ('employee', 'permission')
autocomplete_fields = ('employee',)
readonly_fields = ('created_at', 'updated_at', 'uuid')
fieldsets = (
(None, {
'fields': ('employee', 'permission', 'override_type', 'is_active'),
'description': _(
'grant — выдать раздел сверх группы; '
'revoke — запретить раздел даже если право есть в группе.'
),
}),
(_('Срок действия'), {
'fields': ('valid_from', 'valid_until', 'comment'),
}),
(_('Служебное'), {
'classes': ('collapse',),
'fields': ('uuid', 'created_at', 'updated_at'),
}),
)
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == 'permission':
kwargs['queryset'] = _portal_permissions_qs()
return super().formfield_for_foreignkey(db_field, request, **kwargs)