Add initial implementations for forms and organization apps with serializers, factories, and admin configurations
Some checks failed
CI/CD Pipeline / Code Quality Checks (push) Failing after 5m5s
CI/CD Pipeline / Run Tests (push) Failing after 5m5s
CI/CD Pipeline / Build Docker Images (push) Has been skipped
CI/CD Pipeline / Push to Gitea Registry (push) Has been skipped
CI/CD Pipeline / Deploy to Server (push) Has been skipped

This commit is contained in:
2026-02-17 09:26:08 +01:00
parent fd2adf9ab4
commit 8ed3e1175c
119 changed files with 9091 additions and 0 deletions

View File

View File

@@ -0,0 +1,37 @@
"""Административный интерфейс для организаций."""
from apps.organization.models import Organization
from django.contrib import admin
@admin.register(Organization)
class OrganizationAdmin(admin.ModelAdmin):
"""Админка для организаций."""
list_display = ["name", "inn", "ogrn", "kpp", "created_at"]
list_filter = ["created_at"]
search_fields = ["name", "inn", "ogrn"]
readonly_fields = ["id", "created_at", "updated_at"]
ordering = ["name"]
fieldsets = [
(
"Основная информация",
{
"fields": ["id", "name"],
},
),
(
"Идентификаторы",
{
"fields": ["inn", "ogrn", "kpp", "okpo"],
},
),
(
"Системные поля",
{
"fields": ["created_at", "updated_at"],
"classes": ["collapse"],
},
),
]

View File

@@ -0,0 +1,57 @@
"""
API ViewSets для организаций.
Содержит:
- OrganizationViewSet - CRUD для организаций
"""
from apps.core.viewsets import ReadOnlyViewSet
from apps.organization.models import Organization
from apps.organization.serializers import (
OrganizationListSerializer,
OrganizationSerializer,
)
from django_filters import rest_framework as filters
from rest_framework.permissions import IsAuthenticated
class OrganizationFilter(filters.FilterSet):
"""Фильтры для организаций."""
name = filters.CharFilter(lookup_expr="icontains")
inn = filters.CharFilter(lookup_expr="exact")
ogrn = filters.CharFilter(lookup_expr="exact")
class Meta:
model = Organization
fields = ["name", "inn", "ogrn"]
class OrganizationViewSet(ReadOnlyViewSet[Organization]):
"""
ViewSet для просмотра организаций.
Только чтение - организации создаются автоматически при загрузке форм.
Эндпоинты:
GET /organizations/ - список организаций
GET /organizations/{id}/ - детали организации
"""
queryset = Organization.objects.all()
serializer_class = OrganizationSerializer
permission_classes = [IsAuthenticated]
filterset_class = OrganizationFilter
search_fields = ["name", "inn", "ogrn"]
ordering_fields = ["name", "inn", "created_at"]
ordering = ["name"]
serializer_classes = {
"list": OrganizationListSerializer,
}
def get_serializer_class(self):
"""Возвращает serializer в зависимости от action."""
if self.action in self.serializer_classes:
return self.serializer_classes[self.action]
return super().get_serializer_class()

View File

@@ -0,0 +1,11 @@
"""Конфигурация приложения organization."""
from django.apps import AppConfig
class OrganizationConfig(AppConfig):
"""Конфигурация приложения справочника организаций."""
default_auto_field = "django.db.models.BigAutoField"
name = "apps.organization"
verbose_name = "Организации"

View File

@@ -0,0 +1,45 @@
# Generated by Django 3.2.25 on 2026-02-06 12:49
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Organization',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, db_index=True, help_text='Дата и время создания записи', verbose_name='создано')),
('updated_at', models.DateTimeField(auto_now=True, help_text='Дата и время последнего обновления', verbose_name='обновлено')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(db_index=True, help_text='Полное наименование организации', max_length=500, verbose_name='наименование')),
('inn', models.CharField(db_index=True, help_text='Идентификационный номер налогоплательщика (10 или 12 цифр)', max_length=12, unique=True, verbose_name='ИНН')),
('ogrn', models.CharField(blank=True, db_index=True, default='', help_text='Основной государственный регистрационный номер (13 или 15 цифр)', max_length=15, verbose_name='ОГРН')),
('kpp', models.CharField(blank=True, default='', help_text='Код причины постановки на учёт (9 цифр)', max_length=9, verbose_name='КПП')),
('okpo', models.CharField(blank=True, default='', help_text='Общероссийский классификатор предприятий и организаций', max_length=20, verbose_name='ОКПО')),
],
options={
'verbose_name': 'организация',
'verbose_name_plural': 'организации',
'ordering': ['name'],
},
),
migrations.AddIndex(
model_name='organization',
index=models.Index(fields=['inn'], name='organizatio_inn_6cdafb_idx'),
),
migrations.AddIndex(
model_name='organization',
index=models.Index(fields=['ogrn'], name='organizatio_ogrn_c5495f_idx'),
),
migrations.AddIndex(
model_name='organization',
index=models.Index(fields=['name'], name='organizatio_name_2d216c_idx'),
),
]

View File

