Add initial implementations for forms and organization apps with serializers, factories, and admin configurations
Some checks failed
CI/CD Pipeline / Run Tests (push) Failing after 45s
CI/CD Pipeline / Code Quality Checks (push) Failing after 48s
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-03-28 18:23:06 +01:00
parent 8ed3e1175c
commit 345b1d0cc8
201 changed files with 15097 additions and 6691 deletions

View File

@@ -1,18 +1,199 @@
"""Административный интерфейс для организаций."""
from __future__ import annotations
from apps.form_1.admin import FormF1RecordAdmin
from apps.form_1.models import FormF1Record
from apps.form_2.admin import FormF2RecordAdmin
from apps.form_2.models import FormF2Record
from apps.form_3.admin import FormF3RecordAdmin
from apps.form_3.models import FormF3Record
from apps.form_4.admin import FormF4RecordAdmin
from apps.form_4.models import FormF4Record
from apps.form_5.admin import FormF5RecordAdmin
from apps.form_5.models import FormF5Record
from apps.form_6.admin import FormF6RecordAdmin
from apps.form_6.models import FormF6Record
from apps.core.admin_paper_forms import PaperFormPreviewRendererMixin
from apps.organization.models import Organization
from apps.registers.models import Register, RegistryMembershipPeriod
from django.contrib import admin
from django.db.models import F, Prefetch
class ActiveRegistryListFilter(admin.SimpleListFilter):
"""Filter organizations by active registry membership."""
title = "актуальный реестр"
parameter_name = "active_registry"
def lookups(self, request, model_admin):
return [(str(register.id), register.name) for register in Register.objects.order_by("name")]
def queryset(self, request, queryset):
if not self.value():
return queryset
return queryset.filter(
membership_periods__registry_id=self.value(),
membership_periods__ended_at__isnull=True,
).distinct()
class BaseOrganizationFormInline(PaperFormPreviewRendererMixin, admin.StackedInline):
"""Base inline for report forms embedded into organization admin."""
template = "admin/edit_inline/report_period_tabs.html"
extra = 0
fk_name = "organization"
show_change_link = False
can_delete = False
fields = ["paper_form_preview"]
readonly_fields = ["paper_form_preview"]
def get_fieldsets(self, request, obj=None):
return [
(
self.paper_form_fieldset_title,
{
"fields": ["paper_form_preview"],
"description": "Просмотр записи в формате бумажной табличной формы.",
},
)
]
def has_add_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
def has_change_permission(self, request, obj = None):
return False
def get_queryset(self, request):
return (
super()
.get_queryset(request)
.select_related("organization")
.order_by(
F("report_year").desc(),
F("report_quarter").desc(nulls_last=True),
F("is_active_version").desc(),
F("created_at").desc(),
)
)
class FormF1RecordInline(BaseOrganizationFormInline):
model = FormF1Record
verbose_name = "Ф-1"
verbose_name_plural = "Ф-1. Выпуск продукции"
paper_form_title = FormF1RecordAdmin.paper_form_title
paper_form_sections = FormF1RecordAdmin.paper_form_sections
class FormF2RecordInline(BaseOrganizationFormInline):
model = FormF2Record
verbose_name = "Ф-2"
verbose_name_plural = "Ф-2. Бухгалтерский баланс"
paper_form_title = FormF2RecordAdmin.paper_form_title
paper_form_sections = FormF2RecordAdmin.paper_form_sections
class FormF3RecordInline(BaseOrganizationFormInline):
model = FormF3Record
verbose_name = "Ф-3"
verbose_name_plural = "Ф-3. Кадры и оборудование"
paper_form_title = FormF3RecordAdmin.paper_form_title
paper_form_sections = FormF3RecordAdmin.paper_form_sections
class FormF4RecordInline(BaseOrganizationFormInline):
model = FormF4Record
verbose_name = "Ф-4"
verbose_name_plural = "Ф-4. Сводные финансовые данные"
paper_form_title = FormF4RecordAdmin.paper_form_title
paper_form_sections = FormF4RecordAdmin.paper_form_sections
class FormF5RecordInline(BaseOrganizationFormInline):
model = FormF5Record
verbose_name = "Ф-5"
verbose_name_plural = "Ф-5. Инвентаризация оборудования"
paper_form_title = FormF5RecordAdmin.paper_form_title
paper_form_sections = FormF5RecordAdmin.paper_form_sections
class FormF6RecordInline(BaseOrganizationFormInline):
model = FormF6Record
verbose_name = "Ф-6"
verbose_name_plural = "Ф-6. Возрастная структура оборудования"
paper_form_title = FormF6RecordAdmin.paper_form_title
paper_form_sections = FormF6RecordAdmin.paper_form_sections
class ActiveRegistryMembershipInline(admin.TabularInline):
"""Read-only inline with current registry memberships."""
model = RegistryMembershipPeriod
fk_name = "organization"
verbose_name = "актуальное участие в реестре"
verbose_name_plural = "Актуальные участия в реестрах"
extra = 0
can_delete = False
show_change_link = False
fields = [
"registry",
"started_at",
"started_by_upload",
"ended_at",
"ended_by_upload",
]
readonly_fields = fields
ordering = ["registry__name"]
def get_queryset(self, request):
return (
super()
.get_queryset(request)
.filter(ended_at__isnull=True)
.select_related("registry", "started_by_upload", "ended_by_upload")
)
def has_add_permission(self, request, obj=None):
return False
@admin.register(Organization)
class OrganizationAdmin(admin.ModelAdmin):
"""Админка для организаций."""
list_display = ["name", "inn", "ogrn", "kpp", "created_at"]
list_filter = ["created_at"]
list_display = [
"name",
"inn",
"current_registry_names",
"ogrn",
"kpp",
"created_at",
]
list_filter = [ActiveRegistryListFilter, "created_at"]
search_fields = ["name", "inn", "ogrn"]
readonly_fields = ["id", "created_at", "updated_at"]
readonly_fields = [
"id",
"active_registry_names_display",
"created_at",
"updated_at",
]
ordering = ["name"]
inlines = [
ActiveRegistryMembershipInline,
FormF1RecordInline,
FormF2RecordInline,
FormF3RecordInline,
FormF4RecordInline,
FormF5RecordInline,
FormF6RecordInline,
]
fieldsets = [
(
@@ -27,6 +208,12 @@ class OrganizationAdmin(admin.ModelAdmin):
"fields": ["inn", "ogrn", "kpp", "okpo"],
},
),
(
"Актуальные реестры",
{
"fields": ["active_registry_names_display"],
},
),
(
"Системные поля",
{
@@ -35,3 +222,31 @@ class OrganizationAdmin(admin.ModelAdmin):
},
),
]
def get_queryset(self, request):
return (
super()
.get_queryset(request)
.prefetch_related(
Prefetch(
"membership_periods",
queryset=RegistryMembershipPeriod.objects.filter(
ended_at__isnull=True
).select_related("registry"),
to_attr="active_membership_periods_prefetched",
)
)
)
@admin.display(description="Актуальные реестры")
def current_registry_names(self, obj):
return obj.active_registry_names_display()
def has_add_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
def has_change_permission(self, request, obj = None):
return False

