Corp/backend/api/v1/finance/serializers.py
Игорь Надеин 3da3100328
All checks were successful
Deploy production / deploy (push) Successful in 11m10s
Separate salary participation and preserve historical carryover rules
2026-10-15 19:37:51 +03:00

279 lines
8.6 KiB
Python

from decimal import Decimal
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from finance.models import (
PayrollDailyWorkEntry,
PayrollStatement,
PayrollStatementAcknowledgement,
PayrollStatementLine,
PayrollStatementPaymentConfirmation,
PayrollWorkRecord,
)
class PayrollStatementLineSerializer(serializers.ModelSerializer):
include_in_salary = serializers.SerializerMethodField()
def get_include_in_salary(self, obj) -> bool:
return self.context.get("salary_flags", {}).get(obj.line_id, True)
class Meta:
model = PayrollStatementLine
fields = [
"code",
"label",
"kind",
"amount",
"source_period_from",
"source_period_to",
"is_retro",
"calculated",
"include_in_salary",
]
class PayrollStatementAcknowledgementSerializer(serializers.ModelSerializer):
class Meta:
model = PayrollStatementAcknowledgement
fields = ["viewed_at", "acknowledged_at", "disputed_at"]
class PayrollConfirmedBySerializer(serializers.Serializer):
id = serializers.IntegerField()
display_name = serializers.CharField()
class PayrollStatementPaymentConfirmationSerializer(serializers.ModelSerializer):
confirmed_by = serializers.SerializerMethodField()
class Meta:
model = PayrollStatementPaymentConfirmation
fields = ["confirmed_at", "confirmed_by"]
read_only_fields = fields
@extend_schema_field(PayrollConfirmedBySerializer)
def get_confirmed_by(self, obj) -> dict[str, object]:
user = obj.confirmed_by
return {
"id": user.pk,
"display_name": user.get_full_name().strip() or f"Сотрудник #{user.pk}",
}
class PayrollPeriodPayloadSerializer(serializers.Serializer):
code = serializers.CharField()
name = serializers.CharField()
date_from = serializers.DateField()
date_to = serializers.DateField()
pay_date = serializers.DateField(allow_null=True)
class OwnPayrollPaymentConfirmationWriteSerializer(serializers.Serializer):
confirmed = serializers.BooleanField()
def period_payload(statement):
period = statement.run.period
return {
"code": period.code,
"name": period.name,
"date_from": period.date_from,
"date_to": period.date_to,
"pay_date": period.pay_date,
}
def acknowledgement_payload(statement):
try:
acknowledgement = statement.acknowledgement
except PayrollStatementAcknowledgement.DoesNotExist:
return None
return PayrollStatementAcknowledgementSerializer(acknowledgement).data
def payment_confirmation_payload(statement):
try:
confirmation = statement.payment_confirmation
except PayrollStatementPaymentConfirmation.DoesNotExist:
return None
return PayrollStatementPaymentConfirmationSerializer(confirmation).data
class OwnPayrollStatementSummarySerializer(serializers.ModelSerializer):
"""List metadata only; detailed amounts/lines require the audited endpoint."""
period = serializers.SerializerMethodField()
revision = serializers.IntegerField(source="run.revision", read_only=True)
published_at = serializers.DateTimeField(
source="run.published_at",
read_only=True,
)
acknowledgement = serializers.SerializerMethodField()
payment_confirmation = serializers.SerializerMethodField()
can_confirm_payment = serializers.SerializerMethodField()
class Meta:
model = PayrollStatement
fields = [
"public_id",
"period",
"revision",
"published_at",
"currency",
"payable",
"acknowledgement",
"payment_confirmation",
"can_confirm_payment",
]
read_only_fields = fields
@extend_schema_field(PayrollPeriodPayloadSerializer)
def get_period(self, obj) -> dict[str, object]:
return period_payload(obj)
@extend_schema_field(PayrollStatementAcknowledgementSerializer(allow_null=True))
def get_acknowledgement(self, obj) -> dict[str, object] | None:
return acknowledgement_payload(obj)
@extend_schema_field(PayrollStatementPaymentConfirmationSerializer(allow_null=True))
def get_payment_confirmation(self, obj) -> dict[str, object] | None:
return payment_confirmation_payload(obj)
def get_can_confirm_payment(self, obj) -> bool:
return obj.run.period.status == "closed"
class OwnPayrollStatementSerializer(serializers.ModelSerializer):
non_salary_payment_total = serializers.SerializerMethodField()
def get_non_salary_payment_total(self, obj) -> str:
return str(
(obj.result_snapshot or {})
.get("totals", {})
.get("non_salary_payment_total", "0")
)
period = serializers.SerializerMethodField()
revision = serializers.IntegerField(source="run.revision", read_only=True)
published_at = serializers.DateTimeField(
source="run.published_at",
read_only=True,
)
lines = serializers.SerializerMethodField()
acknowledgement = serializers.SerializerMethodField()
payment_confirmation = serializers.SerializerMethodField()
can_confirm_payment = serializers.SerializerMethodField()
class Meta:
model = PayrollStatement
fields = [
"public_id",
"period",
"revision",
"published_at",
"currency",
"point_delta",
"gross_before_adjustments",
"adjustment_total",
"gross_total",
"deduction_total",
"net_pay",
"payment_total",
"non_salary_payment_total",
"payable",
"lines",
"acknowledgement",
"payment_confirmation",
"can_confirm_payment",
]
read_only_fields = fields
@extend_schema_field(PayrollPeriodPayloadSerializer)
def get_period(self, obj) -> dict[str, object]:
return period_payload(obj)
@extend_schema_field(PayrollStatementAcknowledgementSerializer(allow_null=True))
def get_acknowledgement(self, obj) -> dict[str, object] | None:
return acknowledgement_payload(obj)
@extend_schema_field(PayrollStatementPaymentConfirmationSerializer(allow_null=True))
def get_payment_confirmation(self, obj) -> dict[str, object] | None:
return payment_confirmation_payload(obj)
def get_can_confirm_payment(self, obj) -> bool:
return obj.run.period.status == "closed"
@extend_schema_field(PayrollStatementLineSerializer(many=True))
def get_lines(self, obj):
lines = obj.lines.filter(visible_to_employee=True)
salary_flags = {
line["line_id"]: line.get("include_in_salary", True)
for line in (obj.result_snapshot or {}).get("lines", [])
}
return PayrollStatementLineSerializer(
lines, many=True, context={**self.context, "salary_flags": salary_flags}
).data
class OwnPayrollWorkRecordSerializer(serializers.ModelSerializer):
status_label = serializers.CharField(source="get_status_display", read_only=True)
class Meta:
model = PayrollWorkRecord
fields = [
"id",
"period_id",
"target_points",
"target_points_overridden",
"actual_points",
"revision",
"status",
"status_label",
"lock_version",
"replaces_id",
"reason",
"source",
"created_at",
"updated_at",
"approved_at",
]
read_only_fields = fields
class OwnPayrollDailyWorkEntrySerializer(serializers.ModelSerializer):
class Meta:
model = PayrollDailyWorkEntry
fields = [
"id",
"period_id",
"work_date",
"target_points",
"actual_points",
"note",
"lock_version",
"created_at",
"updated_at",
]
read_only_fields = fields
class OwnPayrollDailyWorkEntryWriteSerializer(serializers.Serializer):
period_id = serializers.IntegerField(min_value=1)
work_date = serializers.DateField()
actual_points = serializers.DecimalField(
max_digits=19,
decimal_places=4,
min_value=Decimal("0"),
)
note = serializers.CharField(
allow_blank=True,
required=False,
trim_whitespace=True,
)
expected_lock_version = serializers.IntegerField(
min_value=0,
required=False,
)