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

@@ -0,0 +1,92 @@
{% load i18n admin_urls static %}
<div class="js-inline-admin-formset inline-group mx-report-period-inline" id="{{ inline_admin_formset.formset.prefix }}-group" data-inline-type="stacked" data-inline-formset="{{ inline_admin_formset.inline_formset_data }}">
<fieldset class="module {{ inline_admin_formset.classes }} card card-outline">
<div class="card-body">
{{ inline_admin_formset.formset.management_form }}
{{ inline_admin_formset.formset.non_form_errors }}
<div class="mx-report-period-inline__header">
<h3 class="mx-report-period-inline__title">{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h3>
<p class="mx-report-period-inline__hint">Печатные формы сгруппированы по отчетным периодам. Архивные версии доступны только в админке.</p>
</div>
<div class="mx-report-period-inline__nav nav nav-tabs" role="tablist">
{% for inline_admin_form in inline_admin_formset %}
{% with inline_admin_form.original as record %}
{% if record %}
{% ifchanged record.report_period_key %}
<a
class="nav-link {% if forloop.first %}active{% endif %}"
id="{{ inline_admin_formset.formset.prefix }}-{{ record.report_period_dom_id }}-tab"
data-toggle="tab"
href="#{{ inline_admin_formset.formset.prefix }}-{{ record.report_period_dom_id }}"
role="tab"
aria-controls="{{ inline_admin_formset.formset.prefix }}-{{ record.report_period_dom_id }}"
aria-selected="{% if forloop.first %}true{% else %}false{% endif %}"
>
{{ record.report_period_display }}
</a>
{% endifchanged %}
{% endif %}
{% endwith %}
{% empty %}
<span class="mx-report-period-inline__empty">Отчетность пока не загружена.</span>
{% endfor %}
</div>
<div class="tab-content mx-report-period-inline__content">
{% for inline_admin_form in inline_admin_formset %}
{% with inline_admin_form.original as record %}
{% if record %}
{% ifchanged record.report_period_key %}
{% if not forloop.first %}
</div>
</div>
{% endif %}
<div
class="tab-pane fade {% if forloop.first %}show active{% endif %}"
id="{{ inline_admin_formset.formset.prefix }}-{{ record.report_period_dom_id }}"
role="tabpanel"
aria-labelledby="{{ inline_admin_formset.formset.prefix }}-{{ record.report_period_dom_id }}-tab"
>
<div class="mx-report-period-inline__cards">
{% endifchanged %}
<article class="mx-report-period-card{% if not record.is_active_version %} mx-report-period-card--archived{% endif %}">
<header class="mx-report-period-card__header">
<div>
<p class="mx-report-period-card__eyebrow">{{ record.report_period_short_label }}</p>
<h4 class="mx-report-period-card__title">{{ record.version_label }}</h4>
</div>
<div class="mx-report-period-card__meta">
<span class="mx-report-period-card__badge{% if record.is_active_version %} is-current{% else %} is-archived{% endif %}">
{% if record.is_active_version %}Актуальная{% else %}Архив{% endif %}
</span>
<span class="mx-report-period-card__badge">Batch {{ record.load_batch }}</span>
{% if record.superseded_by_batch %}
<span class="mx-report-period-card__badge">Заменен batch {{ record.superseded_by_batch }}</span>
{% endif %}
</div>
</header>
{% if inline_admin_form.form.non_field_errors %}
{{ inline_admin_form.form.non_field_errors }}
{% endif %}
{% for fieldset in inline_admin_form %}
{% include "admin/includes/fieldset.html" %}
{% endfor %}
{% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %}
{% if inline_admin_form.fk_field %}{{ inline_admin_form.fk_field.field }}{% endif %}
</article>
{% if forloop.last %}
</div>
</div>
{% endif %}
{% endif %}
{% endwith %}
{% endfor %}
</div>
</div>
</fieldset>
</div>

View File

@@ -0,0 +1,107 @@
{% load jazzmin %}
{% if card %}
<div class="card {{ fieldset.classes|cut:"collapse" }}">
{% if card_header and fieldset.name %}
<div class="card-header">
<div class="card-title">
<strong>{{ fieldset.name }}</strong>{% if fieldset.description %} - <i>{{ fieldset.description }}</i>{% endif %}
</div>
</div>
{% elif fieldset.description %}
<div class="card-header">
<div class="card-title">
{{ fieldset.description }}
</div>
</div>
{% endif %}
<div class="p-5{% if fieldset.name %} card-body{% endif %}">
{% endif %}
{% for line in fieldset %}
{% if line.fields|length_is:'1' %}
{% for field in line %}
{% if field.field.name == "paper_form_preview" %}
<div class="form-group mx-paper-form-row{% if line.errors %} errors{% endif %}{% if not line.has_visible_field %} hidden{% endif %}">
<div class="mx-paper-form-field{% if field.field.is_hidden %} hidden{% endif %}">
{% if field.is_readonly %}
<div class="readonly">{{ field.contents }}</div>
{% else %}
{{ field.field }}
{% endif %}
<div class="help-block red">
{% if not field.is_readonly %}{{ field.errors }}{% endif %}
</div>
{% if field.field.help_text %}
<div class="help-block">{{ field.field.help_text|safe }}</div>
{% endif %}
<div class="help-block text-red">
{{ line.errors }}
</div>
</div>
</div>
{% else %}
<div class="form-group{% if line.errors %} errors{% endif %}{% if not line.has_visible_field %} hidden{% endif %} field-{{ field.field.name }}">
<div class="row">
<label class="col-sm-3 text-left" for="id_{{ field.field.name }}">
{{ field.field.label|capfirst }}
{% if field.field.field.required %}
<span class="text-red">* </span>
{% endif %}
</label>
<div class="col-sm-7 field-{{ field.field.name }}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}{% if field.is_checkcard %} checkcard-row{% endif %}">
{% if field.is_readonly %}
<div class="readonly">{{ field.contents }}</div>
{% else %}
{{ field.field }}
{% endif %}
<div class="help-block red">
{% if not field.is_readonly %}{{ field.errors }}{% endif %}
</div>
{% if field.field.help_text %}
<div class="help-block">{{ field.field.help_text|safe }}</div>
{% endif %}
<div class="help-block text-red">
{{ line.errors }}
</div>
</div>
</div>
</div>
{% endif %}
{% endfor %}
{% else %}
<div class="form-group{% if line.errors %} errors{% endif %}{% if not line.has_visible_field %} hidden{% endif %}{% for field in line %}{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% endfor %}">
<div class="row">
{% for field in line %}
<label class="{% if forloop.counter != 1 %}col-auto {% else %}col-sm-3 {% endif %}text-left" for="id_{{ field.field.name }}">
{{ field.field.label|capfirst }}
{% if field.field.field.required %}
<span class="text-red">* </span>
{% endif %}
</label>
<div class="col-auto fieldBox{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}{% if field.is_checkcard %} checkcard-row{% endif %}">
{% if field.is_readonly %}
<div class="readonly">{{ field.contents }}</div>
{% else %}
{{ field.field }}
{% endif %}
<div class="help-block red">
{% if not field.is_readonly %}{{ field.errors }}{% endif %}
</div>
{% if field.field.help_text %}
<div class="help-block">{{ field.field.help_text|safe }}</div>
{% endif %}
<div class="help-block text-red">
{% if forloop.first %}{{ line.errors }}{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endfor %}
{% if card %}
</div>
</div>
{% endif %}

View File

@@ -0,0 +1,468 @@
{% extends "admin/base_site.html" %}
{% load i18n static jazzmin log %}
{% block bodyclass %}{{ block.super }} dashboard mx-dashboard-page{% endblock %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'admin/css/state-corp-admin-dashboard.css' %}">
{% endblock %}
{% block content_title %}{% trans 'Dashboard' %}{% endblock %}
{% block breadcrumbs %}
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'admin:index' %}">{% trans 'Home' %}</a></li>
<li class="breadcrumb-item">{% trans 'Dashboard' %}</li>
</ol>
{% endblock %}
{% block content %}
{% get_side_menu using="app_list" as dashboard_list %}
<div class="mx-dashboard">
<section class="mx-hero">
<div class="mx-hero__copy">
<p class="mx-eyebrow">Панель управления</p>
<h1 class="mx-hero__title">Панель форм и импортов</h1>
<p class="mx-hero__lead">
Живой обзор по справочнику организаций, наполнению форм Ф-1...Ф-6
и последним импортам без переключения между разделами админки.
</p>
<div class="mx-hero__facts">
{% for item in admin_dashboard_data.hero_stats %}
<div class="mx-hero__fact">
<span class="mx-hero__fact-label">{{ item.label }}</span>
<strong class="mx-hero__fact-value">
{% if item.value is not None %}
{% if item.is_datetime %}
{{ item.value|date:"d.m.Y H:i" }}
{% else %}
{{ item.value }}
{% endif %}
{% else %}
нет данных
{% endif %}
</strong>
</div>
{% endfor %}
</div>
</div>
<div class="mx-hero__visual">
<div class="mx-ring-card">
<div
class="mx-ring-chart"
style="background: {{ admin_dashboard_data.source_mix.background }};"
>
<div class="mx-ring-chart__center">
<span>всего записей</span>
<strong>{{ admin_dashboard_data.source_mix.total_records_label }}</strong>
</div>
</div>
<div class="mx-ring-card__legend">
{% if admin_dashboard_data.source_mix.segments %}
{% for segment in admin_dashboard_data.source_mix.segments %}
<div class="mx-ring-card__legend-item">
<span
class="mx-dot"
style="background: {{ segment.color }};"
></span>
<div>
<strong>{{ segment.title }}</strong>
<span>{{ segment.value }} · {{ segment.share }}%</span>
</div>
</div>
{% endfor %}
{% else %}
<p class="mx-empty-state">
Диаграмма появится после первой загрузки данных из источников.
</p>
{% endif %}
</div>
</div>
</div>
</section>
<section class="mx-overview-grid">
{% for card in admin_dashboard_data.overview_cards %}
<article class="mx-stat-card mx-stat-card--{{ card.tone }}">
<span class="mx-stat-card__label">{{ card.label }}</span>
<strong class="mx-stat-card__value">{{ card.value }}</strong>
<p class="mx-stat-card__caption">{{ card.caption }}</p>
</article>
{% endfor %}
</section>
<section class="mx-panel mx-period-panel">
<div class="mx-panel__header">
<div>
<p class="mx-eyebrow">Отчетные периоды</p>
<h2>Какие формы загружены по периодам</h2>
</div>
<p class="mx-panel__note">
{{ admin_dashboard_data.period_chart.note }}
</p>
</div>
{% if admin_dashboard_data.period_chart.rows %}
<div class="mx-period-grid-wrap">
<div class="mx-period-grid">
<div class="mx-period-grid__row mx-period-grid__row--head">
<span class="mx-period-grid__head-cell">Период</span>
{% for header in admin_dashboard_data.period_chart.headers %}
<span class="mx-period-grid__head-cell mx-period-grid__head-cell--form">
{{ header.short_title }}
</span>
{% endfor %}
<span class="mx-period-grid__head-cell">Комплект</span>
</div>
{% for row in admin_dashboard_data.period_chart.rows %}
<article class="mx-period-grid__row">
<div class="mx-period-row__period">
<strong>{{ row.period_label }}</strong>
<span>{{ row.summary }}</span>
<small>{{ row.note }}</small>
</div>
{% for cell in row.forms %}
<div
class="mx-period-cell {% if cell.is_loaded %}mx-period-cell--loaded{% else %}mx-period-cell--empty{% endif %}"
style="--mx-period-accent: {{ cell.color }};"
>
<strong>{{ cell.headline }}</strong>
<small>{{ cell.caption }}</small>
</div>
{% endfor %}
<div class="mx-period-row__summary">
<strong>{{ row.loaded_forms_label }}</strong>
<span>{{ row.records_total_label }} записей</span>
<div class="mx-bar-track">
<span style="width: {{ row.bar_width }}%;"></span>
</div>
</div>
</article>
{% endfor %}
</div>
</div>
{% else %}
<p class="mx-empty-state">
После первых загрузок форм здесь появится матрица по отчетным периодам.
</p>
{% endif %}
</section>
<section class="mx-tabs">
<input
class="mx-tab-toggle"
type="radio"
name="mx-dashboard-tab"
id="mx-tab-overview"
checked
>
<input
class="mx-tab-toggle"
type="radio"
name="mx-dashboard-tab"
id="mx-tab-analytics"
>
<input
class="mx-tab-toggle"
type="radio"
name="mx-dashboard-tab"
id="mx-tab-admin"
>
<div class="mx-tab-nav" role="tablist" aria-label="Разделы dashboard">
<label for="mx-tab-overview" class="mx-tab-label">Обзор</label>
<label for="mx-tab-analytics" class="mx-tab-label">Аналитика</label>
<label for="mx-tab-admin" class="mx-tab-label">Админка</label>
</div>
<div class="mx-tab-panels">
<div class="mx-tab-panel mx-tab-panel--overview">
<section class="mx-main-grid">
<div class="mx-panel">
<div class="mx-panel__header">
<div>
<p class="mx-eyebrow">Источники</p>
<h2>Покрытие по формам</h2>
</div>
<p class="mx-panel__note">
{{ admin_dashboard_data.source_cards_note }}
</p>
</div>
<div class="mx-source-grid">
{% for card in admin_dashboard_data.source_cards %}
<article
class="mx-source-card mx-tone--{{ card.status_tone }}"
style="--mx-accent: {{ card.color }}; --mx-bar-value: {{ card.records_bar_width }}%; --mx-org-bar-value: {{ card.organizations_bar_width }}%;"
>
<div class="mx-source-card__top">
<span class="mx-status-pill">{{ card.status_label }}</span>
<span class="mx-source-card__share">{{ card.records_share }}%</span>
</div>
<h3>{{ card.title }}</h3>
<p>{{ card.description }}</p>
<div class="mx-source-card__metrics">
<div>
<span>Записей</span>
<strong>{{ card.records_count_label }}</strong>
</div>
<div>
<span>Организаций</span>
<strong>{{ card.organizations_count_label }}</strong>
</div>
</div>
<div class="mx-meter">
<span></span>
</div>
<div class="mx-meter mx-meter--soft">
<span></span>
</div>
<div class="mx-source-card__footer">
{% if card.last_updated_at %}
<span>Обновлено {{ card.last_updated_at|timesince }} назад</span>
{% else %}
<span>Ещё не загружалось</span>
{% endif %}
<span>{{ card.metrics_scope_label }}</span>
{% if card.error_message %}
<span class="mx-source-card__error">{{ card.error_message|truncatechars:80 }}</span>
{% endif %}
</div>
</article>
{% empty %}
<p class="mx-empty-state">Подключённых источников пока нет.</p>
{% endfor %}
</div>
</div>
<aside class="mx-side-stack">
<section class="mx-panel">
<div class="mx-panel__header">
<div>
<p class="mx-eyebrow">Быстрые действия</p>
<h2>Рабочие входы</h2>
</div>
</div>
<div class="mx-action-list">
{% for action in admin_dashboard_data.quick_actions %}
<a href="{{ action.url }}" class="mx-action-card">
<strong>{{ action.label }}</strong>
<span>{{ action.description }}</span>
</a>
{% empty %}
<p class="mx-empty-state">Быстрые действия недоступны.</p>
{% endfor %}
</div>
</section>
<section class="mx-panel">
<div class="mx-panel__header">
<div>
<p class="mx-eyebrow">Поток загрузок</p>
<h2>Последние события</h2>
</div>
</div>
<div class="mx-feed-list">
{% for event in admin_dashboard_data.activity_feed %}
<article class="mx-feed-item mx-tone--{{ event.tone }}">
<div class="mx-feed-item__meta">
<span class="mx-feed-item__kind">{{ event.kind }}</span>
<time>{{ event.timestamp|date:"d.m.Y H:i" }}</time>
</div>
<strong>{{ event.title }}</strong>
<p>{{ event.meta }}</p>
<span class="mx-feed-item__note">{{ event.note|truncatechars:80 }}</span>
</article>
{% empty %}
<p class="mx-empty-state">После первых импортов здесь появится рабочая лента.</p>
{% endfor %}
</div>
</section>
</aside>
</section>
</div>
<div class="mx-tab-panel mx-tab-panel--analytics">
<section class="mx-split-grid">
<div class="mx-panel">
<div class="mx-panel__header">
<div>
<p class="mx-eyebrow">Формы</p>
<h2>Где больше записей</h2>
</div>
<p class="mx-panel__note">
Раскладка показывает объём записей по каждой форме и долю охвата организаций.
</p>
</div>
<div class="mx-bar-list">
{% for form in admin_dashboard_data.form_rows %}
<article class="mx-bar-item">
<div class="mx-bar-item__head">
<div>
<strong>{{ form.name }}</strong>
<span>{{ form.note }}</span>
</div>
<strong>{{ form.value }}</strong>
</div>
<div class="mx-bar-track">
<span style="width: {{ form.bar_width }}%;"></span>
</div>
</article>
{% empty %}
<p class="mx-empty-state">Формы ещё не загружались.</p>
{% endfor %}
</div>
</div>
<div class="mx-panel">
<div class="mx-panel__header">
<div>
<p class="mx-eyebrow">Организации</p>
<h2>Топ по покрытию формами</h2>
</div>
<p class="mx-panel__note">
{{ admin_dashboard_data.organization_rows_note }}
</p>
</div>
<div class="mx-bar-list mx-bar-list--regions">
{% for organization in admin_dashboard_data.organization_rows %}
<article class="mx-bar-item">
<div class="mx-bar-item__head">
<div>
<strong>{{ organization.name }}</strong>
<span>
{{ organization.records_count_label }} записей
· {{ organization.note }}
</span>
</div>
<strong>{{ organization.forms_count_label }} форм</strong>
</div>
<div class="mx-bar-track mx-bar-track--violet">
<span style="width: {{ organization.bar_width }}%;"></span>
</div>
</article>
{% empty %}
<p class="mx-empty-state">Рейтинг появится после первых загрузок форм.</p>
{% endfor %}
</div>
</div>
</section>
</div>
<div class="mx-tab-panel mx-tab-panel--admin">
<section class="mx-split-grid">
<div class="mx-panel">
<div class="mx-panel__header">
<div>
<p class="mx-eyebrow">Разделы</p>
<h2>Рабочие зоны админки</h2>
</div>
</div>
<div class="mx-app-grid">
{% for app in dashboard_list %}
<article class="mx-app-card">
<div class="mx-app-card__head">
<h3>{{ app.name }}</h3>
<span>{{ app.models|length }} моделей</span>
</div>
<div class="mx-app-card__body">
{% for model in app.models %}
<div class="mx-app-model">
<span class="mx-app-model__name">
{% if model.url %}
<a href="{{ model.url }}">{{ model.name }}</a>
{% else %}
{{ model.name }}
{% endif %}
</span>
<div class="mx-app-model__actions">
{% if model.add_url %}
<a href="{{ model.add_url }}" class="mx-mini-button">Добавить</a>
{% endif %}
{% if model.url %}
<a href="{{ model.url }}" class="mx-mini-button mx-mini-button--ghost">
{% if model.view_only %}Открыть{% else %}Управлять{% endif %}
</a>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</article>
{% empty %}
<p class="mx-empty-state">Доступных разделов не найдено.</p>
{% endfor %}
</div>
</div>
<div class="mx-panel">
<div class="mx-panel__header">
<div>
<p class="mx-eyebrow">Audit</p>
<h2>Последние действия в админке</h2>
</div>
</div>
{% get_admin_log 8 as admin_log for_user user %}
<div class="mx-feed-list">
{% for entry in admin_log %}
<article class="mx-feed-item mx-tone--cyan">
<div class="mx-feed-item__meta">
<span class="mx-feed-item__kind">
{% if entry.is_change %}
Изменение
{% elif entry.is_deletion %}
Удаление
{% elif entry.is_addition %}
Создание
{% else %}
Действие
{% endif %}
</span>
<time>{{ entry.action_time|date:"d.m.Y H:i" }}</time>
</div>
<strong>
{% if entry.is_deletion or not entry.get_admin_url %}
{{ entry.object_repr }}
{% else %}
<a href="{{ entry.get_admin_url }}">{{ entry.object_repr }}</a>
{% endif %}
</strong>
<p>
{% if entry.model %}
{{ entry.model|capfirst }}
{% else %}
Объект без модели
{% endif %}
</p>
<span class="mx-feed-item__note">{{ entry }}</span>
</article>
{% empty %}
<p class="mx-empty-state">История действий пока пустая.</p>
{% endfor %}
</div>
</div>
</section>
</div>
</div>
</section>
</div>
{% endblock %}

