"""Shared upload serializers and response helpers for form upload endpoints.""" from __future__ import annotations from datetime import datetime from typing import Any from uuid import uuid4 from django.utils import timezone from rest_framework import serializers, status from rest_framework.response import Response MAX_UPLOAD_SIZE_BYTES = 50 * 1024 * 1024 ALLOWED_UPLOAD_EXTENSIONS = (".xlsx", ".xls") _ROMAN_NUMBERS = { 1: "I", 2: "II", 3: "III", 4: "IV", } def _normalize_extension(name: str) -> str: return name.rsplit(".", 1)[-1].lower() if "." in name else "" def _validate_uploaded_file(value: Any) -> None: if not hasattr(value, "name") or not value.name: raise serializers.ValidationError("Файл не выбран") extension = "." + _normalize_extension(value.name) if extension not in ALLOWED_UPLOAD_EXTENSIONS: raise serializers.ValidationError("Неподдерживаемый формат файла") if value.size > MAX_UPLOAD_SIZE_BYTES: raise serializers.ValidationError("Размер файла превышает 50MB") def report_quarter_display(report_year: int, report_quarter: int) -> str: return f"{_ROMAN_NUMBERS.get(report_quarter, report_quarter)} квартал {report_year}" def report_half_year_display(report_year: int, report_half_year: int) -> str: return f"{_ROMAN_NUMBERS.get(report_half_year, report_half_year)} полугодие {report_year}" def report_annual_display(report_year: int) -> str: return f"{report_year} год" def build_upload_success_payload( *, form: str, report_year: int, status: str, report_quarter: int | None = None, report_half_year: int | None = None, result: dict[str, Any] | None = None, job_id: str | None = None, created_at: datetime | None = None, ) -> dict[str, Any]: created = created_at or timezone.now() payload: dict[str, Any] = { "upload_id": str(uuid4()), "form": form, "report_year": report_year, "status": status, "created_at": created.isoformat(), } if report_half_year is not None: payload["report_half_year"] = report_half_year payload["report_period_display"] = report_half_year_display( report_year=report_year, report_half_year=report_half_year, ) elif report_quarter is not None: payload["report_quarter"] = report_quarter payload["report_period_display"] = report_quarter_display( report_year=report_year, report_quarter=report_quarter, ) else: payload["report_period_display"] = report_annual_display( report_year=report_year ) if result is not None: payload["result"] = result if job_id is not None: payload["job_id"] = job_id return payload def build_upload_validation_payload(errors: dict[str, Any]) -> dict[str, Any]: details: list[dict[str, Any]] = [] def _collect(field: str, value: Any) -> None: if isinstance(value, dict): for nested_field, nested_value in value.items(): nested_name = f"{field}.{nested_field}" if field else nested_field _collect(nested_name, nested_value) return if isinstance(value, list): for item in value: _collect(field, item) return code = getattr(value, "code", None) or "invalid" details.append( { "field": field, "code": code, "message": str(value), } ) for field, value in errors.items(): target_field = "non_field_errors" if field == "non_field_errors" else field _collect(target_field, value) return { "error_code": "validation_error", "error_message": "Validation failed", "details": details, } def build_upload_error_payload( *, error_code: str, error_message: str, details: list[dict[str, Any]] | None = None, ) -> dict[str, Any]: return { "error_code": error_code, "error_message": error_message, "details": details or [], } def build_upload_error_response( *, error_code: str, error_message: str, details: list[dict[str, Any]] | None = None, status_code: int = status.HTTP_400_BAD_REQUEST, ) -> Response: return Response( build_upload_error_payload( error_code=error_code, error_message=error_message, details=details, ), status=status_code, ) def build_upload_validation_response(errors: dict[str, Any]) -> Response: return Response( build_upload_validation_payload(errors), status=status.HTTP_400_BAD_REQUEST, ) class BaseUploadSerializer(serializers.Serializer): file = serializers.FileField(help_text="Excel файл (.xlsx, .xls)") report_year = serializers.IntegerField(min_value=2000, help_text="Отчетный год") def validate_file(self, value): _validate_uploaded_file(value) return value class UploadQuarterSerializer(BaseUploadSerializer): report_quarter = serializers.IntegerField( min_value=1, max_value=4, required=True, help_text="Отчетный квартал от 1 до 4", ) class UploadHalfYearSerializer(BaseUploadSerializer): report_half_year = serializers.IntegerField( min_value=1, max_value=2, required=True, help_text="Полугодие отчета: 1 или 2", ) class UploadAnnualSerializer(BaseUploadSerializer): """Upload payload without period quarter (годовая форма).""" class FieldErrorSerializer(serializers.Serializer): field = serializers.CharField() message = serializers.CharField() class RowValidationErrorSerializer(serializers.Serializer): row = serializers.IntegerField() inn = serializers.CharField(allow_null=True) kpp = serializers.CharField(allow_null=True) organization_name = serializers.CharField(allow_null=True) errors = FieldErrorSerializer(many=True) class UploadParseResultSerializer(serializers.Serializer): batch_id = serializers.IntegerField() loaded_count = serializers.IntegerField() skipped_count = serializers.IntegerField() errors = RowValidationErrorSerializer(many=True)