View File

@@ -11,6 +11,8 @@ from apps.organization.serializers import (
OrganizationListSerializer,
OrganizationSerializer,
)
from apps.registers.models import RegistryMembershipPeriod
from django.db.models import Prefetch
from django_filters import rest_framework as filters
from rest_framework.permissions import IsAuthenticated
@@ -21,10 +23,18 @@ class OrganizationFilter(filters.FilterSet):
name = filters.CharFilter(lookup_expr="icontains")
inn = filters.CharFilter(lookup_expr="exact")
ogrn = filters.CharFilter(lookup_expr="exact")
registry = filters.UUIDFilter(method="filter_registry")
@staticmethod
def filter_registry(queryset, _name, value):
return queryset.filter(
membership_periods__registry_id=value,
membership_periods__ended_at__isnull=True,
).distinct()
class Meta:
model = Organization
fields = ["name", "inn", "ogrn"]
fields = ["name", "inn", "ogrn", "registry"]
class OrganizationViewSet(ReadOnlyViewSet[Organization]):
@@ -55,3 +65,18 @@ class OrganizationViewSet(ReadOnlyViewSet[Organization]):
if self.action in self.serializer_classes:
return self.serializer_classes[self.action]
return super().get_serializer_class()
def get_queryset(self):
return (
super()
.get_queryset()
.prefetch_related(
Prefetch(
"membership_periods",
queryset=RegistryMembershipPeriod.objects.filter(
ended_at__isnull=True
).select_related("registry"),
to_attr="active_membership_periods_prefetched",
)
)
)

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.2.25 on 2026-03-28 15:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('registers', '0002_seed_registers'),
('organization', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='organization',
name='registers',
field=models.ManyToManyField(blank=True, related_name='organizations', through='registers.RegistryMembershipPeriod', to='registers.Register', verbose_name='реестры'),
),
]

