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
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:
0
src/apps/organization/__init__.py
Normal file
0
src/apps/organization/__init__.py
Normal file
37
src/apps/organization/admin.py
Normal file
37
src/apps/organization/admin.py
Normal 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"],
|
||||
},
|
||||
),
|
||||
]
|
||||
57
src/apps/organization/api.py
Normal file
57
src/apps/organization/api.py
Normal 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()
|
||||
11
src/apps/organization/apps.py
Normal file
11
src/apps/organization/apps.py
Normal 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 = "Организации"
|
||||
45
src/apps/organization/migrations/0001_initial.py
Normal file
45
src/apps/organization/migrations/0001_initial.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
0
src/apps/organization/migrations/__init__.py
Normal file
0
src/apps/organization/migrations/__init__.py
Normal file
83
src/apps/organization/models.py
Normal file
83
src/apps/organization/models.py
Normal 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})"
|
||||
41
src/apps/organization/serializers.py
Normal file
41
src/apps/organization/serializers.py
Normal 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",
|
||||
]
|
||||
107
src/apps/organization/services.py
Normal file
107
src/apps/organization/services.py
Normal 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
|
||||
12
src/apps/organization/urls.py
Normal file
12
src/apps/organization/urls.py
Normal 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)),
|
||||
]
|
||||
Reference in New Issue
Block a user