810 lines
25 KiB
Python
810 lines
25 KiB
Python
"""Restricted operational admin for the first payroll backend slice."""
|
||
|
||
from django import forms
|
||
from django.contrib import admin, messages
|
||
from django.contrib.admin import helpers
|
||
from django.core.exceptions import PermissionDenied
|
||
from django.db import transaction
|
||
from django.db.models import Q
|
||
from django.template.response import TemplateResponse
|
||
|
||
from .enums import ApprovalStatus
|
||
from .models import (
|
||
EmployeePayRate,
|
||
FinanceSettings,
|
||
PayrollAuditEvent,
|
||
PayrollDailyWorkEntry,
|
||
PayrollInputLine,
|
||
PayrollPeriod,
|
||
PayrollRun,
|
||
PayrollStatement,
|
||
PayrollStatementAcknowledgement,
|
||
PayrollStatementLine,
|
||
PayrollWorkSettings,
|
||
PayrollWorkRecord,
|
||
)
|
||
from .payroll.access import has_payroll_permission, has_simple_admin_access
|
||
from .payroll.exceptions import PayrollOperationError
|
||
from .payroll.services import (
|
||
approve_input_line,
|
||
approve_pay_rate,
|
||
approve_run,
|
||
approve_work_record,
|
||
calculate_period,
|
||
publish_run,
|
||
return_run_for_correction,
|
||
submit_run_for_review,
|
||
)
|
||
|
||
|
||
@admin.register(FinanceSettings)
|
||
class FinanceSettingsAdmin(admin.ModelAdmin):
|
||
"""Singleton allow-list for the native finance management workspace."""
|
||
|
||
list_display = ("name", "management_users_count", "updated_at")
|
||
fields = ("name", "management_users", "updated_at")
|
||
readonly_fields = ("updated_at",)
|
||
autocomplete_fields = ("management_users",)
|
||
|
||
def has_add_permission(self, request):
|
||
if FinanceSettings.objects.exists():
|
||
return False
|
||
return super().has_add_permission(request)
|
||
|
||
def has_delete_permission(self, request, obj=None):
|
||
return request.user.is_superuser
|
||
|
||
@admin.display(description="Доступ к управлению")
|
||
def management_users_count(self, obj):
|
||
count = obj.management_users.count()
|
||
return count or "Все администраторы"
|
||
|
||
|
||
def payroll_reason_action_response(
|
||
model_admin,
|
||
request,
|
||
queryset,
|
||
*,
|
||
action_name,
|
||
title,
|
||
reason_required=True,
|
||
error="",
|
||
):
|
||
context = {
|
||
**model_admin.admin_site.each_context(request),
|
||
"title": title,
|
||
"opts": model_admin.model._meta,
|
||
"queryset": queryset,
|
||
"action_name": action_name,
|
||
"action_checkbox_name": helpers.ACTION_CHECKBOX_NAME,
|
||
"reason_required": reason_required,
|
||
"error": error,
|
||
}
|
||
return TemplateResponse(
|
||
request,
|
||
"admin/finance/payroll_reason_action.html",
|
||
context,
|
||
)
|
||
|
||
|
||
def payroll_approval_action_response(
|
||
model_admin,
|
||
request,
|
||
queryset,
|
||
*,
|
||
action_name,
|
||
title,
|
||
error="",
|
||
):
|
||
context = {
|
||
**model_admin.admin_site.each_context(request),
|
||
"title": title,
|
||
"opts": model_admin.model._meta,
|
||
"queryset": queryset,
|
||
"action_name": action_name,
|
||
"action_checkbox_name": helpers.ACTION_CHECKBOX_NAME,
|
||
"error": error,
|
||
}
|
||
return TemplateResponse(
|
||
request,
|
||
"admin/finance/payroll_approval_action.html",
|
||
context,
|
||
)
|
||
|
||
|
||
class VersionedDraftAdminForm(forms.ModelForm):
|
||
"""Carry the version seen on GET back to the locked admin POST."""
|
||
|
||
expected_lock_version = forms.IntegerField(
|
||
min_value=0,
|
||
required=False,
|
||
widget=forms.HiddenInput,
|
||
)
|
||
|
||
class Meta:
|
||
fields = "__all__"
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
if not self.is_bound and self.instance and self.instance.pk:
|
||
self.initial["expected_lock_version"] = self.instance.lock_version
|
||
|
||
|
||
class PayrollSensitiveAdminMixin:
|
||
"""Use explicit payroll roles instead of generic Django model access."""
|
||
|
||
view_permissions = ()
|
||
|
||
def _has_payroll_access(self, request):
|
||
if request.user.is_superuser:
|
||
return True
|
||
return any(
|
||
has_payroll_permission(request.user, permission)
|
||
for permission in self.view_permissions
|
||
)
|
||
|
||
def has_module_permission(self, request):
|
||
return self._has_payroll_access(request)
|
||
|
||
def has_view_permission(self, request, obj=None):
|
||
return self._has_payroll_access(request)
|
||
|
||
|
||
class ImmutableApprovedAdminMixin(PayrollSensitiveAdminMixin):
|
||
"""Prevent editing approved/voided inputs through Django admin."""
|
||
|
||
view_permissions = (
|
||
"finance.manage_payroll_inputs",
|
||
"finance.approve_payroll_inputs",
|
||
"finance.view_all_payroll",
|
||
)
|
||
form = VersionedDraftAdminForm
|
||
|
||
def get_readonly_fields(self, request, obj=None):
|
||
readonly = list(super().get_readonly_fields(request, obj))
|
||
if "lock_version" not in readonly:
|
||
readonly.append("lock_version")
|
||
if "status" not in readonly:
|
||
readonly.append("status")
|
||
if obj is not None and (
|
||
not has_payroll_permission(
|
||
request.user,
|
||
"finance.manage_payroll_inputs",
|
||
)
|
||
or obj.status != ApprovalStatus.DRAFT
|
||
or (
|
||
obj.created_by_id != request.user.pk
|
||
and not has_simple_admin_access(request.user)
|
||
)
|
||
):
|
||
readonly.extend(
|
||
field.name
|
||
for field in obj._meta.concrete_fields
|
||
if field.name not in readonly
|
||
)
|
||
return readonly
|
||
|
||
def has_delete_permission(self, request, obj=None):
|
||
return request.user.is_superuser
|
||
|
||
def has_add_permission(self, request):
|
||
return has_payroll_permission(
|
||
request.user,
|
||
"finance.manage_payroll_inputs",
|
||
)
|
||
|
||
def has_change_permission(self, request, obj=None):
|
||
return self._has_payroll_access(request)
|
||
|
||
@transaction.atomic
|
||
def save_model(self, request, obj, form, change):
|
||
if not has_payroll_permission(
|
||
request.user,
|
||
"finance.manage_payroll_inputs",
|
||
):
|
||
raise PermissionDenied("Нет права изменять входные данные зарплаты.")
|
||
if change:
|
||
if form is None or form.cleaned_data.get("expected_lock_version") is None:
|
||
raise PermissionDenied(
|
||
"Версия открытого черновика потеряна; обновите страницу."
|
||
)
|
||
expected_lock_version = form.cleaned_data["expected_lock_version"]
|
||
current = obj.__class__.objects.select_for_update().get(pk=obj.pk)
|
||
if current.status != ApprovalStatus.DRAFT or (
|
||
current.created_by_id != request.user.pk
|
||
and not has_simple_admin_access(request.user)
|
||
):
|
||
raise PermissionDenied("Этот черновик нельзя изменить.")
|
||
if current.lock_version != expected_lock_version:
|
||
raise PermissionDenied(
|
||
"Черновик уже изменён другой операцией; обновите страницу."
|
||
)
|
||
obj.status = current.status
|
||
obj.lock_version = current.lock_version
|
||
obj._expected_lock_version = expected_lock_version
|
||
if not change:
|
||
obj.created_by = request.user
|
||
super().save_model(request, obj, form, change)
|
||
|
||
|
||
@admin.register(PayrollPeriod)
|
||
class PayrollPeriodAdmin(PayrollSensitiveAdminMixin, admin.ModelAdmin):
|
||
view_permissions = (
|
||
"finance.manage_payroll_inputs",
|
||
"finance.calculate_payroll",
|
||
"finance.approve_payroll",
|
||
"finance.publish_payroll",
|
||
"finance.view_all_payroll",
|
||
)
|
||
list_display = [
|
||
"code",
|
||
"name",
|
||
"date_from",
|
||
"date_to",
|
||
"status",
|
||
"current_run",
|
||
]
|
||
list_filter = ["status", "currency"]
|
||
search_fields = ["code", "name"]
|
||
readonly_fields = [
|
||
"status",
|
||
"current_run",
|
||
"lock_version",
|
||
"created_by",
|
||
"created_at",
|
||
"updated_at",
|
||
]
|
||
actions = ["calculate_selected"]
|
||
|
||
def save_model(self, request, obj, form, change):
|
||
if not change:
|
||
obj.created_by = request.user
|
||
super().save_model(request, obj, form, change)
|
||
|
||
def has_add_permission(self, request):
|
||
return has_payroll_permission(
|
||
request.user,
|
||
"finance.manage_payroll_inputs",
|
||
)
|
||
|
||
def has_change_permission(self, request, obj=None):
|
||
if obj is None:
|
||
return self._has_payroll_access(request)
|
||
return has_payroll_permission(
|
||
request.user,
|
||
"finance.manage_payroll_inputs",
|
||
)
|
||
|
||
def has_delete_permission(self, request, obj=None):
|
||
return request.user.is_superuser
|
||
|
||
@admin.action(description="Рассчитать выбранные периоды")
|
||
def calculate_selected(self, request, queryset):
|
||
if not has_payroll_permission(request.user, "finance.calculate_payroll"):
|
||
self.message_user(request, "Недостаточно прав.", level=messages.ERROR)
|
||
return
|
||
reason_required = any(period.current_run_id for period in queryset)
|
||
if "confirm_payroll_action" not in request.POST:
|
||
return payroll_reason_action_response(
|
||
self,
|
||
request,
|
||
queryset,
|
||
action_name="calculate_selected",
|
||
title="Расчёт зарплатного периода",
|
||
reason_required=reason_required,
|
||
)
|
||
reason = request.POST.get("reason", "").strip()
|
||
if reason_required and not reason:
|
||
return payroll_reason_action_response(
|
||
self,
|
||
request,
|
||
queryset,
|
||
action_name="calculate_selected",
|
||
title="Расчёт зарплатного периода",
|
||
reason_required=True,
|
||
error="Для повторного расчёта обязательно укажите основание.",
|
||
)
|
||
success_count = 0
|
||
for period in queryset:
|
||
try:
|
||
calculate_period(
|
||
period.pk,
|
||
actor=request.user,
|
||
recalculation_reason=reason if period.current_run_id else "",
|
||
)
|
||
except PayrollOperationError as exc:
|
||
self.message_user(
|
||
request,
|
||
f"{period}: {exc.message} ({exc.code})",
|
||
level=messages.ERROR,
|
||
)
|
||
else:
|
||
success_count += 1
|
||
if success_count:
|
||
self.message_user(
|
||
request,
|
||
f"Рассчитано периодов: {success_count}",
|
||
level=messages.SUCCESS,
|
||
)
|
||
|
||
|
||
@admin.register(EmployeePayRate)
|
||
class EmployeePayRateAdmin(ImmutableApprovedAdminMixin, admin.ModelAdmin):
|
||
exclude = ["self_approval_overridden"]
|
||
list_display = [
|
||
"employee",
|
||
"rate_code",
|
||
"amount",
|
||
"currency",
|
||
"effective_from",
|
||
"revision",
|
||
"status",
|
||
]
|
||
list_filter = [
|
||
"status",
|
||
"currency",
|
||
"source",
|
||
"effective_from",
|
||
]
|
||
search_fields = [
|
||
"employee__last_name",
|
||
"employee__first_name",
|
||
"employee__email",
|
||
"rate_code",
|
||
"source_ref",
|
||
]
|
||
readonly_fields = [
|
||
"created_by",
|
||
"created_at",
|
||
"updated_at",
|
||
"approved_by",
|
||
"approved_at",
|
||
"voided_by",
|
||
"voided_at",
|
||
]
|
||
actions = ["approve_selected"]
|
||
|
||
@staticmethod
|
||
def _revision_chain_ids(records):
|
||
chain_ids = {record.pk for record in records if record.pk is not None}
|
||
frontier = set(chain_ids)
|
||
while frontier:
|
||
linked_rows = EmployeePayRate.objects.filter(
|
||
Q(pk__in=frontier) | Q(replaces_id__in=frontier)
|
||
).values_list("pk", "replaces_id")
|
||
linked_ids = {
|
||
linked_id
|
||
for row in linked_rows
|
||
for linked_id in row
|
||
if linked_id is not None
|
||
}
|
||
frontier = linked_ids - chain_ids
|
||
chain_ids.update(frontier)
|
||
return chain_ids
|
||
|
||
def get_deleted_objects(self, objs, request):
|
||
chain_ids = self._revision_chain_ids(objs)
|
||
records = EmployeePayRate.objects.filter(pk__in=chain_ids).order_by(
|
||
"employee_id",
|
||
"effective_from",
|
||
"revision",
|
||
"pk",
|
||
)
|
||
deleted_objects = [
|
||
(
|
||
f"{self.model._meta.verbose_name}: {record} "
|
||
f"(ID {record.pk}, ревизия {record.revision}, "
|
||
f"статус: {record.get_status_display()})"
|
||
)
|
||
for record in records
|
||
]
|
||
return (
|
||
deleted_objects,
|
||
{self.model._meta.verbose_name_plural: len(deleted_objects)},
|
||
set(),
|
||
[],
|
||
)
|
||
|
||
@transaction.atomic
|
||
def _delete_revision_chains(self, records):
|
||
chain_ids = self._revision_chain_ids(records)
|
||
revisions = EmployeePayRate.objects.select_for_update().filter(
|
||
pk__in=chain_ids
|
||
).order_by("-revision", "-pk")
|
||
for revision in revisions:
|
||
revision.delete()
|
||
|
||
def delete_model(self, request, obj):
|
||
self._delete_revision_chains([obj])
|
||
|
||
def delete_queryset(self, request, queryset):
|
||
self._delete_revision_chains(queryset)
|
||
|
||
@admin.action(description="Утвердить выбранные ставки")
|
||
def approve_selected(self, request, queryset):
|
||
return self._approve_records(
|
||
request,
|
||
queryset,
|
||
approve_pay_rate,
|
||
action_name="approve_selected",
|
||
title="Утверждение ставок",
|
||
)
|
||
|
||
def _approve_records(
|
||
self,
|
||
request,
|
||
queryset,
|
||
service,
|
||
*,
|
||
action_name,
|
||
title,
|
||
):
|
||
if not has_payroll_permission(
|
||
request.user,
|
||
"finance.approve_payroll_inputs",
|
||
):
|
||
self.message_user(request, "Недостаточно прав.", level=messages.ERROR)
|
||
return
|
||
if "confirm_payroll_approval" not in request.POST:
|
||
return payroll_approval_action_response(
|
||
self,
|
||
request,
|
||
queryset,
|
||
action_name=action_name,
|
||
title=title,
|
||
)
|
||
for record in queryset:
|
||
try:
|
||
expected_lock_version = int(request.POST[f"lock_version_{record.pk}"])
|
||
except (KeyError, TypeError, ValueError):
|
||
return payroll_approval_action_response(
|
||
self,
|
||
request,
|
||
queryset,
|
||
action_name=action_name,
|
||
title=title,
|
||
error="Версия черновика потеряна; проверьте данные ещё раз.",
|
||
)
|
||
try:
|
||
service(
|
||
record.pk,
|
||
actor=request.user,
|
||
expected_lock_version=expected_lock_version,
|
||
)
|
||
except PayrollOperationError as exc:
|
||
self.message_user(
|
||
request,
|
||
f"{record}: {exc.message} ({exc.code})",
|
||
level=messages.ERROR,
|
||
)
|
||
|
||
|
||
@admin.register(PayrollWorkRecord)
|
||
class PayrollWorkRecordAdmin(ImmutableApprovedAdminMixin, admin.ModelAdmin):
|
||
exclude = ["self_approval_overridden"]
|
||
list_display = [
|
||
"period",
|
||
"employee",
|
||
"target_points",
|
||
"actual_points",
|
||
"revision",
|
||
"status",
|
||
]
|
||
list_filter = ["period", "status", "source"]
|
||
search_fields = [
|
||
"employee__last_name",
|
||
"employee__first_name",
|
||
"employee__email",
|
||
"source_ref",
|
||
]
|
||
readonly_fields = [
|
||
"created_by",
|
||
"created_at",
|
||
"updated_at",
|
||
"approved_by",
|
||
"approved_at",
|
||
"voided_by",
|
||
"voided_at",
|
||
]
|
||
actions = ["approve_selected"]
|
||
|
||
@admin.action(description="Утвердить выбранную выработку")
|
||
def approve_selected(self, request, queryset):
|
||
return EmployeePayRateAdmin._approve_records(
|
||
self,
|
||
request,
|
||
queryset,
|
||
approve_work_record,
|
||
action_name="approve_selected",
|
||
title="Утверждение выработки",
|
||
)
|
||
|
||
|
||
@admin.register(PayrollDailyWorkEntry)
|
||
class PayrollDailyWorkEntryAdmin(admin.ModelAdmin):
|
||
list_display = [
|
||
"work_date",
|
||
"employee",
|
||
"target_points",
|
||
"actual_points",
|
||
"period",
|
||
"updated_at",
|
||
]
|
||
list_filter = ["period", "work_date"]
|
||
search_fields = [
|
||
"employee__last_name",
|
||
"employee__first_name",
|
||
"employee__email",
|
||
"note",
|
||
]
|
||
readonly_fields = ["lock_version", "created_at", "updated_at"]
|
||
|
||
|
||
@admin.register(PayrollWorkSettings)
|
||
class PayrollWorkSettingsAdmin(admin.ModelAdmin):
|
||
fields = ["daily_target_points", "updated_at"]
|
||
list_display = ["daily_target_points", "updated_at"]
|
||
readonly_fields = ["updated_at"]
|
||
|
||
def has_add_permission(self, request):
|
||
if PayrollWorkSettings.objects.exists():
|
||
return False
|
||
return super().has_add_permission(request)
|
||
|
||
def has_delete_permission(self, request, obj=None):
|
||
return request.user.is_superuser
|
||
|
||
|
||
@admin.register(PayrollInputLine)
|
||
class PayrollInputLineAdmin(ImmutableApprovedAdminMixin, admin.ModelAdmin):
|
||
exclude = ["reversal_of", "self_approval_overridden"]
|
||
list_display = [
|
||
"period",
|
||
"employee",
|
||
"component_code",
|
||
"amount",
|
||
"relates_to_period",
|
||
"status",
|
||
]
|
||
list_filter = [
|
||
"period",
|
||
"status",
|
||
"component_code",
|
||
"source",
|
||
]
|
||
search_fields = [
|
||
"employee__last_name",
|
||
"employee__first_name",
|
||
"employee__email",
|
||
"reason",
|
||
"source_ref",
|
||
]
|
||
readonly_fields = [
|
||
"idempotency_key",
|
||
"created_by",
|
||
"created_at",
|
||
"approved_by",
|
||
"approved_at",
|
||
"voided_by",
|
||
"voided_at",
|
||
]
|
||
actions = ["approve_selected"]
|
||
|
||
@admin.action(description="Утвердить выбранные строки")
|
||
def approve_selected(self, request, queryset):
|
||
return EmployeePayRateAdmin._approve_records(
|
||
self,
|
||
request,
|
||
queryset,
|
||
approve_input_line,
|
||
action_name="approve_selected",
|
||
title="Утверждение начислений и удержаний",
|
||
)
|
||
|
||
|
||
class PayrollStatementLineInline(admin.TabularInline):
|
||
model = PayrollStatementLine
|
||
extra = 0
|
||
can_delete = False
|
||
fields = ["position", "code", "label", "kind", "amount", "reason"]
|
||
readonly_fields = fields
|
||
|
||
def has_add_permission(self, request, obj=None):
|
||
return request.user.is_superuser
|
||
|
||
def has_delete_permission(self, request, obj=None):
|
||
return request.user.is_superuser
|
||
|
||
|
||
@admin.register(PayrollRun)
|
||
class PayrollRunAdmin(PayrollSensitiveAdminMixin, admin.ModelAdmin):
|
||
exclude = ["self_approval_overridden"]
|
||
view_permissions = (
|
||
"finance.calculate_payroll",
|
||
"finance.approve_payroll",
|
||
"finance.publish_payroll",
|
||
"finance.view_all_payroll",
|
||
)
|
||
list_display = [
|
||
"period",
|
||
"revision",
|
||
"status",
|
||
"employee_count",
|
||
"gross_total",
|
||
"payable_total",
|
||
"requested_by",
|
||
]
|
||
list_filter = ["status", "period"]
|
||
search_fields = ["period__code", "input_hash", "result_hash"]
|
||
readonly_fields = [
|
||
field.name
|
||
for field in PayrollRun._meta.concrete_fields
|
||
if field.name != "self_approval_overridden"
|
||
]
|
||
actions = [
|
||
"request_review",
|
||
"return_selected",
|
||
"approve_selected",
|
||
"publish_selected",
|
||
]
|
||
|
||
def has_add_permission(self, request):
|
||
return request.user.is_superuser
|
||
|
||
def has_delete_permission(self, request, obj=None):
|
||
return request.user.is_superuser
|
||
|
||
def has_change_permission(self, request, obj=None):
|
||
return self._has_payroll_access(request)
|
||
|
||
def _run_action(self, request, queryset, service):
|
||
for run in queryset:
|
||
try:
|
||
service(run.pk, actor=request.user)
|
||
except PayrollOperationError as exc:
|
||
self.message_user(
|
||
request,
|
||
f"{run}: {exc.message} ({exc.code})",
|
||
level=messages.ERROR,
|
||
)
|
||
|
||
@admin.action(description="Передать на проверку")
|
||
def request_review(self, request, queryset):
|
||
self._run_action(request, queryset, submit_run_for_review)
|
||
|
||
@admin.action(description="Вернуть расчёт на исправление")
|
||
def return_selected(self, request, queryset):
|
||
if "confirm_payroll_action" not in request.POST:
|
||
return payroll_reason_action_response(
|
||
self,
|
||
request,
|
||
queryset,
|
||
action_name="return_selected",
|
||
title="Возврат расчёта на исправление",
|
||
)
|
||
reason = request.POST.get("reason", "").strip()
|
||
if not reason:
|
||
return payroll_reason_action_response(
|
||
self,
|
||
request,
|
||
queryset,
|
||
action_name="return_selected",
|
||
title="Возврат расчёта на исправление",
|
||
error="Укажите причину возврата.",
|
||
)
|
||
for run in queryset:
|
||
try:
|
||
return_run_for_correction(
|
||
run.pk,
|
||
actor=request.user,
|
||
reason=reason,
|
||
)
|
||
except PayrollOperationError as exc:
|
||
self.message_user(
|
||
request,
|
||
f"{run}: {exc.message} ({exc.code})",
|
||
level=messages.ERROR,
|
||
)
|
||
|
||
@admin.action(description="Утвердить расчёт")
|
||
def approve_selected(self, request, queryset):
|
||
self._run_action(request, queryset, approve_run)
|
||
|
||
@admin.action(description="Опубликовать расчётные листки")
|
||
def publish_selected(self, request, queryset):
|
||
self._run_action(request, queryset, publish_run)
|
||
|
||
|
||
@admin.register(PayrollStatement)
|
||
class PayrollStatementAdmin(PayrollSensitiveAdminMixin, admin.ModelAdmin):
|
||
view_permissions = ("finance.view_all_payroll",)
|
||
list_display = [
|
||
"run",
|
||
"employee",
|
||
"gross_total",
|
||
"deduction_total",
|
||
"payment_total",
|
||
"opening_balance",
|
||
"closing_balance",
|
||
"payable",
|
||
]
|
||
list_filter = ["run__period", "run__status", "currency"]
|
||
search_fields = [
|
||
"employee__last_name",
|
||
"employee__first_name",
|
||
"employee__email",
|
||
"result_hash",
|
||
]
|
||
readonly_fields = [field.name for field in PayrollStatement._meta.concrete_fields]
|
||
inlines = [PayrollStatementLineInline]
|
||
|
||
def has_add_permission(self, request):
|
||
return request.user.is_superuser
|
||
|
||
def has_delete_permission(self, request, obj=None):
|
||
return request.user.is_superuser
|
||
|
||
def has_change_permission(self, request, obj=None):
|
||
return self._has_payroll_access(request)
|
||
|
||
|
||
@admin.register(PayrollStatementAcknowledgement)
|
||
class PayrollStatementAcknowledgementAdmin(
|
||
PayrollSensitiveAdminMixin,
|
||
admin.ModelAdmin,
|
||
):
|
||
view_permissions = (
|
||
"finance.view_all_payroll",
|
||
"finance.audit_payroll",
|
||
)
|
||
list_display = [
|
||
"statement",
|
||
"employee",
|
||
"viewed_at",
|
||
"acknowledged_at",
|
||
"disputed_at",
|
||
]
|
||
list_filter = ["acknowledged_at", "disputed_at"]
|
||
search_fields = [
|
||
"employee__last_name",
|
||
"employee__first_name",
|
||
"statement__result_hash",
|
||
]
|
||
readonly_fields = [
|
||
field.name for field in PayrollStatementAcknowledgement._meta.concrete_fields
|
||
]
|
||
|
||
def has_add_permission(self, request):
|
||
return request.user.is_superuser
|
||
|
||
def has_change_permission(self, request, obj=None):
|
||
return self._has_payroll_access(request)
|
||
|
||
def has_delete_permission(self, request, obj=None):
|
||
return request.user.is_superuser
|
||
|
||
|
||
@admin.register(PayrollAuditEvent)
|
||
class PayrollAuditEventAdmin(PayrollSensitiveAdminMixin, admin.ModelAdmin):
|
||
view_permissions = ("finance.audit_payroll",)
|
||
list_display = [
|
||
"created_at",
|
||
"actor",
|
||
"action",
|
||
"object_type",
|
||
"object_id",
|
||
"period",
|
||
]
|
||
list_filter = ["action", "object_type", "period"]
|
||
search_fields = ["object_id", "before_hash", "after_hash"]
|
||
readonly_fields = [field.name for field in PayrollAuditEvent._meta.concrete_fields]
|
||
|
||
def has_add_permission(self, request):
|
||
return request.user.is_superuser
|
||
|
||
def has_change_permission(self, request, obj=None):
|
||
return has_payroll_permission(request.user, "finance.audit_payroll")
|
||
|
||
def has_delete_permission(self, request, obj=None):
|
||
return request.user.is_superuser
|