From 1a953cc55860f1bd585a2e564b4d5c7266c208ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D1=88=D0=BA=D0=B8=D0=BD=20=D0=A1=D0=B5=D1=80?= =?UTF-8?q?=D0=B3=D0=B5=D0=B9?= Date: Mon, 15 Dec 2025 17:54:26 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=B6=D1=83=D1=80=D0=BD=D0=B0=D0=BB=20=D0=BE=D1=88=D0=B8=D0=B1?= =?UTF-8?q?=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dbapp/mainapp/admin.py | 74 +++ .../migrations/0027_errors_report_models.py | 80 +++ .../0028_remove_issue_type_fields.py | 29 + dbapp/mainapp/models/__init__.py | 8 + dbapp/mainapp/models/errors_report.py | 135 +++- dbapp/mainapp/permissions.py | 6 + .../templates/mainapp/components/_navbar.html | 3 + .../templates/mainapp/errors_report.html | 586 ++++++++++++++++++ dbapp/mainapp/urls.py | 14 + dbapp/mainapp/views/__init__.py | 13 + dbapp/mainapp/views/errors_report.py | 218 +++++++ dbapp/mainapp/views/user_permissions.py | 3 + 12 files changed, 1162 insertions(+), 7 deletions(-) create mode 100644 dbapp/mainapp/migrations/0027_errors_report_models.py create mode 100644 dbapp/mainapp/migrations/0028_remove_issue_type_fields.py create mode 100644 dbapp/mainapp/templates/mainapp/errors_report.html create mode 100644 dbapp/mainapp/views/errors_report.py diff --git a/dbapp/mainapp/admin.py b/dbapp/mainapp/admin.py index 77eacbe..b292c91 100644 --- a/dbapp/mainapp/admin.py +++ b/dbapp/mainapp/admin.py @@ -1380,3 +1380,77 @@ class SourceRequestStatusHistoryAdmin(BaseAdmin): def has_change_permission(self, request, obj=None): return False + + +# ============================================================================ +# Errors Report Admin +# ============================================================================ + +from .models import IssueType, DailyReport, DowntimePeriod, IssueMark + + +class DowntimePeriodInline(admin.TabularInline): + """Inline для периодов простоя.""" + model = DowntimePeriod + extra = 1 + fields = ('start_time', 'end_time', 'reason') + + +class IssueMarkInline(admin.TabularInline): + """Inline для отметок об ошибках.""" + model = IssueMark + extra = 0 + fields = ('issue_type', 'is_present') + autocomplete_fields = ('issue_type',) + + +@admin.register(IssueType) +class IssueTypeAdmin(BaseAdmin): + """Админ-панель для типов ошибок/неисправностей.""" + + list_display = ('name', 'category') + list_filter = ('category',) + search_fields = ('name',) + ordering = ('category', 'name') + + +@admin.register(DailyReport) +class DailyReportAdmin(BaseAdmin): + """Админ-панель для ежедневных отчётов.""" + + list_display = ( + 'date', + 'daily_work_hours', + 'weekly_work_hours', + 'downtime_count', + 'issues_count', + 'created_at', + ) + list_filter = ( + ('date', DateRangeQuickSelectListFilterBuilder()), + ) + search_fields = ('explanation', 'comment') + ordering = ('-date',) + readonly_fields = ('created_at', 'updated_at', 'created_by') + inlines = [DowntimePeriodInline, IssueMarkInline] + + fieldsets = ( + ('Основная информация', { + 'fields': ('date', 'daily_work_hours', 'weekly_work_hours') + }), + ('Примечания', { + 'fields': ('explanation', 'comment') + }), + ('Метаданные', { + 'fields': ('created_at', 'updated_at', 'created_by'), + 'classes': ('collapse',) + }), + ) + + def downtime_count(self, obj): + return obj.downtime_periods.count() + downtime_count.short_description = 'Простоев' + + def issues_count(self, obj): + return obj.issue_marks.filter(is_present=True).count() + issues_count.short_description = 'Ошибок/неисправностей' diff --git a/dbapp/mainapp/migrations/0027_errors_report_models.py b/dbapp/mainapp/migrations/0027_errors_report_models.py new file mode 100644 index 0000000..62b743d --- /dev/null +++ b/dbapp/mainapp/migrations/0027_errors_report_models.py @@ -0,0 +1,80 @@ +# Generated by Django 5.2.7 on 2025-12-15 13:21 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mainapp', '0026_alter_userpermission_code'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='IssueType', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='Название')), + ('category', models.CharField(choices=[('error', 'Ошибка'), ('malfunction', 'Неисправность')], default='error', max_length=20, verbose_name='Категория')), + ('order', models.PositiveIntegerField(default=0, verbose_name='Порядок сортировки')), + ('is_active', models.BooleanField(default=True, verbose_name='Активен')), + ], + options={ + 'verbose_name': 'Тип ошибки/неисправности', + 'verbose_name_plural': 'Типы ошибок/неисправностей', + 'ordering': ['category', 'order', 'name'], + }, + ), + migrations.CreateModel( + name='DailyReport', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField(db_index=True, help_text='Дата отчёта', unique=True, verbose_name='Дата')), + ('daily_work_hours', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='Время работы за день (ч)')), + ('weekly_work_hours', models.DecimalField(decimal_places=2, default=0, max_digits=6, verbose_name='Время работы за неделю (ч)')), + ('explanation', models.TextField(blank=True, null=True, verbose_name='Пояснение')), + ('comment', models.TextField(blank=True, null=True, verbose_name='Комментарий')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Создано')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Обновлено')), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='daily_reports_created', to=settings.AUTH_USER_MODEL, verbose_name='Создал')), + ], + options={ + 'verbose_name': 'Ежедневный отчёт', + 'verbose_name_plural': 'Ежедневные отчёты', + 'ordering': ['-date'], + }, + ), + migrations.CreateModel( + name='DowntimePeriod', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('start_time', models.TimeField(verbose_name='Время начала')), + ('end_time', models.TimeField(verbose_name='Время окончания')), + ('reason', models.TextField(verbose_name='Причина простоя')), + ('report', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='downtime_periods', to='mainapp.dailyreport', verbose_name='Отчёт')), + ], + options={ + 'verbose_name': 'Период простоя', + 'verbose_name_plural': 'Периоды простоя', + 'ordering': ['start_time'], + }, + ), + migrations.CreateModel( + name='IssueMark', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_present', models.BooleanField(default=False, verbose_name='Наличие')), + ('report', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_marks', to='mainapp.dailyreport', verbose_name='Отчёт')), + ('issue_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='marks', to='mainapp.issuetype', verbose_name='Тип ошибки/неисправности')), + ], + options={ + 'verbose_name': 'Отметка об ошибке', + 'verbose_name_plural': 'Отметки об ошибках', + 'ordering': ['issue_type__category', 'issue_type__order'], + 'unique_together': {('report', 'issue_type')}, + }, + ), + ] diff --git a/dbapp/mainapp/migrations/0028_remove_issue_type_fields.py b/dbapp/mainapp/migrations/0028_remove_issue_type_fields.py new file mode 100644 index 0000000..59fb42b --- /dev/null +++ b/dbapp/mainapp/migrations/0028_remove_issue_type_fields.py @@ -0,0 +1,29 @@ +# Generated by Django 5.2.7 on 2025-12-15 13:26 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('mainapp', '0027_errors_report_models'), + ] + + operations = [ + migrations.AlterModelOptions( + name='issuemark', + options={'ordering': ['issue_type__category', 'issue_type__name'], 'verbose_name': 'Отметка об ошибке', 'verbose_name_plural': 'Отметки об ошибках'}, + ), + migrations.AlterModelOptions( + name='issuetype', + options={'ordering': ['category', 'name'], 'verbose_name': 'Тип ошибки/неисправности', 'verbose_name_plural': 'Типы ошибок/неисправностей'}, + ), + migrations.RemoveField( + model_name='issuetype', + name='is_active', + ), + migrations.RemoveField( + model_name='issuetype', + name='order', + ), + ] diff --git a/dbapp/mainapp/models/__init__.py b/dbapp/mainapp/models/__init__.py index 8795094..2bde817 100644 --- a/dbapp/mainapp/models/__init__.py +++ b/dbapp/mainapp/models/__init__.py @@ -26,6 +26,9 @@ from .tech_analyze import TechAnalyze, ObjectMark # Заявки from .requests import SourceRequest, SourceRequestStatusHistory +# Отчёты об ошибках +from .errors_report import IssueType, DailyReport, DowntimePeriod, IssueMark + # Вспомогательные функции для default значений from .defaults import ( get_default_polarization, @@ -62,6 +65,11 @@ __all__ = [ # Заявки 'SourceRequest', 'SourceRequestStatusHistory', + # Отчёты об ошибках + 'IssueType', + 'DailyReport', + 'DowntimePeriod', + 'IssueMark', # Функции 'get_default_polarization', 'get_default_modulation', diff --git a/dbapp/mainapp/models/errors_report.py b/dbapp/mainapp/models/errors_report.py index 856cfda..723c4ea 100644 --- a/dbapp/mainapp/models/errors_report.py +++ b/dbapp/mainapp/models/errors_report.py @@ -1,9 +1,130 @@ from django.db import models +from django.contrib.auth import get_user_model -# class IssueType(models.Model): -# name = models.CharField(max_length=100) -# CATEGORY_CHOICES = [ -# ('error', 'Ошибка'), -# ('malfunction', 'Неисправность'), -# ] -# category = models.CharField(max_length=12, choices=CATEGORY_CHOICES) \ No newline at end of file +User = get_user_model() + + +class IssueType(models.Model): + """Тип ошибки или неисправности""" + CATEGORY_CHOICES = [ + ('error', 'Ошибка'), + ('malfunction', 'Неисправность'), + ] + + name = models.CharField(max_length=255, verbose_name="Название") + category = models.CharField( + max_length=20, + choices=CATEGORY_CHOICES, + default='error', + verbose_name='Категория' + ) + + def __str__(self): + return f"{self.name} ({self.get_category_display()})" + + class Meta: + verbose_name = "Тип ошибки/неисправности" + verbose_name_plural = "Типы ошибок/неисправностей" + ordering = ["category", "name"] + + +class DailyReport(models.Model): + """Ежедневный отчёт""" + date = models.DateField( + unique=True, + verbose_name="Дата", + db_index=True, + help_text="Дата отчёта" + ) + daily_work_hours = models.DecimalField( + max_digits=5, + decimal_places=2, + default=0, + verbose_name="Время работы за день (ч)" + ) + weekly_work_hours = models.DecimalField( + max_digits=6, + decimal_places=2, + default=0, + verbose_name="Время работы за неделю (ч)" + ) + explanation = models.TextField(blank=True, null=True, verbose_name='Пояснение') + comment = models.TextField(blank=True, null=True, verbose_name='Комментарий') + + created_at = models.DateTimeField(auto_now_add=True, verbose_name="Создано") + updated_at = models.DateTimeField(auto_now=True, verbose_name="Обновлено") + created_by = models.ForeignKey( + User, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='daily_reports_created', + verbose_name="Создал" + ) + + def __str__(self): + return f"Отчёт за {self.date}" + + class Meta: + verbose_name = "Ежедневный отчёт" + verbose_name_plural = "Ежедневные отчёты" + ordering = ["-date"] + + +class DowntimePeriod(models.Model): + """Период простоя""" + report = models.ForeignKey( + DailyReport, + on_delete=models.CASCADE, + related_name='downtime_periods', + verbose_name="Отчёт" + ) + start_time = models.TimeField(verbose_name="Время начала") + end_time = models.TimeField(verbose_name="Время окончания") + reason = models.TextField(verbose_name="Причина простоя") + + def __str__(self): + return f"{self.start_time.strftime('%H:%M')}-{self.end_time.strftime('%H:%M')}: {self.reason[:30]}" + + @property + def duration_hours(self): + """Длительность простоя в часах""" + from datetime import datetime, timedelta + start = datetime.combine(datetime.today(), self.start_time) + end = datetime.combine(datetime.today(), self.end_time) + if end < start: + end += timedelta(days=1) + delta = end - start + return delta.total_seconds() / 3600 + + class Meta: + verbose_name = "Период простоя" + verbose_name_plural = "Периоды простоя" + ordering = ["start_time"] + + +class IssueMark(models.Model): + """Отметка об ошибке/неисправности в отчёте""" + report = models.ForeignKey( + DailyReport, + on_delete=models.CASCADE, + related_name='issue_marks', + verbose_name="Отчёт" + ) + issue_type = models.ForeignKey( + IssueType, + on_delete=models.CASCADE, + related_name='marks', + verbose_name="Тип ошибки/неисправности" + ) + is_present = models.BooleanField(default=False, verbose_name="Наличие") + + def __str__(self): + mark = "+" if self.is_present else "-" + return f"{self.report.date} - {self.issue_type.name}: {mark}" + + class Meta: + verbose_name = "Отметка об ошибке" + verbose_name_plural = "Отметки об ошибках" + unique_together = ['report', 'issue_type'] + ordering = ["issue_type__category", "issue_type__name"] diff --git a/dbapp/mainapp/permissions.py b/dbapp/mainapp/permissions.py index 60ebfe7..75f86b9 100644 --- a/dbapp/mainapp/permissions.py +++ b/dbapp/mainapp/permissions.py @@ -63,6 +63,11 @@ PERMISSIONS = [ ('transponder_delete', 'Удаление транспондера', 'Удаление транспондера'), ('transponder_import_xml', 'Импорт транспондеров из XML', 'Загрузка транспондеров из XML файла'), + # Errors Report (Журнал ошибок) + ('errors_report_create', 'Создание записи журнала ошибок', 'Создание новой записи в журнале ошибок'), + ('errors_report_edit', 'Редактирование записи журнала ошибок', 'Редактирование записи в журнале ошибок'), + ('errors_report_delete', 'Удаление записи журнала ошибок', 'Удаление записи из журнала ошибок'), + # Admin access # ('admin_access', 'Доступ к админ-панели', 'Доступ к административной панели Django'), ] @@ -85,6 +90,7 @@ DEFAULT_ROLE_PERMISSIONS = { 'mark_create', 'mark_edit', 'statistics_view', 'kubsat_view', 'kubsat_edit', + 'errors_report_create', 'errors_report_edit', ], 'user': [ 'statistics_view', diff --git a/dbapp/mainapp/templates/mainapp/components/_navbar.html b/dbapp/mainapp/templates/mainapp/components/_navbar.html index 19560b5..a9b7608 100644 --- a/dbapp/mainapp/templates/mainapp/components/_navbar.html +++ b/dbapp/mainapp/templates/mainapp/components/_navbar.html @@ -38,6 +38,9 @@ + {% if user|has_perm:'kubsat_view' %}