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
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:
@@ -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
|
||||
@@ -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",
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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='реестры'),
|
||||
),
|
||||
]
|
||||
@@ -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 "—"
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user