890 lines
32 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Native, permission-gated payroll administration API.
The endpoints in this module are an HTTP adapter around the existing finance
models and payroll services. Generic Django model permissions never grant
access to payroll data.
"""
from __future__ import annotations
from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import IntegrityError, transaction
from django.db.models import Q
from django.http import Http404
from django.shortcuts import get_object_or_404
from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import status
from rest_framework.exceptions import ValidationError
from rest_framework.response import Response
from finance.enums import (
ApprovalStatus,
PayrollPeriodStatus,
)
from finance.models import (
EmployeePayRate,
PayrollAuditEvent,
PayrollInputLine,
PayrollInputCopyConfiguration,
PayrollPeriod,
PayrollWorkRecord,
)
from finance.payroll.access import (
has_simple_admin_access,
)
from finance.payroll.services import (
approve_input_line,
approve_pay_rate,
approve_work_record,
bulk_set_pay_rate,
bulk_set_point_rate,
bulk_set_target_points,
clear_component_input_lines,
pay_rates_relevant_to_period,
set_component_input_line_value,
)
from ..admin_serializers import (
ApprovalCommandSerializer,
BulkPayRateCommandSerializer,
BulkPointRateCommandSerializer,
BulkTargetPointsCommandSerializer,
ClearComponentCellCommandSerializer,
CopyPayrollInputLinesCommandSerializer,
EmployeePayRateRevisionSerializer,
EmployeePayRateSerializer,
EmployeePayRateWriteSerializer,
PayrollEmployeeMiniSerializer,
PayrollInputLineSerializer,
PayrollInputLineWriteSerializer,
PayrollWorkRecordRevisionSerializer,
PayrollWorkRecordSerializer,
PayrollWorkRecordWriteSerializer,
SetComponentCellCommandSerializer,
PayrollUserRefSerializer,
_validation_detail,
)
from .common import (
INPUT_VIEW_PERMISSIONS,
PAYROLL_PERMISSIONS,
PayrollAdminAPIView,
_audit_api_mutation,
_conflict,
_lock_period_accepting_draft_edits,
_optional_int,
_payroll_history_action_label,
logger,
)
from .input_copy import (
_apply_input_copy,
_input_copy_preview,
_validate_input_copy_periods,
)
class DraftCollectionView(PayrollAdminAPIView):
allowed_permissions = INPUT_VIEW_PERMISSIONS
model = None
read_serializer_class = None
write_serializer_class = None
audit_label = "input"
period_scoped = False
def get_serializer_class(self):
return self.read_serializer_class or super().get_serializer_class()
def get_queryset(self):
return self.model.objects.all()
def filter_queryset(self, request, queryset):
employee_id = _optional_int(request, "employee_id")
if employee_id is not None:
queryset = queryset.filter(employee_id=employee_id)
status_value = request.query_params.get("status")
if status_value:
queryset = queryset.filter(status=status_value)
search = request.query_params.get("search", "").strip()
if search:
queryset = queryset.filter(
Q(employee__last_name__icontains=search)
| Q(employee__first_name__icontains=search)
| Q(employee__patronymic__icontains=search)
)
return queryset
def get(self, request):
queryset = self.filter_queryset(request, self.get_queryset())
return Response(self.read_serializer_class(queryset, many=True).data)
def validate_create(self, validated_data):
if not self.period_scoped:
return
period = _lock_period_accepting_draft_edits(validated_data["period"].pk)
validated_data["period"] = period
@transaction.atomic
def post(self, request):
self.require_permission(request, PAYROLL_PERMISSIONS["manage_inputs"])
serializer = self.write_serializer_class(
data=request.data,
context={"actor": request.user},
)
serializer.is_valid(raise_exception=True)
self.validate_create(serializer.validated_data)
try:
instance = serializer.save()
except IntegrityError:
logger.exception(
"Database rejected payroll draft creation",
extra={
"event": "finance.admin_views.database.rejected.payroll.draft.creation",
"payroll_model": self.model._meta.label,
"payroll_actor_id": request.user.pk,
},
)
_conflict(
"DRAFT_SAVE_FAILED",
(
"Не удалось сохранить данные расчёта: база данных отклонила "
"запись. Обновите страницу и повторите попытку."
),
)
period = getattr(instance, "period", None)
_audit_api_mutation(
actor=request.user,
action=f"payroll.{self.audit_label}_draft_created",
instance=instance,
period=period,
metadata={"lock_version": instance.lock_version},
)
instance = self.get_queryset().get(pk=instance.pk)
return Response(
self.read_serializer_class(instance).data,
status=status.HTTP_201_CREATED,
)
class PayrollInputHistoryView(PayrollAdminAPIView):
allowed_permissions = INPUT_VIEW_PERMISSIONS
def get(self, request):
period_id = _optional_int(request, "period_id")
if period_id is None:
raise ValidationError({"period_id": "Укажите расчётный период."})
period = get_object_or_404(PayrollPeriod, pk=period_id)
kind = request.query_params.get("kind", "").strip()
if kind == "rates":
records = pay_rates_relevant_to_period(
period,
queryset=EmployeePayRate.objects.select_related(
"employee",
"employee__position",
).prefetch_related("employee__departments_links__department"),
)
object_type = EmployeePayRate._meta.label_lower
subjects = {record.pk: "Ставка" for record in records}
elif kind == "work":
records = (
PayrollWorkRecord.objects.filter(period=period)
.select_related(
"employee",
"employee__position",
)
.prefetch_related("employee__departments_links__department")
)
object_type = PayrollWorkRecord._meta.label_lower
subjects = {record.pk: "Выработка" for record in records}
elif kind == "inputs":
records = (
PayrollInputLine.objects.filter(period=period)
.select_related(
"employee",
"employee__position",
)
.prefetch_related("employee__departments_links__department")
)
object_type = PayrollInputLine._meta.label_lower
subjects = {record.pk: record.component.name for record in records}
else:
raise ValidationError(
{"kind": "Допустимые значения: rates, work или inputs."}
)
records_by_id = {record.pk: record for record in records}
events = (
PayrollAuditEvent.objects.filter(
object_type=object_type,
object_id__in=[str(record_id) for record_id in records_by_id],
)
.select_related("actor")
.order_by("-created_at", "-id")
)
result = []
for event in events:
try:
record_id = int(event.object_id)
except (TypeError, ValueError):
continue
record = records_by_id.get(record_id)
if record is None:
continue
result.append(
{
"id": event.pk,
"record_id": record_id,
"action": event.action,
"action_label": _payroll_history_action_label(event.action),
"subject": subjects[record_id],
"employee": PayrollEmployeeMiniSerializer(record.employee).data,
"actor": (
PayrollUserRefSerializer(event.actor).data
if event.actor is not None
else None
),
"created_at": event.created_at,
}
)
return Response(result)
class DraftDetailView(PayrollAdminAPIView):
allowed_permissions = INPUT_VIEW_PERMISSIONS
model = None
read_serializer_class = None
write_serializer_class = None
def get_serializer_class(self):
return self.read_serializer_class or super().get_serializer_class()
audit_label = "input"
period_scoped = False
immutable_patch_fields = ()
def get_queryset(self):
return self.model.objects.all()
def get(self, request, pk):
instance = get_object_or_404(self.get_queryset(), pk=pk)
return Response(self.read_serializer_class(instance).data)
@transaction.atomic
def patch(self, request, pk):
self.require_permission(request, PAYROLL_PERMISSIONS["manage_inputs"])
forbidden_fields = set(request.data) & set(self.immutable_patch_fields)
if forbidden_fields:
raise ValidationError(
{
field_name: "Поле нельзя менять после создания черновика."
for field_name in sorted(forbidden_fields)
}
)
# Do not reveal whether an out-of-scope or immutable payroll record exists.
scoped_queryset = self.get_queryset().filter(
pk=pk,
status=ApprovalStatus.DRAFT,
)
if not has_simple_admin_access(request.user):
scoped_queryset = scoped_queryset.filter(created_by=request.user)
if self.period_scoped:
reference_queryset = self.model.objects.only("period_id").filter(
pk=pk,
status=ApprovalStatus.DRAFT,
)
if not has_simple_admin_access(request.user):
reference_queryset = reference_queryset.filter(created_by=request.user)
reference = get_object_or_404(reference_queryset)
period = _lock_period_accepting_draft_edits(reference.period_id)
else:
period = None
# Read serializers join nullable audit/revision relations. PostgreSQL
# cannot apply FOR UPDATE to the nullable side of those outer joins,
# and only the draft itself needs to be locked here.
instance = get_object_or_404(scoped_queryset.select_for_update(of=("self",)))
if period is not None:
instance._state.fields_cache["period"] = period
self.validate_period_update(instance, period)
serializer = self.write_serializer_class(
instance,
data=request.data,
partial=True,
context={"actor": request.user},
)
serializer.is_valid(raise_exception=True)
expected_lock_version = serializer.validated_data.get("expected_lock_version")
if instance.lock_version != expected_lock_version:
_conflict(
"STALE_DRAFT",
"Черновик уже изменён; обновите данные.",
expected_lock_version=expected_lock_version,
actual_lock_version=instance.lock_version,
)
try:
instance = serializer.save()
except IntegrityError:
_conflict(
"DRAFT_CONFLICT",
(
"Не удалось сохранить изменения: база данных отклонила "
"запись. Обновите данные и повторите попытку."
),
)
_audit_api_mutation(
actor=request.user,
action=f"payroll.{self.audit_label}_draft_updated",
instance=instance,
period=getattr(instance, "period", None),
metadata={"lock_version": instance.lock_version},
)
instance = self.get_queryset().get(pk=instance.pk)
return Response(self.read_serializer_class(instance).data)
def validate_period_update(self, instance, period):
return None
@extend_schema_view(
get=extend_schema(operation_id="finance_payroll_admin_pay_rates_list")
)
class EmployeePayRateListCreateView(DraftCollectionView):
model = EmployeePayRate
read_serializer_class = EmployeePayRateSerializer
write_serializer_class = EmployeePayRateWriteSerializer
audit_label = "rate"
def get_queryset(self):
return (
EmployeePayRate.objects.select_related(
"employee",
"employee__position",
"created_by",
"approved_by",
)
.prefetch_related("employee__departments_links__department")
.order_by("-created_at", "-id")
)
def filter_queryset(self, request, queryset):
queryset = super().filter_queryset(request, queryset)
period_id = _optional_int(request, "period_id")
if period_id is None:
return queryset
period = get_object_or_404(PayrollPeriod, pk=period_id)
return pay_rates_relevant_to_period(period, queryset=queryset)
@extend_schema_view(
get=extend_schema(operation_id="finance_payroll_admin_pay_rates_retrieve")
)
class EmployeePayRateDetailView(DraftDetailView):
model = EmployeePayRate
read_serializer_class = EmployeePayRateSerializer
write_serializer_class = EmployeePayRateWriteSerializer
audit_label = "rate"
immutable_patch_fields = ("employee_id", "rate_code", "currency")
def get_queryset(self):
return EmployeePayRate.objects.select_related(
"employee",
"employee__position",
"created_by",
"approved_by",
).prefetch_related("employee__departments_links__department")
class PayrollBulkPointRateView(PayrollAdminAPIView):
allowed_permissions = (PAYROLL_PERMISSIONS["manage_inputs"],)
def post(self, request, pk):
get_object_or_404(PayrollPeriod.objects.only("id"), pk=pk)
command = BulkPointRateCommandSerializer(data=request.data)
command.is_valid(raise_exception=True)
result = bulk_set_point_rate(
pk,
actor=request.user,
**command.validated_data,
)
return Response(
{
"mode": result["mode"],
"point_rate": (
f"{result['point_rate']:.4f}"
if result["point_rate"] is not None
else None
),
"summary": result["summary"],
}
)
class PayrollBulkPayRateView(PayrollAdminAPIView):
allowed_permissions = (PAYROLL_PERMISSIONS["manage_inputs"],)
def post(self, request, pk):
get_object_or_404(PayrollPeriod.objects.only("id"), pk=pk)
command = BulkPayRateCommandSerializer(data=request.data)
command.is_valid(raise_exception=True)
result = bulk_set_pay_rate(
pk,
actor=request.user,
**command.validated_data,
)
return Response(
{
"amount": f"{result['amount']:.4f}",
"effective_from": result["effective_from"].isoformat(),
"summary": result["summary"],
}
)
class PayrollBulkTargetPointsView(PayrollAdminAPIView):
allowed_permissions = (PAYROLL_PERMISSIONS["manage_inputs"],)
def post(self, request, pk):
get_object_or_404(PayrollPeriod.objects.only("id"), pk=pk)
command = BulkTargetPointsCommandSerializer(data=request.data)
command.is_valid(raise_exception=True)
result = bulk_set_target_points(
pk,
actor=request.user,
**command.validated_data,
)
return Response(
{
"mode": result["mode"],
"target_points": (
f"{result['target_points']:.4f}"
if result["target_points"] is not None
else None
),
"summary": result["summary"],
}
)
@extend_schema_view(
get=extend_schema(operation_id="finance_payroll_admin_work_records_list")
)
class PayrollWorkRecordListCreateView(DraftCollectionView):
model = PayrollWorkRecord
read_serializer_class = PayrollWorkRecordSerializer
write_serializer_class = PayrollWorkRecordWriteSerializer
audit_label = "work_record"
period_scoped = True
def validate_create(self, validated_data):
super().validate_create(validated_data)
if validated_data["period"].status == PayrollPeriodStatus.PUBLISHED:
_conflict(
"PUBLISHED_WORK_REPLACEMENT_REQUIRED",
"После публикации показатели меняются только новой ревизией.",
)
def get_queryset(self):
return (
PayrollWorkRecord.objects.select_related(
"period",
"employee",
"employee__position",
"created_by",
"approved_by",
)
.prefetch_related("employee__departments_links__department")
.order_by("-created_at", "-id")
)
def filter_queryset(self, request, queryset):
queryset = super().filter_queryset(request, queryset)
period_id = _optional_int(request, "period_id")
return queryset.filter(period_id=period_id) if period_id else queryset
@extend_schema_view(
get=extend_schema(operation_id="finance_payroll_admin_work_records_retrieve")
)
class PayrollWorkRecordDetailView(DraftDetailView):
model = PayrollWorkRecord
read_serializer_class = PayrollWorkRecordSerializer
write_serializer_class = PayrollWorkRecordWriteSerializer
audit_label = "work_record"
period_scoped = True
immutable_patch_fields = ("period_id", "employee_id")
def validate_period_update(self, instance, period):
if period.status == PayrollPeriodStatus.PUBLISHED and not instance.replaces_id:
_conflict(
"PUBLISHED_WORK_REPLACEMENT_REQUIRED",
"После публикации показатели меняются только новой ревизией.",
)
def get_queryset(self):
return PayrollWorkRecord.objects.select_related(
"period",
"employee",
"employee__position",
"created_by",
"approved_by",
).prefetch_related("employee__departments_links__department")
@extend_schema_view(
get=extend_schema(operation_id="finance_payroll_admin_input_lines_list")
)
class PayrollInputLineListCreateView(DraftCollectionView):
model = PayrollInputLine
read_serializer_class = PayrollInputLineSerializer
write_serializer_class = PayrollInputLineWriteSerializer
audit_label = "input_line"
period_scoped = True
def get_queryset(self):
return (
PayrollInputLine.objects.select_related(
"period",
"employee",
"employee__position",
"relates_to_period",
"created_by",
"approved_by",
)
.prefetch_related("employee__departments_links__department")
.order_by("-created_at", "-id")
)
def filter_queryset(self, request, queryset):
queryset = super().filter_queryset(request, queryset)
period_id = _optional_int(request, "period_id")
component_code = request.query_params.get("component_code", "").strip()
if period_id:
queryset = queryset.filter(period_id=period_id)
if component_code:
queryset = queryset.filter(component_code=component_code)
return queryset
@extend_schema_view(
get=extend_schema(operation_id="finance_payroll_admin_input_lines_retrieve")
)
class PayrollInputLineDetailView(DraftDetailView):
model = PayrollInputLine
read_serializer_class = PayrollInputLineSerializer
write_serializer_class = PayrollInputLineWriteSerializer
audit_label = "input_line"
period_scoped = True
immutable_patch_fields = ("period_id", "employee_id", "component_code")
def get_queryset(self):
return PayrollInputLine.objects.select_related(
"period",
"employee",
"employee__position",
"relates_to_period",
"created_by",
"approved_by",
).prefetch_related("employee__departments_links__department")
class PayrollComponentCellClearView(PayrollAdminAPIView):
allowed_permissions = (PAYROLL_PERMISSIONS["manage_inputs"],)
def post(self, request, pk):
command = ClearComponentCellCommandSerializer(data=request.data)
command.is_valid(raise_exception=True)
summary = clear_component_input_lines(
pk,
employee_id=command.validated_data["employee_id"],
component_code=command.validated_data["component_code"],
actor=request.user,
)
return Response(summary)
class PayrollComponentCellSetView(PayrollAdminAPIView):
allowed_permissions = (PAYROLL_PERMISSIONS["manage_inputs"],)
def post(self, request, pk):
command = SetComponentCellCommandSerializer(data=request.data)
command.is_valid(raise_exception=True)
line = set_component_input_line_value(
pk,
employee_id=command.validated_data["employee_id"],
component_code=command.validated_data["component_code"],
amount=command.validated_data["amount"],
actor=request.user,
)
return Response(
PayrollInputLineSerializer(line).data,
status=status.HTTP_201_CREATED,
)
class PayrollInputLinePeriodCopyView(PayrollAdminAPIView):
allowed_permissions = (PAYROLL_PERMISSIONS["manage_inputs"],)
def get(self, request, pk):
target_period = get_object_or_404(PayrollPeriod, pk=pk)
source_period_id = _optional_int(request, "source_period_id")
if source_period_id is None:
raise ValidationError({"source_period_id": "Выберите период-источник."})
source_period = get_object_or_404(PayrollPeriod, pk=source_period_id)
return Response(
_input_copy_preview(
target_period,
source_period,
actor=request.user,
)
)
@transaction.atomic
def post(self, request, pk):
command = CopyPayrollInputLinesCommandSerializer(data=request.data)
command.is_valid(raise_exception=True)
data = command.validated_data
target_period = _lock_period_accepting_draft_edits(pk)
if target_period.lock_version != data["expected_period_lock_version"]:
_conflict(
"STALE_PERIOD",
"Период уже изменён; обновите данные и повторите перенос.",
expected_lock_version=data["expected_period_lock_version"],
actual_lock_version=target_period.lock_version,
)
source_period = get_object_or_404(
PayrollPeriod.objects.select_for_update(of=("self",)),
pk=data["source_period_id"],
)
_validate_input_copy_periods(target_period, source_period)
if data["remember_configuration"]:
PayrollInputCopyConfiguration.objects.update_or_create(
owner=request.user,
defaults={
"component_codes": data["component_codes"],
"employee_scope": data["employee_scope"],
"employee_ids": data["employee_ids"],
"existing_mode": data["existing_mode"],
"auto_apply_on_period_creation": data[
"auto_apply_on_period_creation"
],
},
)
else:
PayrollInputCopyConfiguration.objects.filter(owner=request.user).delete()
result = _apply_input_copy(
target_period=target_period,
source_period=source_period,
actor=request.user,
data=data,
)
return Response(
result,
status=(
status.HTTP_200_OK
if result["idempotent_replay"]
else status.HTTP_201_CREATED
),
)
class DraftApprovalView(PayrollAdminAPIView):
allowed_permissions = (PAYROLL_PERMISSIONS["approve_inputs"],)
service = None
queryset = None
read_serializer_class = None
def get_serializer_class(self):
return self.read_serializer_class or super().get_serializer_class()
def post(self, request, pk):
serializer = ApprovalCommandSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
if not self.queryset.filter(pk=pk).exists():
raise Http404
instance = self.service(
pk,
actor=request.user,
expected_lock_version=serializer.validated_data["expected_lock_version"],
)
instance = self.queryset.get(pk=instance.pk)
return Response(self.read_serializer_class(instance).data)
class EmployeePayRateApproveView(DraftApprovalView):
service = staticmethod(approve_pay_rate)
queryset = EmployeePayRate.objects.select_related(
"employee", "employee__position", "created_by", "approved_by"
).prefetch_related("employee__departments_links__department")
read_serializer_class = EmployeePayRateSerializer
class PayrollWorkRecordApproveView(DraftApprovalView):
service = staticmethod(approve_work_record)
queryset = PayrollWorkRecord.objects.select_related(
"period", "employee", "employee__position", "created_by", "approved_by"
).prefetch_related("employee__departments_links__department")
read_serializer_class = PayrollWorkRecordSerializer
class PayrollInputLineApproveView(DraftApprovalView):
service = staticmethod(approve_input_line)
queryset = PayrollInputLine.objects.select_related(
"period",
"employee",
"employee__position",
"created_by",
"approved_by",
).prefetch_related("employee__departments_links__department")
read_serializer_class = PayrollInputLineSerializer
class EmployeePayRateReviseView(PayrollAdminAPIView):
serializer_class = EmployeePayRateSerializer
allowed_permissions = INPUT_VIEW_PERMISSIONS
@transaction.atomic
def post(self, request, pk):
self.require_permission(request, PAYROLL_PERMISSIONS["manage_inputs"])
source = get_object_or_404(
EmployeePayRate.objects.select_for_update(of=("self",)).select_related(
"employee"
),
pk=pk,
status=ApprovalStatus.APPROVED,
)
if EmployeePayRate.objects.filter(replaces=source).exists():
_conflict(
"REVISION_ALREADY_EXISTS",
"Для этой ставки уже создана следующая ревизия.",
)
command = EmployeePayRateRevisionSerializer(data=request.data)
command.is_valid(raise_exception=True)
values = {
"amount": source.amount,
"point_rate": source.point_rate,
"currency": source.currency,
**command.validated_data,
}
revision = EmployeePayRate(
employee=source.employee,
rate_code=source.rate_code,
effective_from=source.effective_from,
revision=source.revision + 1,
replaces=source,
status=ApprovalStatus.DRAFT,
source="manual",
created_by=request.user,
**values,
)
try:
revision.full_clean()
revision.save(force_insert=True)
except DjangoValidationError as exc:
raise ValidationError(_validation_detail(exc)) from exc
except IntegrityError:
_conflict(
"REVISION_CONFLICT",
"Не удалось создать новую версию ставки. Обновите данные и повторите попытку.",
)
_audit_api_mutation(
actor=request.user,
action="payroll.rate_revision_draft_created",
instance=revision,
metadata={"replaces_id": source.pk, "revision": revision.revision},
)
revision = (
EmployeePayRate.objects.select_related(
"employee", "employee__position", "created_by", "approved_by"
)
.prefetch_related("employee__departments_links__department")
.get(pk=revision.pk)
)
return Response(
EmployeePayRateSerializer(revision).data,
status=status.HTTP_201_CREATED,
)
class PayrollWorkRecordReviseView(PayrollAdminAPIView):
serializer_class = PayrollWorkRecordSerializer
allowed_permissions = INPUT_VIEW_PERMISSIONS
@transaction.atomic
def post(self, request, pk):
self.require_permission(request, PAYROLL_PERMISSIONS["manage_inputs"])
reference = get_object_or_404(
PayrollWorkRecord.objects.only("period_id"),
pk=pk,
status=ApprovalStatus.APPROVED,
)
period = _lock_period_accepting_draft_edits(reference.period_id)
source = get_object_or_404(
PayrollWorkRecord.objects.select_for_update(of=("self",)).select_related(
"period", "employee"
),
pk=pk,
status=ApprovalStatus.APPROVED,
)
source._state.fields_cache["period"] = period
if PayrollWorkRecord.objects.filter(replaces=source).exists():
_conflict(
"REVISION_ALREADY_EXISTS",
"Для показателей уже создана следующая ревизия.",
)
command = PayrollWorkRecordRevisionSerializer(data=request.data)
command.is_valid(raise_exception=True)
values = {
"target_points": source.target_points,
"target_points_overridden": source.target_points_overridden,
"actual_points": source.actual_points,
"expected_point_amount": source.expected_point_amount,
"expected_gross": source.expected_gross,
"expected_recalculated_gross": source.expected_recalculated_gross,
"expected_payable": source.expected_payable,
**command.validated_data,
}
if "target_points" in command.validated_data:
values["target_points_overridden"] = True
revision = PayrollWorkRecord(
period=source.period,
employee=source.employee,
revision=source.revision + 1,
replaces=source,
status=ApprovalStatus.DRAFT,
source="manual",
created_by=request.user,
**values,
)
try:
revision.full_clean()
revision.save(force_insert=True)
except DjangoValidationError as exc:
raise ValidationError(_validation_detail(exc)) from exc
except IntegrityError:
_conflict(
"REVISION_CONFLICT",
"Не удалось создать новую версию показателей. Обновите данные и повторите попытку.",
)
_audit_api_mutation(
actor=request.user,
action="payroll.work_record_revision_draft_created",
instance=revision,
period=revision.period,
metadata={"replaces_id": source.pk, "revision": revision.revision},
)
revision = (
PayrollWorkRecord.objects.select_related(
"period",
"employee",
"employee__position",
"created_by",
"approved_by",
)
.prefetch_related("employee__departments_links__department")
.get(pk=revision.pk)
)
return Response(
PayrollWorkRecordSerializer(revision).data,
status=status.HTTP_201_CREATED,
)