@@ -0,0 +1,83 @@
"""
Модели справочника организаций.
Содержит:
- Organization - справочник организаций (ИНН, ОГРН, КПП, наименование)
"""
import uuid
from apps.core.mixins import TimestampMixin
from django.db import models
from django.utils.translation import gettext_lazy as _
class Organization(TimestampMixin, models.Model):
"""
Справочник организаций.
Централизованное хранение данных организаций для нормализации БД.
Все формы отчётности ссылаются на эту таблицу через FK.
Поля:
name: Наименование организации
inn: ИНН (уникальный)
ogrn: ОГРН
kpp: КПП
okpo: ОКПО
"""
id = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
editable=False,
verbose_name=_("ID"),
)
name = models.CharField(
_("наименование"),
max_length=500,
db_index=True,
help_text=_("Полное наименование организации"),
)
inn = models.CharField(
_("ИНН"),
max_length=12,
unique=True,
db_index=True,
help_text=_("Идентификационный номер налогоплательщика (10 или 12 цифр)"),
)
ogrn = models.CharField(
_("ОГРН"),
max_length=15,
db_index=True,
blank=True,
default="",
help_text=_("Основной государственный регистрационный номер (13 или 15 цифр)"),
)
kpp = models.CharField(
_("КПП"),
max_length=9,
blank=True,
default="",
help_text=_("Код причины постановки на учёт (9 цифр)"),
)
okpo = models.CharField(
_("ОКПО"),
max_length=20,
blank=True,
default="",
help_text=_("Общероссийский классификатор предприятий и организаций"),
)
class Meta:
verbose_name = _("организация")
verbose_name_plural = _("организации")
ordering = ["name"]
indexes = [
models.Index(fields=["inn"]),
models.Index(fields=["ogrn"]),
models.Index(fields=["name"]),
]
def __str__(self) -> str:
return f"{self.name} (ИНН: {self.inn})"

View File

@@ -0,0 +1,41 @@
"""
Сериализаторы для организаций.
Содержит:
- OrganizationSerializer - полный сериализатор
- OrganizationListSerializer - краткий для списков
"""
from apps.organization.models import Organization
from rest_framework import serializers
class OrganizationSerializer(serializers.ModelSerializer):
"""Полный сериализатор организации."""
class Meta:
model = Organization
fields = [
"id",
"name",
"inn",
"ogrn",
"kpp",
"okpo",
"created_at",
"updated_at",
]
read_only_fields = ["id", "created_at", "updated_at"]
class OrganizationListSerializer(serializers.ModelSerializer):
"""Краткий сериализатор для списков."""
class Meta:
model = Organization
fields = [
"id",
"name",
"inn",
"ogrn",
]

View File

@@ -0,0 +1,107 @@
"""
Сервисы для работы с организациями.
Содержит:
- OrganizationService - CRUD операции и бизнес-логика
"""
import logging
from typing import Any
from apps.core.services import BaseService
from apps.organization.models import Organization
from django.db import transaction
logger = logging.getLogger(__name__)
class OrganizationService(BaseService[Organization]):
"""
Сервис для работы с организациями.
Методы:
get_or_create_by_inn: Получить или создать организацию по ИНН
update_organization: Обновить данные организации
search_by_name: Поиск по наименованию
"""
model = Organization
@classmethod
@transaction.atomic
def get_or_create_by_inn(
cls,
inn: str,
defaults: dict[str, Any] | None = None,
) -> tuple[Organization, bool]:
"""
Получить или создать организацию по ИНН.
Args:
inn: ИНН организации
defaults: Значения по умолчанию для создания
Returns:
(Organization, created) - организация и флаг создания
"""
defaults = defaults or {}
org, created = cls.model.objects.get_or_create(
inn=inn,
defaults=defaults,
)
if created:
logger.info(
f"Создана организация: {org.name} (ИНН: {inn})",
extra={"inn": inn, "org_id": str(org.id)},
)
else:
# Обновляем данные если переданы новые
updated_fields = []
for field, value in defaults.items():
if value and getattr(org, field, None) != value:
# Обновляем только пустые поля или если значение изменилось
current = getattr(org, field, None)
if not current or field == "name":
setattr(org, field, value)
updated_fields.append(field)
if updated_fields:
org.save(update_fields=updated_fields + ["updated_at"])
logger.info(
f"Обновлена организация: {org.name} (ИНН: {inn}), поля: {updated_fields}",
extra={"inn": inn, "org_id": str(org.id), "fields": updated_fields},
)
return org, created
@classmethod
def search_by_name(cls, query: str, limit: int = 20):
"""
Поиск организаций по наименованию.
Args:
query: Строка поиска
limit: Максимальное количество результатов
Returns:
QuerySet организаций
"""
return cls.model.objects.filter(name__icontains=query)[:limit]
@classmethod
def get_by_inn(cls, inn: str) -> Organization | None:
"""
Получить организацию по ИНН.
Args:
inn: ИНН организации
Returns:
Organization или None
"""
try:
return cls.model.objects.get(inn=inn)
except cls.model.DoesNotExist:
return None

View File

@@ -0,0 +1,12 @@
"""URL маршруты для организаций."""
from apps.organization.api import OrganizationViewSet
from django.urls import include, path
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register("", OrganizationViewSet, basename="organization")
urlpatterns = [
path("", include(router.urls)),
]