219 lines
6.3 KiB
Python
219 lines
6.3 KiB
Python
"""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)
|