View File

@@ -0,0 +1,9 @@
{% extends "admin/change_list.html" %}
{% load admin_urls %}
{% block object-tools-items %}
<a href="{{ upload_backup_url }}" class="btn btn-primary mr-2">
Импортировать backup реестров
</a>
{{ block.super }}
{% endblock %}

View File

@@ -0,0 +1,56 @@
{% extends "admin/base_site.html" %}
{% block breadcrumbs %}
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'admin:index' %}">Главная</a></li>
<li class="breadcrumb-item"><a href="{{ changelist_url }}">Загрузки реестров</a></li>
<li class="breadcrumb-item active">Импорт backup</li>
</ol>
{% endblock %}
{% block content_title %}Импорт backup реестров{% endblock %}
{% block content %}
<div class="row">
<div class="col-12 col-lg-8">
<div class="card">
<div class="card-body">
<p class="text-muted">
Импорт выполняется синхронно. Загрузите архив <code>.zip</code> или файл
<code>.bin</code>, экспортированный из внешней системы.
</p>
<p class="text-muted">
Файл должен содержать зашифрованный backup с данными реестров.
Организаций в текущем справочнике: {{ organizations_count }}.
</p>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-group">
<label for="id_files">Backup файлы</label>
<input
type="file"
name="files"
id="id_files"
class="form-control"
accept=".zip,.bin"
multiple
required
/>
<small class="form-text text-muted">
Поддерживаются архивы <code>.zip</code> с вложенным <code>.bin</code>
и прямые файлы <code>.bin</code>. Для расшифровки используется
<code>BACKUP_ENCRYPTION_KEY</code>.
</small>
</div>
<div class="d-flex align-items-center">
<input type="submit" value="Загрузить" class="btn btn-primary mr-2" />
<a href="{{ changelist_url }}" class="btn btn-outline-secondary">Отмена</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}