706 lines
22 KiB
Python
706 lines
22 KiB
Python
"""Serializers for the native payroll administration workspace.
|
|
|
|
Workflow fields, hashes, source references and idempotency keys intentionally
|
|
never form part of a writable/read payload here. State transitions are owned
|
|
by :mod:`finance.payroll.services`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from decimal import Decimal
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
|
from rest_framework import serializers
|
|
|
|
from finance.enums import ApprovalStatus, InputSource
|
|
from finance.models import (
|
|
EmployeePayRate,
|
|
PayrollComponent,
|
|
PayrollInputLine,
|
|
PayrollPeriod,
|
|
PayrollRun,
|
|
PayrollWorkRecord,
|
|
)
|
|
from finance.payroll.work_norm import calculate_period_target_points
|
|
|
|
Employee = get_user_model()
|
|
|
|
|
|
def _validation_detail(exc: DjangoValidationError):
|
|
if hasattr(exc, "message_dict"):
|
|
return exc.message_dict
|
|
return {"non_field_errors": exc.messages}
|
|
|
|
|
|
class PayrollEmployeeMiniSerializer(serializers.ModelSerializer):
|
|
display_name = serializers.SerializerMethodField()
|
|
position = serializers.SerializerMethodField()
|
|
department = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = Employee
|
|
fields = ["id", "display_name", "position", "department"]
|
|
read_only_fields = fields
|
|
|
|
def get_display_name(self, obj):
|
|
return obj.get_full_name().strip() or f"Сотрудник #{obj.pk}"
|
|
|
|
def get_position(self, obj):
|
|
position = getattr(obj, "position", None)
|
|
return position.name if position is not None else None
|
|
|
|
def get_department(self, obj):
|
|
prefetched = getattr(obj, "_prefetched_objects_cache", {}).get(
|
|
"departments_links"
|
|
)
|
|
if prefetched is None:
|
|
link = (
|
|
obj.departments_links.filter(is_active=True)
|
|
.select_related("department")
|
|
.order_by("department__name", "id")
|
|
.first()
|
|
)
|
|
else:
|
|
link = next(
|
|
(
|
|
item
|
|
for item in sorted(
|
|
prefetched,
|
|
key=lambda item: (item.department.name, item.pk),
|
|
)
|
|
if item.is_active
|
|
),
|
|
None,
|
|
)
|
|
return link.department.name if link is not None else None
|
|
|
|
|
|
class PayrollUserRefSerializer(serializers.ModelSerializer):
|
|
display_name = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = Employee
|
|
fields = ["id", "display_name"]
|
|
read_only_fields = fields
|
|
|
|
def get_display_name(self, obj):
|
|
return obj.get_full_name().strip() or f"Сотрудник #{obj.pk}"
|
|
|
|
|
|
class PayrollComponentSerializer(serializers.ModelSerializer):
|
|
kind_label = serializers.CharField(source="get_kind_display", read_only=True)
|
|
|
|
class Meta:
|
|
model = PayrollComponent
|
|
fields = [
|
|
"id",
|
|
"code",
|
|
"name",
|
|
"kind",
|
|
"kind_label",
|
|
"requires_reason",
|
|
"is_active",
|
|
"display_order",
|
|
]
|
|
read_only_fields = fields
|
|
|
|
|
|
class PayrollPeriodSerializer(serializers.ModelSerializer):
|
|
status_label = serializers.CharField(source="get_status_display", read_only=True)
|
|
current_run_id = serializers.IntegerField(read_only=True, allow_null=True)
|
|
created_by_id = serializers.IntegerField(read_only=True)
|
|
|
|
class Meta:
|
|
model = PayrollPeriod
|
|
fields = [
|
|
"id",
|
|
"code",
|
|
"name",
|
|
"date_from",
|
|
"date_to",
|
|
"pay_date",
|
|
"currency",
|
|
"status",
|
|
"status_label",
|
|
"current_run_id",
|
|
"lock_version",
|
|
"created_by_id",
|
|
"created_at",
|
|
"updated_at",
|
|
]
|
|
read_only_fields = fields
|
|
|
|
|
|
class PayrollPeriodWriteSerializer(serializers.ModelSerializer):
|
|
expected_lock_version = serializers.IntegerField(
|
|
min_value=0,
|
|
required=False,
|
|
write_only=True,
|
|
)
|
|
|
|
class Meta:
|
|
model = PayrollPeriod
|
|
fields = [
|
|
"code",
|
|
"name",
|
|
"date_from",
|
|
"date_to",
|
|
"pay_date",
|
|
"currency",
|
|
"expected_lock_version",
|
|
]
|
|
|
|
def validate(self, attrs):
|
|
if self.instance is not None and "expected_lock_version" not in attrs:
|
|
raise serializers.ValidationError(
|
|
{"expected_lock_version": "Укажите открытую версию периода."}
|
|
)
|
|
return attrs
|
|
|
|
|
|
class DraftWriteSerializerMixin:
|
|
"""Create manual drafts and update them through model optimistic locking."""
|
|
|
|
expected_lock_version = serializers.IntegerField(
|
|
min_value=0,
|
|
required=False,
|
|
write_only=True,
|
|
)
|
|
|
|
def validate(self, attrs):
|
|
attrs = super().validate(attrs)
|
|
if self.instance is not None and "expected_lock_version" not in attrs:
|
|
raise serializers.ValidationError(
|
|
{"expected_lock_version": "Укажите открытую версию черновика."}
|
|
)
|
|
return attrs
|
|
|
|
def create(self, validated_data):
|
|
validated_data.pop("expected_lock_version", None)
|
|
instance = self.Meta.model(
|
|
**validated_data,
|
|
status=ApprovalStatus.DRAFT,
|
|
source=InputSource.MANUAL,
|
|
created_by=self.context["actor"],
|
|
)
|
|
try:
|
|
instance.full_clean()
|
|
except DjangoValidationError as exc:
|
|
raise serializers.ValidationError(_validation_detail(exc)) from exc
|
|
instance.save(force_insert=True)
|
|
return instance
|
|
|
|
def update(self, instance, validated_data):
|
|
expected_lock_version = validated_data.pop("expected_lock_version")
|
|
for field_name, value in validated_data.items():
|
|
setattr(instance, field_name, value)
|
|
instance._expected_lock_version = expected_lock_version
|
|
try:
|
|
instance.full_clean()
|
|
instance.save()
|
|
except DjangoValidationError as exc:
|
|
raise serializers.ValidationError(_validation_detail(exc)) from exc
|
|
return instance
|
|
|
|
|
|
class EmployeePayRateSerializer(serializers.ModelSerializer):
|
|
employee = PayrollEmployeeMiniSerializer(read_only=True)
|
|
employee_id = serializers.IntegerField(read_only=True)
|
|
status_label = serializers.CharField(source="get_status_display", read_only=True)
|
|
created_by_id = serializers.IntegerField(read_only=True)
|
|
approved_by_id = serializers.IntegerField(read_only=True, allow_null=True)
|
|
voided_by_id = serializers.IntegerField(read_only=True, allow_null=True)
|
|
replaces_id = serializers.IntegerField(read_only=True, allow_null=True)
|
|
created_by = PayrollUserRefSerializer(read_only=True)
|
|
approved_by = PayrollUserRefSerializer(read_only=True)
|
|
|
|
class Meta:
|
|
model = EmployeePayRate
|
|
fields = [
|
|
"id",
|
|
"employee_id",
|
|
"employee",
|
|
"rate_code",
|
|
"amount",
|
|
"point_rate",
|
|
"currency",
|
|
"effective_from",
|
|
"revision",
|
|
"status",
|
|
"status_label",
|
|
"lock_version",
|
|
"replaces_id",
|
|
"reason",
|
|
"source",
|
|
"created_by_id",
|
|
"created_by",
|
|
"created_at",
|
|
"approved_by_id",
|
|
"approved_by",
|
|
"approved_at",
|
|
"voided_by_id",
|
|
"voided_at",
|
|
]
|
|
read_only_fields = fields
|
|
|
|
|
|
class EmployeePayRateWriteSerializer(
|
|
DraftWriteSerializerMixin,
|
|
serializers.ModelSerializer,
|
|
):
|
|
expected_lock_version = DraftWriteSerializerMixin.expected_lock_version
|
|
employee_id = serializers.PrimaryKeyRelatedField(
|
|
source="employee",
|
|
queryset=Employee.objects.filter(is_active=True),
|
|
)
|
|
|
|
class Meta:
|
|
model = EmployeePayRate
|
|
fields = [
|
|
"employee_id",
|
|
"rate_code",
|
|
"amount",
|
|
"point_rate",
|
|
"currency",
|
|
"effective_from",
|
|
"reason",
|
|
"expected_lock_version",
|
|
]
|
|
|
|
|
|
class EmployeePayRateRevisionSerializer(serializers.Serializer):
|
|
amount = serializers.DecimalField(max_digits=19, decimal_places=4, required=False)
|
|
point_rate = serializers.DecimalField(
|
|
max_digits=19,
|
|
decimal_places=4,
|
|
min_value=0,
|
|
required=False,
|
|
allow_null=True,
|
|
)
|
|
currency = serializers.CharField(max_length=3, required=False)
|
|
reason = serializers.CharField(allow_blank=False, trim_whitespace=True)
|
|
|
|
|
|
class BulkPointRateCommandSerializer(serializers.Serializer):
|
|
MODE_FIXED = "fixed"
|
|
MODE_IN_NORM = "in_norm"
|
|
|
|
employee_ids = serializers.ListField(
|
|
child=serializers.IntegerField(min_value=1),
|
|
allow_empty=False,
|
|
)
|
|
mode = serializers.ChoiceField(
|
|
choices=(MODE_FIXED, MODE_IN_NORM),
|
|
required=False,
|
|
)
|
|
point_rate = serializers.DecimalField(
|
|
max_digits=19,
|
|
decimal_places=4,
|
|
min_value=0,
|
|
required=False,
|
|
allow_null=True,
|
|
)
|
|
reason = serializers.CharField(allow_blank=False, trim_whitespace=True)
|
|
|
|
def validate_employee_ids(self, value):
|
|
return list(dict.fromkeys(value))
|
|
|
|
def validate(self, attrs):
|
|
attrs = super().validate(attrs)
|
|
mode = attrs.get("mode")
|
|
if mode is None:
|
|
mode = (
|
|
self.MODE_FIXED
|
|
if attrs.get("point_rate") is not None
|
|
else self.MODE_IN_NORM
|
|
)
|
|
attrs["mode"] = mode
|
|
if mode == self.MODE_FIXED and attrs.get("point_rate") is None:
|
|
raise serializers.ValidationError(
|
|
{
|
|
"point_rate": "Укажите фиксированную цену или выберите автоматический расчёт."
|
|
}
|
|
)
|
|
if mode == self.MODE_IN_NORM:
|
|
attrs["point_rate"] = None
|
|
return attrs
|
|
|
|
|
|
class BulkPayRateCommandSerializer(serializers.Serializer):
|
|
employee_ids = serializers.ListField(
|
|
child=serializers.IntegerField(min_value=1),
|
|
allow_empty=False,
|
|
)
|
|
amount = serializers.DecimalField(
|
|
max_digits=19,
|
|
decimal_places=4,
|
|
min_value=Decimal("0.0001"),
|
|
)
|
|
effective_from = serializers.DateField()
|
|
reason = serializers.CharField(allow_blank=False, trim_whitespace=True)
|
|
|
|
def validate_employee_ids(self, value):
|
|
return list(dict.fromkeys(value))
|
|
|
|
|
|
class BulkTargetPointsCommandSerializer(serializers.Serializer):
|
|
MODE_FIXED = "fixed"
|
|
MODE_AUTOMATIC = "automatic"
|
|
|
|
employee_ids = serializers.ListField(
|
|
child=serializers.IntegerField(min_value=1),
|
|
allow_empty=False,
|
|
)
|
|
mode = serializers.ChoiceField(choices=(MODE_FIXED, MODE_AUTOMATIC))
|
|
target_points = serializers.DecimalField(
|
|
max_digits=19,
|
|
decimal_places=4,
|
|
min_value=Decimal("0.0001"),
|
|
required=False,
|
|
allow_null=True,
|
|
)
|
|
reason = serializers.CharField(allow_blank=False, trim_whitespace=True)
|
|
|
|
def validate_employee_ids(self, value):
|
|
return list(dict.fromkeys(value))
|
|
|
|
def validate(self, attrs):
|
|
attrs = super().validate(attrs)
|
|
if attrs["mode"] == self.MODE_FIXED and attrs.get("target_points") is None:
|
|
raise serializers.ValidationError(
|
|
{"target_points": "Укажите норму баллов."}
|
|
)
|
|
if attrs["mode"] == self.MODE_AUTOMATIC:
|
|
attrs["target_points"] = None
|
|
return attrs
|
|
|
|
|
|
class PayrollWorkRecordSerializer(serializers.ModelSerializer):
|
|
employee = PayrollEmployeeMiniSerializer(read_only=True)
|
|
employee_id = serializers.IntegerField(read_only=True)
|
|
period_id = serializers.IntegerField(read_only=True)
|
|
status_label = serializers.CharField(source="get_status_display", read_only=True)
|
|
created_by_id = serializers.IntegerField(read_only=True)
|
|
approved_by_id = serializers.IntegerField(read_only=True, allow_null=True)
|
|
voided_by_id = serializers.IntegerField(read_only=True, allow_null=True)
|
|
replaces_id = serializers.IntegerField(read_only=True, allow_null=True)
|
|
created_by = PayrollUserRefSerializer(read_only=True)
|
|
approved_by = PayrollUserRefSerializer(read_only=True)
|
|
|
|
class Meta:
|
|
model = PayrollWorkRecord
|
|
fields = [
|
|
"id",
|
|
"period_id",
|
|
"employee_id",
|
|
"employee",
|
|
"target_points",
|
|
"target_points_overridden",
|
|
"actual_points",
|
|
"expected_point_amount",
|
|
"expected_gross",
|
|
"expected_recalculated_gross",
|
|
"expected_payable",
|
|
"revision",
|
|
"status",
|
|
"status_label",
|
|
"lock_version",
|
|
"replaces_id",
|
|
"reason",
|
|
"source",
|
|
"created_by_id",
|
|
"created_by",
|
|
"created_at",
|
|
"updated_at",
|
|
"approved_by_id",
|
|
"approved_by",
|
|
"approved_at",
|
|
"voided_by_id",
|
|
"voided_at",
|
|
]
|
|
read_only_fields = fields
|
|
|
|
|
|
class PayrollWorkRecordWriteSerializer(
|
|
DraftWriteSerializerMixin,
|
|
serializers.ModelSerializer,
|
|
):
|
|
expected_lock_version = DraftWriteSerializerMixin.expected_lock_version
|
|
period_id = serializers.PrimaryKeyRelatedField(
|
|
source="period",
|
|
queryset=PayrollPeriod.objects.all(),
|
|
)
|
|
employee_id = serializers.PrimaryKeyRelatedField(
|
|
source="employee",
|
|
queryset=Employee.objects.filter(is_active=True),
|
|
)
|
|
target_points = serializers.DecimalField(
|
|
max_digits=19,
|
|
decimal_places=4,
|
|
min_value=Decimal("0.0001"),
|
|
required=False,
|
|
allow_null=True,
|
|
)
|
|
|
|
class Meta:
|
|
model = PayrollWorkRecord
|
|
fields = [
|
|
"period_id",
|
|
"employee_id",
|
|
"target_points",
|
|
"actual_points",
|
|
"expected_point_amount",
|
|
"expected_gross",
|
|
"expected_recalculated_gross",
|
|
"expected_payable",
|
|
"reason",
|
|
"expected_lock_version",
|
|
]
|
|
|
|
def validate(self, attrs):
|
|
attrs = super().validate(attrs)
|
|
target_was_sent = "target_points" in attrs
|
|
should_calculate = (self.instance is None and not target_was_sent) or attrs.get(
|
|
"target_points"
|
|
) is None
|
|
if should_calculate:
|
|
period = attrs.get("period") or self.instance.period
|
|
employee = attrs.get("employee") or self.instance.employee
|
|
target_points, _, _ = calculate_period_target_points(
|
|
period,
|
|
employee=employee,
|
|
)
|
|
attrs["target_points"] = target_points
|
|
attrs["target_points_overridden"] = False
|
|
elif target_was_sent:
|
|
attrs["target_points_overridden"] = True
|
|
return attrs
|
|
|
|
|
|
class PayrollWorkRecordRevisionSerializer(serializers.Serializer):
|
|
target_points = serializers.DecimalField(
|
|
max_digits=19,
|
|
decimal_places=4,
|
|
min_value=0,
|
|
required=False,
|
|
)
|
|
actual_points = serializers.DecimalField(
|
|
max_digits=19,
|
|
decimal_places=4,
|
|
min_value=0,
|
|
required=False,
|
|
)
|
|
expected_point_amount = serializers.DecimalField(
|
|
max_digits=19,
|
|
decimal_places=2,
|
|
allow_null=True,
|
|
required=False,
|
|
)
|
|
expected_gross = serializers.DecimalField(
|
|
max_digits=19,
|
|
decimal_places=2,
|
|
allow_null=True,
|
|
required=False,
|
|
)
|
|
expected_recalculated_gross = serializers.DecimalField(
|
|
max_digits=19,
|
|
decimal_places=2,
|
|
allow_null=True,
|
|
required=False,
|
|
)
|
|
expected_payable = serializers.DecimalField(
|
|
max_digits=19,
|
|
decimal_places=2,
|
|
allow_null=True,
|
|
required=False,
|
|
)
|
|
reason = serializers.CharField(allow_blank=False, trim_whitespace=True)
|
|
|
|
|
|
class PayrollInputLineSerializer(serializers.ModelSerializer):
|
|
employee = PayrollEmployeeMiniSerializer(read_only=True)
|
|
employee_id = serializers.IntegerField(read_only=True)
|
|
period_id = serializers.IntegerField(read_only=True)
|
|
component = PayrollComponentSerializer(read_only=True)
|
|
component_id = serializers.IntegerField(read_only=True)
|
|
relates_to_period_id = serializers.IntegerField(read_only=True, allow_null=True)
|
|
status_label = serializers.CharField(source="get_status_display", read_only=True)
|
|
created_by_id = serializers.IntegerField(read_only=True)
|
|
approved_by_id = serializers.IntegerField(read_only=True, allow_null=True)
|
|
voided_by_id = serializers.IntegerField(read_only=True, allow_null=True)
|
|
created_by = PayrollUserRefSerializer(read_only=True)
|
|
approved_by = PayrollUserRefSerializer(read_only=True)
|
|
|
|
class Meta:
|
|
model = PayrollInputLine
|
|
fields = [
|
|
"id",
|
|
"period_id",
|
|
"employee_id",
|
|
"employee",
|
|
"component_id",
|
|
"component",
|
|
"amount",
|
|
"relates_to_period_id",
|
|
"reason",
|
|
"status",
|
|
"status_label",
|
|
"lock_version",
|
|
"source",
|
|
"created_by_id",
|
|
"created_by",
|
|
"created_at",
|
|
"approved_by_id",
|
|
"approved_by",
|
|
"approved_at",
|
|
"voided_by_id",
|
|
"voided_at",
|
|
]
|
|
read_only_fields = fields
|
|
|
|
|
|
class PayrollInputLineWriteSerializer(
|
|
DraftWriteSerializerMixin,
|
|
serializers.ModelSerializer,
|
|
):
|
|
expected_lock_version = DraftWriteSerializerMixin.expected_lock_version
|
|
period_id = serializers.PrimaryKeyRelatedField(
|
|
source="period",
|
|
queryset=PayrollPeriod.objects.all(),
|
|
)
|
|
employee_id = serializers.PrimaryKeyRelatedField(
|
|
source="employee",
|
|
queryset=Employee.objects.filter(is_active=True),
|
|
)
|
|
component_id = serializers.PrimaryKeyRelatedField(
|
|
source="component",
|
|
queryset=PayrollComponent.objects.all(),
|
|
)
|
|
relates_to_period_id = serializers.PrimaryKeyRelatedField(
|
|
source="relates_to_period",
|
|
queryset=PayrollPeriod.objects.all(),
|
|
allow_null=True,
|
|
required=False,
|
|
)
|
|
|
|
class Meta:
|
|
model = PayrollInputLine
|
|
fields = [
|
|
"period_id",
|
|
"employee_id",
|
|
"component_id",
|
|
"amount",
|
|
"relates_to_period_id",
|
|
"reason",
|
|
"expected_lock_version",
|
|
]
|
|
|
|
|
|
class ApprovalCommandSerializer(serializers.Serializer):
|
|
expected_lock_version = serializers.IntegerField(min_value=0)
|
|
|
|
|
|
class ClearComponentCellCommandSerializer(serializers.Serializer):
|
|
employee_id = serializers.IntegerField(min_value=1)
|
|
component_id = serializers.IntegerField(min_value=1)
|
|
|
|
|
|
class CalculatePayrollCommandSerializer(serializers.Serializer):
|
|
expected_lock_version = serializers.IntegerField(min_value=0)
|
|
recalculation_reason = serializers.CharField(
|
|
allow_blank=True,
|
|
required=False,
|
|
default="",
|
|
trim_whitespace=True,
|
|
)
|
|
idempotency_key = serializers.UUIDField(required=False, allow_null=True)
|
|
|
|
|
|
class AttendanceWorkImportCommandSerializer(serializers.Serializer):
|
|
mode = serializers.ChoiceField(
|
|
choices=("missing_only", "replace_existing"),
|
|
)
|
|
preview_token = serializers.CharField(min_length=64, max_length=64)
|
|
expected_period_lock_version = serializers.IntegerField(min_value=0)
|
|
reason = serializers.CharField(
|
|
allow_blank=True,
|
|
required=False,
|
|
default="",
|
|
trim_whitespace=True,
|
|
)
|
|
|
|
def validate(self, attrs):
|
|
if attrs["mode"] == "replace_existing" and not attrs["reason"]:
|
|
raise serializers.ValidationError(
|
|
{"reason": "Для пересчёта существующих записей укажите причину."}
|
|
)
|
|
return attrs
|
|
|
|
|
|
class WorkbookWorkPreviewSerializer(serializers.Serializer):
|
|
file = serializers.FileField()
|
|
|
|
def validate_file(self, value):
|
|
if not value.name.lower().endswith(".xlsx"):
|
|
raise serializers.ValidationError("Выберите файл Excel в формате .xlsx.")
|
|
if value.size > 10 * 1024 * 1024:
|
|
raise serializers.ValidationError("Excel-файл должен быть не больше 10 МБ.")
|
|
return value
|
|
|
|
|
|
class WorkbookWorkImportSerializer(WorkbookWorkPreviewSerializer):
|
|
mode = serializers.ChoiceField(choices=("skip_existing", "replace_existing"))
|
|
mappings = serializers.CharField(required=False, default="{}")
|
|
expected_file_hash = serializers.RegexField(regex=r"^[0-9a-f]{64}$")
|
|
expected_period_lock_version = serializers.IntegerField(min_value=0)
|
|
|
|
|
|
class ReturnPayrollCommandSerializer(serializers.Serializer):
|
|
reason = serializers.CharField(allow_blank=False, trim_whitespace=True)
|
|
|
|
|
|
class PayrollRunSerializer(serializers.ModelSerializer):
|
|
period_id = serializers.IntegerField(read_only=True)
|
|
status_label = serializers.CharField(source="get_status_display", read_only=True)
|
|
requested_by_id = serializers.IntegerField(read_only=True)
|
|
approved_by_id = serializers.IntegerField(read_only=True, allow_null=True)
|
|
published_by_id = serializers.IntegerField(read_only=True, allow_null=True)
|
|
supersedes_id = serializers.IntegerField(read_only=True, allow_null=True)
|
|
requested_by = PayrollUserRefSerializer(read_only=True)
|
|
approved_by = PayrollUserRefSerializer(read_only=True)
|
|
published_by = PayrollUserRefSerializer(read_only=True)
|
|
|
|
class Meta:
|
|
model = PayrollRun
|
|
fields = [
|
|
"id",
|
|
"period_id",
|
|
"revision",
|
|
"status",
|
|
"status_label",
|
|
"supersedes_id",
|
|
"recalculation_reason",
|
|
"employee_count",
|
|
"gross_total",
|
|
"deduction_total",
|
|
"payable_total",
|
|
"requested_by_id",
|
|
"requested_by",
|
|
"requested_at",
|
|
"approved_by_id",
|
|
"approved_by",
|
|
"approved_at",
|
|
"published_by_id",
|
|
"published_by",
|
|
"published_at",
|
|
]
|
|
read_only_fields = fields
|
|
|
|
def to_representation(self, instance):
|
|
data = super().to_representation(instance)
|
|
if not self.context.get("include_amounts", False):
|
|
for field_name in ("gross_total", "deduction_total", "payable_total"):
|
|
data[field_name] = None
|
|
return data
|