Files
state-corp-backend/src/apps/core/upload_contracts.py

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)