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, )