fix: create organizations from form uploads
All checks were successful
All checks were successful
This commit is contained in:
@@ -253,7 +253,7 @@ class BaseExcelParser(ABC, Generic[T]):
|
||||
]
|
||||
|
||||
def create_record(self, row_data: RowData) -> FormF1Record:
|
||||
org = OrganizationService.get_required_by_inn(...)
|
||||
org = OrganizationService.get_or_create_from_form_identifiers(...)
|
||||
return FormF1Record.objects.create(organization=org, ...)
|
||||
"""
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ from django_filters import rest_framework as filters
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
@@ -41,6 +42,48 @@ logger = logging.getLogger(__name__)
|
||||
# Порог для фоновой обработки (количество строк)
|
||||
BACKGROUND_THRESHOLD = 100
|
||||
|
||||
_PERIOD_ORDERING_FIELDS = {"report_year", "report_quarter"}
|
||||
_ALL_PERIOD_ORDERING_FIELDS = _PERIOD_ORDERING_FIELDS | {"report_month"}
|
||||
|
||||
|
||||
def _field_name(ordering_item: str) -> str:
|
||||
return ordering_item.lstrip("-")
|
||||
|
||||
|
||||
def _period_ordering_direction(ordering: list[str]) -> str:
|
||||
for ordering_item in ordering:
|
||||
if _field_name(ordering_item) in _ALL_PERIOD_ORDERING_FIELDS:
|
||||
return "-" if ordering_item.startswith("-") else ""
|
||||
return "-"
|
||||
|
||||
|
||||
def _with_form_f1_period_tiebreakers(ordering: list[str]) -> list[str]:
|
||||
ordered_fields = {_field_name(ordering_item) for ordering_item in ordering}
|
||||
if not (ordered_fields & _PERIOD_ORDERING_FIELDS):
|
||||
return ordering
|
||||
|
||||
resolved_ordering = list(ordering)
|
||||
direction = _period_ordering_direction(resolved_ordering)
|
||||
|
||||
if "report_month" not in ordered_fields:
|
||||
resolved_ordering.append(f"{direction}report_month")
|
||||
|
||||
if "created_at" not in ordered_fields:
|
||||
resolved_ordering.append("-created_at")
|
||||
|
||||
return resolved_ordering
|
||||
|
||||
|
||||
class FormF1OrderingFilter(OrderingFilter):
|
||||
"""Adds monthly period tiebreakers for F-1 ordering requests."""
|
||||
|
||||
def get_ordering(self, request, queryset, view):
|
||||
ordering = super().get_ordering(request, queryset, view)
|
||||
if not ordering:
|
||||
return ordering
|
||||
|
||||
return _with_form_f1_period_tiebreakers(list(ordering))
|
||||
|
||||
|
||||
class FormF1Filter(filters.FilterSet):
|
||||
"""Фильтры для записей формы Ф-1."""
|
||||
@@ -168,6 +211,11 @@ class FormF1RecordViewSet(ReadOnlyViewSet[FormF1Record]):
|
||||
)
|
||||
serializer_class = FormF1RecordSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
filter_backends = [
|
||||
filters.DjangoFilterBackend,
|
||||
SearchFilter,
|
||||
FormF1OrderingFilter,
|
||||
]
|
||||
filterset_class = FormF1Filter
|
||||
search_fields = ["organization__name", "organization__inn"]
|
||||
ordering_fields = [
|
||||
@@ -189,6 +237,12 @@ class FormF1RecordViewSet(ReadOnlyViewSet[FormF1Record]):
|
||||
return self.serializer_classes[self.action]
|
||||
return super().get_serializer_class()
|
||||
|
||||
def retrieve(self, request: Request, *args, **kwargs) -> Response:
|
||||
"""Return plain detail payload matching the generated F-1 API contract."""
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance)
|
||||
return Response(serializer.data)
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=["Форма Ф-1"],
|
||||
operation_summary="Список загрузок",
|
||||
|
||||
@@ -275,7 +275,13 @@ class FormF1Parser(ReportingPeriodParserMixin, BaseExcelParser[FormF1Record]):
|
||||
"""Создать запись формы Ф-1."""
|
||||
row_data = self._normalize_row_data(row_data)
|
||||
batch_id = batch_id or getattr(self, "load_batch", self.get_next_batch_id())
|
||||
org = OrganizationService.get_required_by_inn(row_data.inn)
|
||||
org = OrganizationService.get_or_create_from_form_identifiers(
|
||||
name=row_data.organization_name,
|
||||
inn=row_data.inn,
|
||||
ogrn=row_data.ogrn,
|
||||
kpp=row_data.kpp,
|
||||
okpo=row_data.okpo,
|
||||
)
|
||||
extra_period_fields = {}
|
||||
report_quarter = self.report_quarter
|
||||
if self.report_month is not None:
|
||||
|
||||
@@ -366,7 +366,13 @@ class FormF2Parser(ReportingPeriodParserMixin, BaseExcelParser[FormF2Record]):
|
||||
"""Создать запись формы Ф-2."""
|
||||
row_data = self._normalize_row_data(row_data)
|
||||
batch_id = batch_id or getattr(self, "load_batch", self.get_next_batch_id())
|
||||
org = OrganizationService.get_required_by_inn(row_data.inn)
|
||||
org = OrganizationService.get_or_create_from_form_identifiers(
|
||||
name=row_data.organization_name,
|
||||
inn=row_data.inn,
|
||||
ogrn=row_data.ogrn,
|
||||
kpp=row_data.kpp,
|
||||
okpo=row_data.okpo,
|
||||
)
|
||||
|
||||
record = FormF2Service.create_versioned_record(
|
||||
organization=org,
|
||||
|
||||
@@ -185,7 +185,13 @@ class FormF3Parser(ReportingPeriodParserMixin, BaseExcelParser[FormF3Record]):
|
||||
"""Создать запись формы Ф-3."""
|
||||
row_data = self._normalize_row_data(row_data)
|
||||
batch_id = batch_id or getattr(self, "load_batch", self.get_next_batch_id())
|
||||
org = OrganizationService.get_required_by_inn(row_data.inn)
|
||||
org = OrganizationService.get_or_create_from_form_identifiers(
|
||||
name=row_data.organization_name,
|
||||
inn=row_data.inn,
|
||||
ogrn=row_data.ogrn,
|
||||
kpp=row_data.kpp,
|
||||
okpo=row_data.okpo,
|
||||
)
|
||||
|
||||
record = FormF3Service.create_versioned_record(
|
||||
organization=org,
|
||||
|
||||
@@ -166,7 +166,13 @@ class FormF4Parser(ReportingPeriodParserMixin, BaseExcelParser[FormF4Record]):
|
||||
) -> FormF4Record:
|
||||
row_data = self._normalize_row_data(row_data)
|
||||
batch_id = batch_id or getattr(self, "load_batch", self.get_next_batch_id())
|
||||
org = OrganizationService.get_required_by_inn(row_data.inn)
|
||||
org = OrganizationService.get_or_create_from_form_identifiers(
|
||||
name=row_data.organization_name,
|
||||
inn=row_data.inn,
|
||||
ogrn=row_data.ogrn,
|
||||
kpp=row_data.kpp,
|
||||
okpo=row_data.okpo,
|
||||
)
|
||||
report_half_year = self.report_half_year
|
||||
if report_half_year is None and self.report_quarter in {1, 2}:
|
||||
report_half_year = self.report_quarter
|
||||
|
||||
@@ -148,7 +148,13 @@ class FormF5Parser(ReportingPeriodParserMixin, BaseExcelParser[FormF5Record]):
|
||||
) -> FormF5Record:
|
||||
row_data = self._normalize_row_data(row_data)
|
||||
batch_id = batch_id or getattr(self, "load_batch", self.get_next_batch_id())
|
||||
org = OrganizationService.get_required_by_inn(row_data.inn)
|
||||
org = OrganizationService.get_or_create_from_form_identifiers(
|
||||
name=row_data.organization_name,
|
||||
inn=row_data.inn,
|
||||
ogrn=row_data.ogrn,
|
||||
kpp=row_data.kpp,
|
||||
okpo=row_data.okpo,
|
||||
)
|
||||
return FormF5Service.create_versioned_record(
|
||||
organization=org,
|
||||
load_batch=batch_id,
|
||||
|
||||
@@ -135,7 +135,13 @@ class FormF6Parser(ReportingPeriodParserMixin, BaseExcelParser[FormF6Record]):
|
||||
) -> FormF6Record:
|
||||
row_data = self._normalize_row_data(row_data)
|
||||
batch_id = batch_id or getattr(self, "load_batch", self.get_next_batch_id())
|
||||
org = OrganizationService.get_required_by_inn(row_data.inn)
|
||||
org = OrganizationService.get_or_create_from_form_identifiers(
|
||||
name=row_data.organization_name,
|
||||
inn=row_data.inn,
|
||||
ogrn=row_data.ogrn,
|
||||
kpp=row_data.kpp,
|
||||
okpo=row_data.okpo,
|
||||
)
|
||||
return FormF6Service.create_versioned_record(
|
||||
organization=org,
|
||||
load_batch=batch_id,
|
||||
|
||||
@@ -7,7 +7,7 @@ from apps.organization.models import Organization
|
||||
|
||||
|
||||
class OrganizationNotFoundError(ValueError):
|
||||
"""Raised when a non-Mostovik import references an unknown organization."""
|
||||
"""Raised when organization identifiers cannot be resolved."""
|
||||
|
||||
|
||||
class OrganizationService(BaseService[Organization]):
|
||||
@@ -16,6 +16,7 @@ class OrganizationService(BaseService[Organization]):
|
||||
|
||||
Методы:
|
||||
get_or_create_by_inn: Legacy lookup API. Creation is intentionally disabled.
|
||||
get_or_create_from_form_identifiers: Создать организацию из строки формы.
|
||||
update_organization: Обновить данные организации
|
||||
search_by_name: Поиск по наименованию
|
||||
"""
|
||||
@@ -53,6 +54,58 @@ class OrganizationService(BaseService[Organization]):
|
||||
)
|
||||
return organization
|
||||
|
||||
@classmethod
|
||||
def get_or_create_from_form_identifiers(
|
||||
cls,
|
||||
*,
|
||||
name: str | None,
|
||||
inn: str | None,
|
||||
ogrn: str | None = None,
|
||||
kpp: str | None = None,
|
||||
okpo: str | None = None,
|
||||
) -> Organization:
|
||||
"""
|
||||
Return an organization for a report row, creating it when missing.
|
||||
|
||||
Form imports are allowed to create the organization reference when the row
|
||||
contains the standard identifiers. Existing organizations are not
|
||||
overwritten; the form row only fills identifier fields that are still blank.
|
||||
"""
|
||||
normalized_inn = cls._clean_digits(inn)
|
||||
if not normalized_inn:
|
||||
raise OrganizationNotFoundError("ИНН организации не указан")
|
||||
|
||||
normalized_name = cls._clean_string(name)
|
||||
if not normalized_name:
|
||||
raise OrganizationNotFoundError("Наименование организации не указано")
|
||||
|
||||
organization, created = cls.model.objects.get_or_create(
|
||||
inn=normalized_inn,
|
||||
defaults={
|
||||
"name": normalized_name,
|
||||
"ogrn": cls._clean_digits(ogrn),
|
||||
"kpp": cls._clean_digits(kpp),
|
||||
"okpo": cls._clean_digits(okpo),
|
||||
},
|
||||
)
|
||||
if created:
|
||||
return organization
|
||||
|
||||
update_fields: list[str] = []
|
||||
for field_name, value in (
|
||||
("ogrn", cls._clean_digits(ogrn)),
|
||||
("kpp", cls._clean_digits(kpp)),
|
||||
("okpo", cls._clean_digits(okpo)),
|
||||
):
|
||||
if value and not getattr(organization, field_name):
|
||||
setattr(organization, field_name, value)
|
||||
update_fields.append(field_name)
|
||||
|
||||
if update_fields:
|
||||
organization.save(update_fields=update_fields + ["updated_at"])
|
||||
|
||||
return organization
|
||||
|
||||
@classmethod
|
||||
def search_by_name(cls, query: str, limit: int = 20):
|
||||
"""
|
||||
@@ -91,3 +144,9 @@ class OrganizationService(BaseService[Organization]):
|
||||
if value is None:
|
||||
return ""
|
||||
return "".join(char for char in str(value).strip() if char.isdigit())
|
||||
|
||||
@staticmethod
|
||||
def _clean_string(value: Any) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
return str(value).strip()
|
||||
|
||||
Reference in New Issue
Block a user