View File

@@ -5,6 +5,8 @@
- Organization - справочник организаций (ИНН, ОГРН, КПП, наименование)
"""
from __future__ import annotations
import uuid
from apps.core.mixins import TimestampMixin
@@ -68,6 +70,13 @@ class Organization(TimestampMixin, models.Model):
default="",
help_text=_("Общероссийский классификатор предприятий и организаций"),
)
registers = models.ManyToManyField(
"registers.Register",
through="registers.RegistryMembershipPeriod",
related_name="organizations",
verbose_name=_("реестры"),
blank=True,
)
class Meta:
verbose_name = _("организация")
@@ -81,3 +90,21 @@ class Organization(TimestampMixin, models.Model):
def __str__(self) -> str:
return f"{self.name} (ИНН: {self.inn})"
def get_active_membership_periods(self):
prefetched = getattr(self, "active_membership_periods_prefetched", None)
if prefetched is not None:
return prefetched
return list(
self.membership_periods.filter(ended_at__isnull=True).select_related("registry")
)
def get_active_registries(self):
return [period.registry for period in self.get_active_membership_periods()]
def get_active_registry_names(self) -> list[str]:
return [registry.name for registry in self.get_active_registries()]
def active_registry_names_display(self) -> str:
names = self.get_active_registry_names()
return ", ".join(names) if names else ""

View File

@@ -7,12 +7,33 @@
"""
from apps.organization.models import Organization
from apps.registers.models import Register
from rest_framework import serializers
class OrganizationRegisterSerializer(serializers.ModelSerializer):
"""Краткий сериализатор активного реестра организации."""
class Meta:
model = Register
fields = ["id", "name"]
read_only_fields = fields
class OrganizationSerializer(serializers.ModelSerializer):
"""Полный сериализатор организации."""
active_registry_names = serializers.SerializerMethodField()
active_registries = serializers.SerializerMethodField()
@staticmethod
def get_active_registry_names(obj: Organization) -> list[str]:
return obj.get_active_registry_names()
@staticmethod
def get_active_registries(obj: Organization) -> list[dict[str, str]]:
return OrganizationRegisterSerializer(obj.get_active_registries(), many=True).data
class Meta:
model = Organization
fields = [
@@ -22,6 +43,8 @@ class OrganizationSerializer(serializers.ModelSerializer):
"ogrn",
"kpp",
"okpo",
"active_registry_names",
"active_registries",
"created_at",
"updated_at",
]
@@ -31,6 +54,12 @@ class OrganizationSerializer(serializers.ModelSerializer):
class OrganizationListSerializer(serializers.ModelSerializer):
"""Краткий сериализатор для списков."""
active_registry_names = serializers.SerializerMethodField()
@staticmethod
def get_active_registry_names(obj: Organization) -> list[str]:
return obj.get_active_registry_names()
class Meta:
model = Organization
fields = [
@@ -38,4 +67,5 @@ class OrganizationListSerializer(serializers.ModelSerializer):
"name",
"inn",
"ogrn",
"active_registry_names",
]

View File

@@ -63,7 +63,7 @@ class OrganizationService(BaseService[Organization]):
if value and getattr(org, field, None) != value:
# Обновляем только пустые поля или если значение изменилось
current = getattr(org, field, None)
if not current or field == "name":
if not current:
setattr(org, field, value)
updated_fields.append(field)