Добавил журнал ошибок
This commit is contained in:
@@ -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 = 'Ошибок/неисправностей'
|
||||
|
||||
80
dbapp/mainapp/migrations/0027_errors_report_models.py
Normal file
80
dbapp/mainapp/migrations/0027_errors_report_models.py
Normal file
@@ -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')},
|
||||
},
|
||||
),
|
||||
]
|
||||
29
dbapp/mainapp/migrations/0028_remove_issue_type_fields.py
Normal file
29
dbapp/mainapp/migrations/0028_remove_issue_type_fields.py
Normal file
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
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"]
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -38,6 +38,9 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mainapp:signal_marks' %}">Отметки сигналов</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mainapp:errors_report' %}">Журнал ошибок</a>
|
||||
</li>
|
||||
{% if user|has_perm:'kubsat_view' %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'mainapp:kubsat' %}">Кубсат</a>
|
||||
|
||||
586
dbapp/mainapp/templates/mainapp/errors_report.html
Normal file
586
dbapp/mainapp/templates/mainapp/errors_report.html
Normal file
@@ -0,0 +1,586 @@
|
||||
{% extends "mainapp/base.html" %}
|
||||
{% load static %}
|
||||
{% load permission_tags %}
|
||||
|
||||
{% block title %}Журнал ошибок{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.mark-cell {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
width: 60px;
|
||||
}
|
||||
.mark-positive {
|
||||
background-color: #d4edda !important;
|
||||
color: #155724;
|
||||
}
|
||||
.mark-negative {
|
||||
background-color: #f8d7da !important;
|
||||
color: #721c24;
|
||||
}
|
||||
.mark-empty {
|
||||
color: #6c757d;
|
||||
}
|
||||
.downtime-cell {
|
||||
font-size: 0.85em;
|
||||
max-width: 250px;
|
||||
}
|
||||
.table-responsive {
|
||||
max-height: calc(100vh - 250px);
|
||||
overflow: auto;
|
||||
}
|
||||
.table thead th {
|
||||
background: #343a40;
|
||||
color: white;
|
||||
font-size: 0.85em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.table thead th.issue-header {
|
||||
padding: 5px;
|
||||
font-size: 0.75em;
|
||||
min-width: 60px;
|
||||
max-width: 100px;
|
||||
word-wrap: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
.filter-panel {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.btn-edit {
|
||||
padding: 2px 6px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.weekly-hours-cell {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: bold;
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
/* Модальное окно */
|
||||
.downtime-item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
.downtime-item input[type="time"] {
|
||||
width: 110px;
|
||||
}
|
||||
.downtime-item input[type="text"] {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid mt-3">
|
||||
<h4>Журнал ошибок и неисправностей</h4>
|
||||
|
||||
<div class="filter-panel d-flex gap-3 align-items-end flex-wrap">
|
||||
<div>
|
||||
<label class="form-label">Дата с:</label>
|
||||
<input type="date" id="dateFrom" class="form-control form-control-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Дата по:</label>
|
||||
<input type="date" id="dateTo" class="form-control form-control-sm">
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm" onclick="loadData()">
|
||||
Применить
|
||||
</button>
|
||||
{% if user|has_perm:'errors_report_create' %}
|
||||
<button class="btn btn-success btn-sm" onclick="openCreateModal()">
|
||||
<i class="bi bi-plus-circle"></i> Создать
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover table-sm" id="reportTable">
|
||||
<thead>
|
||||
<tr id="headerRowTop">
|
||||
</tr>
|
||||
<tr id="headerRowBottom">
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="reportBody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для создания/редактирования -->
|
||||
<div class="modal fade" id="editModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modalTitle">Новая запись</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="editForm">
|
||||
<input type="hidden" id="editId">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Дата</label>
|
||||
<input type="date" id="editDate" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Время работы за день (ч)</label>
|
||||
<input type="number" step="0.01" min="0" id="editDailyHours" class="form-control" value="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Периоды простоя</label>
|
||||
<div id="editDowntimeList"></div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm mt-2" onclick="addDowntimeRow()">
|
||||
<i class="bi bi-plus"></i> Добавить период
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<h6>Ошибки</h6>
|
||||
<div id="editErrors"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>Неисправности</h6>
|
||||
<div id="editMalfunctions"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Пояснение</label>
|
||||
<textarea id="editExplanation" class="form-control" rows="2"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Комментарий</label>
|
||||
<textarea id="editComment" class="form-control" rows="2"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
{% if user|has_perm:'errors_report_delete' %}
|
||||
<button type="button" class="btn btn-danger me-auto" id="btnDelete" onclick="deleteRecord()" style="display: none;">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveRecord()">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
const issueTypes = {
|
||||
errors: [
|
||||
{% for e in errors %}
|
||||
{id: {{ e.id }}, name: "{{ e.name|escapejs }}"},
|
||||
{% endfor %}
|
||||
],
|
||||
malfunctions: [
|
||||
{% for m in malfunctions %}
|
||||
{id: {{ m.id }}, name: "{{ m.name|escapejs }}"},
|
||||
{% endfor %}
|
||||
]
|
||||
};
|
||||
|
||||
let currentData = [];
|
||||
let currentColumns = [];
|
||||
let editModal = null;
|
||||
|
||||
// Инициализация
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
editModal = new bootstrap.Modal(document.getElementById('editModal'));
|
||||
loadData();
|
||||
});
|
||||
|
||||
// Загрузка данных
|
||||
async function loadData() {
|
||||
const dateFrom = document.getElementById('dateFrom').value;
|
||||
const dateTo = document.getElementById('dateTo').value;
|
||||
|
||||
let url = '{% url "mainapp:errors_report_api" %}?';
|
||||
if (dateFrom) url += `date_from=${dateFrom}&`;
|
||||
if (dateTo) url += `date_to=${dateTo}&`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const result = await response.json();
|
||||
|
||||
currentData = result.data;
|
||||
currentColumns = result.columns;
|
||||
|
||||
renderTable();
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки:', error);
|
||||
alert('Ошибка загрузки данных');
|
||||
}
|
||||
}
|
||||
|
||||
// Получить номер ISO недели для даты
|
||||
function getWeekKey(dateStr) {
|
||||
const date = new Date(dateStr);
|
||||
// Получаем понедельник этой недели
|
||||
const day = date.getDay();
|
||||
const diff = date.getDate() - day + (day === 0 ? -6 : 1);
|
||||
const monday = new Date(date.setDate(diff));
|
||||
// Формируем ключ: год-номер_недели
|
||||
const year = monday.getFullYear();
|
||||
const startOfYear = new Date(year, 0, 1);
|
||||
const weekNum = Math.ceil(((monday - startOfYear) / 86400000 + startOfYear.getDay() + 1) / 7);
|
||||
return `${year}-W${weekNum}`;
|
||||
}
|
||||
|
||||
// Группировка данных по неделям и расчёт rowspan
|
||||
function calculateWeekGroups(data) {
|
||||
const weekGroups = {};
|
||||
|
||||
// Группируем записи по неделям
|
||||
data.forEach((row, idx) => {
|
||||
const weekKey = getWeekKey(row.date);
|
||||
if (!weekGroups[weekKey]) {
|
||||
weekGroups[weekKey] = {
|
||||
rows: [],
|
||||
totalHours: 0
|
||||
};
|
||||
}
|
||||
weekGroups[weekKey].rows.push(idx);
|
||||
weekGroups[weekKey].totalHours += parseFloat(row.daily_work_hours) || 0;
|
||||
});
|
||||
|
||||
// Определяем для каждой строки: нужно ли рендерить ячейку и с каким rowspan
|
||||
const rowInfo = {};
|
||||
Object.values(weekGroups).forEach(group => {
|
||||
const firstRowIdx = group.rows[0];
|
||||
group.rows.forEach((idx, i) => {
|
||||
rowInfo[idx] = {
|
||||
isFirst: i === 0,
|
||||
rowspan: group.rows.length,
|
||||
weeklyTotal: group.totalHours.toFixed(2)
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return rowInfo;
|
||||
}
|
||||
|
||||
// Рендер таблицы
|
||||
function renderTable() {
|
||||
const errors = currentColumns.filter(c => c.category === 'error');
|
||||
const malfunctions = currentColumns.filter(c => c.category === 'malfunction');
|
||||
|
||||
// Верхний ряд заголовков
|
||||
const headerRowTop = document.getElementById('headerRowTop');
|
||||
let topHtml = `
|
||||
<th rowspan="2" style="width: 50px;"></th>
|
||||
<th rowspan="2" style="width: 120px;">Дата</th>
|
||||
<th rowspan="2" style="min-width: 200px;">Простои</th>
|
||||
`;
|
||||
|
||||
if (errors.length > 0) {
|
||||
topHtml += `<th colspan="${errors.length}" class="text-center">Ошибки</th>`;
|
||||
}
|
||||
if (malfunctions.length > 0) {
|
||||
topHtml += `<th colspan="${malfunctions.length}" class="text-center">Неисправности</th>`;
|
||||
}
|
||||
|
||||
topHtml += `
|
||||
<th rowspan="2" style="width: 80px;">Раб. ч/день</th>
|
||||
<th rowspan="2" style="width: 100px;">Раб. ч/нед.</th>
|
||||
<th rowspan="2" style="min-width: 150px;">Пояснение</th>
|
||||
<th rowspan="2" style="min-width: 150px;">Комментарий</th>
|
||||
`;
|
||||
headerRowTop.innerHTML = topHtml;
|
||||
|
||||
// Нижний ряд заголовков (названия ошибок/неисправностей)
|
||||
const headerRowBottom = document.getElementById('headerRowBottom');
|
||||
let bottomHtml = '';
|
||||
|
||||
errors.forEach(col => {
|
||||
bottomHtml += `<th class="issue-header" title="${col.name}">${col.name}</th>`;
|
||||
});
|
||||
|
||||
malfunctions.forEach(col => {
|
||||
bottomHtml += `<th class="issue-header" title="${col.name}">${col.name}</th>`;
|
||||
});
|
||||
|
||||
headerRowBottom.innerHTML = bottomHtml;
|
||||
|
||||
// Рендерим строки
|
||||
const tbody = document.getElementById('reportBody');
|
||||
if (currentData.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="100" class="text-center text-muted">Нет данных</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Рассчитываем группы по неделям
|
||||
const weekInfo = calculateWeekGroups(currentData);
|
||||
|
||||
let bodyHtml = '';
|
||||
currentData.forEach((row, idx) => {
|
||||
const date = new Date(row.date);
|
||||
const dateStr = date.toLocaleDateString('ru-RU');
|
||||
const dayStr = date.toLocaleDateString('ru-RU', { weekday: 'short' });
|
||||
const fullDateStr = dateStr + ' (' + dayStr + ')';
|
||||
const info = weekInfo[idx];
|
||||
|
||||
bodyHtml += `<tr>`;
|
||||
{% if user|has_perm:'errors_report_edit' %}
|
||||
bodyHtml += `<td><button class="btn btn-outline-primary btn-edit" onclick="openEditModal(${row.id})"><i class="bi bi-pencil"></i></button></td>`;
|
||||
{% else %}
|
||||
bodyHtml += `<td>${row.id}</td>`;
|
||||
{% endif %}
|
||||
bodyHtml += `<td>${fullDateStr}</td>`;
|
||||
bodyHtml += `<td class="downtime-cell">${row.downtimes_display || '—'}</td>`;
|
||||
|
||||
// Ошибки
|
||||
errors.forEach(col => {
|
||||
const value = row[col.field];
|
||||
bodyHtml += renderMarkCell(value);
|
||||
});
|
||||
|
||||
// Неисправности
|
||||
malfunctions.forEach(col => {
|
||||
const value = row[col.field];
|
||||
bodyHtml += renderMarkCell(value);
|
||||
});
|
||||
|
||||
bodyHtml += `<td class="text-center">${row.daily_work_hours || 0}</td>`;
|
||||
|
||||
// Ячейка "Раб. ч/нед." с rowspan
|
||||
if (info.isFirst) {
|
||||
bodyHtml += `<td class="text-center align-middle weekly-hours-cell" rowspan="${info.rowspan}">${info.weeklyTotal}</td>`;
|
||||
}
|
||||
|
||||
bodyHtml += `<td>${row.explanation || ''}</td>`;
|
||||
bodyHtml += `<td>${row.comment || ''}</td>`;
|
||||
bodyHtml += `</tr>`;
|
||||
});
|
||||
|
||||
tbody.innerHTML = bodyHtml;
|
||||
}
|
||||
|
||||
// Рендер ячейки с отметкой
|
||||
function renderMarkCell(value) {
|
||||
if (value === true) {
|
||||
return '<td class="mark-cell mark-positive"><i class="bi bi-check-lg"></i></td>';
|
||||
} else if (value === false) {
|
||||
return '<td class="mark-cell mark-negative"><i class="bi bi-x-lg"></i></td>';
|
||||
}
|
||||
return '<td class="mark-cell mark-empty">—</td>';
|
||||
}
|
||||
|
||||
// Открыть модальное окно для создания
|
||||
function openCreateModal() {
|
||||
document.getElementById('modalTitle').textContent = 'Новая запись';
|
||||
document.getElementById('editId').value = '';
|
||||
document.getElementById('editDate').value = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('editDailyHours').value = 0;
|
||||
document.getElementById('editExplanation').value = '';
|
||||
document.getElementById('editComment').value = '';
|
||||
|
||||
const btnDelete = document.getElementById('btnDelete');
|
||||
if (btnDelete) btnDelete.style.display = 'none';
|
||||
|
||||
// Очищаем простои
|
||||
document.getElementById('editDowntimeList').innerHTML = '';
|
||||
|
||||
// Заполняем чекбоксы
|
||||
renderIssueCheckboxes({});
|
||||
|
||||
editModal.show();
|
||||
}
|
||||
|
||||
// Открыть модальное окно для редактирования
|
||||
async function openEditModal(id) {
|
||||
const row = currentData.find(r => r.id === id);
|
||||
if (!row) return;
|
||||
|
||||
document.getElementById('modalTitle').textContent = 'Редактирование записи';
|
||||
document.getElementById('editId').value = row.id;
|
||||
document.getElementById('editDate').value = row.date;
|
||||
document.getElementById('editDailyHours').value = row.daily_work_hours || 0;
|
||||
document.getElementById('editExplanation').value = row.explanation || '';
|
||||
document.getElementById('editComment').value = row.comment || '';
|
||||
|
||||
const btnDelete = document.getElementById('btnDelete');
|
||||
if (btnDelete) btnDelete.style.display = 'block';
|
||||
|
||||
// Заполняем простои
|
||||
renderDowntimes(row.downtimes || []);
|
||||
|
||||
// Заполняем чекбоксы
|
||||
renderIssueCheckboxes(row);
|
||||
|
||||
editModal.show();
|
||||
}
|
||||
|
||||
// Рендер периодов простоя
|
||||
function renderDowntimes(downtimes) {
|
||||
const container = document.getElementById('editDowntimeList');
|
||||
let html = '';
|
||||
downtimes.forEach((dt, idx) => {
|
||||
html += createDowntimeRowHtml(idx, dt.start || '', dt.end || '', dt.reason || '');
|
||||
});
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// Создать HTML для строки простоя
|
||||
function createDowntimeRowHtml(idx, start, end, reason) {
|
||||
return `
|
||||
<div class="downtime-item" data-idx="${idx}">
|
||||
<input type="time" class="form-control form-control-sm dt-start" value="${start}">
|
||||
<span>—</span>
|
||||
<input type="time" class="form-control form-control-sm dt-end" value="${end}">
|
||||
<input type="text" class="form-control form-control-sm dt-reason" placeholder="Причина" value="${reason}">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeDowntimeRow(this)">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Добавить период простоя
|
||||
function addDowntimeRow() {
|
||||
const container = document.getElementById('editDowntimeList');
|
||||
const items = container.querySelectorAll('.downtime-item');
|
||||
const newIdx = items.length;
|
||||
|
||||
// Создаём новый элемент без затрагивания существующих
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = createDowntimeRowHtml(newIdx, '', '', '').trim();
|
||||
container.appendChild(template.content.firstChild);
|
||||
}
|
||||
|
||||
// Удалить период простоя
|
||||
function removeDowntimeRow(btn) {
|
||||
btn.closest('.downtime-item').remove();
|
||||
}
|
||||
|
||||
// Рендер чекбоксов ошибок/неисправностей
|
||||
function renderIssueCheckboxes(row) {
|
||||
let errorsHtml = '';
|
||||
issueTypes.errors.forEach(e => {
|
||||
const checked = row[`issue_${e.id}`] ? 'checked' : '';
|
||||
errorsHtml += `
|
||||
<div class="form-check">
|
||||
<input class="form-check-input issue-check" type="checkbox" id="issue_${e.id}" data-issue-id="${e.id}" ${checked}>
|
||||
<label class="form-check-label" for="issue_${e.id}">${e.name}</label>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
document.getElementById('editErrors').innerHTML = errorsHtml;
|
||||
|
||||
let malfHtml = '';
|
||||
issueTypes.malfunctions.forEach(m => {
|
||||
const checked = row[`issue_${m.id}`] ? 'checked' : '';
|
||||
malfHtml += `
|
||||
<div class="form-check">
|
||||
<input class="form-check-input issue-check" type="checkbox" id="issue_${m.id}" data-issue-id="${m.id}" ${checked}>
|
||||
<label class="form-check-label" for="issue_${m.id}">${m.name}</label>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
document.getElementById('editMalfunctions').innerHTML = malfHtml;
|
||||
}
|
||||
|
||||
// Собрать данные простоев из формы
|
||||
function collectDowntimes() {
|
||||
const items = document.querySelectorAll('#editDowntimeList .downtime-item');
|
||||
const result = [];
|
||||
items.forEach(item => {
|
||||
const start = item.querySelector('.dt-start').value;
|
||||
const end = item.querySelector('.dt-end').value;
|
||||
const reason = item.querySelector('.dt-reason').value;
|
||||
if (start && end) {
|
||||
result.push({start, end, reason});
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// Сохранение записи
|
||||
async function saveRecord() {
|
||||
const date = document.getElementById('editDate').value;
|
||||
if (!date) {
|
||||
alert('Укажите дату');
|
||||
return;
|
||||
}
|
||||
|
||||
// Собираем отметки
|
||||
const issueMarks = {};
|
||||
document.querySelectorAll('.issue-check').forEach(cb => {
|
||||
issueMarks[cb.dataset.issueId] = cb.checked;
|
||||
});
|
||||
|
||||
const data = {
|
||||
id: document.getElementById('editId').value || null,
|
||||
date: date,
|
||||
daily_work_hours: parseFloat(document.getElementById('editDailyHours').value) || 0,
|
||||
explanation: document.getElementById('editExplanation').value,
|
||||
comment: document.getElementById('editComment').value,
|
||||
downtimes: collectDowntimes(),
|
||||
issue_marks: issueMarks
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('{% url "mainapp:errors_report_save" %}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token }}'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
editModal.hide();
|
||||
loadData();
|
||||
} else {
|
||||
alert('Ошибка: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения:', error);
|
||||
alert('Ошибка сохранения');
|
||||
}
|
||||
}
|
||||
|
||||
// Удаление записи
|
||||
async function deleteRecord() {
|
||||
const id = document.getElementById('editId').value;
|
||||
if (!id) return;
|
||||
|
||||
if (!confirm('Удалить эту запись?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('{% url "mainapp:errors_report_delete" %}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token }}'
|
||||
},
|
||||
body: JSON.stringify({id: id})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
editModal.hide();
|
||||
loadData();
|
||||
} else {
|
||||
alert('Ошибка: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления:', error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -101,6 +101,13 @@ from .views.user_permissions import (
|
||||
UserPermissionsApiView,
|
||||
InitPermissionsView,
|
||||
)
|
||||
from .views.errors_report import (
|
||||
ErrorsReportView,
|
||||
ErrorsReportAPIView,
|
||||
ErrorsReportSaveAPIView,
|
||||
ErrorsReportDeleteAPIView,
|
||||
WeeklyHoursAPIView,
|
||||
)
|
||||
|
||||
app_name = 'mainapp'
|
||||
|
||||
@@ -208,4 +215,11 @@ urlpatterns = [
|
||||
path('user-permissions/<int:pk>/edit/', UserPermissionsEditView.as_view(), name='user_permissions_edit'),
|
||||
path('user-permissions/init/', InitPermissionsView.as_view(), name='init_permissions'),
|
||||
path('api/user-permissions/<int:pk>/', UserPermissionsApiView.as_view(), name='user_permissions_api'),
|
||||
|
||||
# Errors Report (Журнал ошибок)
|
||||
path('errors-report/', ErrorsReportView.as_view(), name='errors_report'),
|
||||
path('api/errors-report/', ErrorsReportAPIView.as_view(), name='errors_report_api'),
|
||||
path('api/errors-report/save/', ErrorsReportSaveAPIView.as_view(), name='errors_report_save'),
|
||||
path('api/errors-report/delete/', ErrorsReportDeleteAPIView.as_view(), name='errors_report_delete'),
|
||||
path('api/errors-report/weekly-hours/', WeeklyHoursAPIView.as_view(), name='weekly_hours_api'),
|
||||
]
|
||||
@@ -85,6 +85,13 @@ from .source_requests import (
|
||||
SourceRequestAPIView,
|
||||
SourceRequestDetailAPIView,
|
||||
)
|
||||
from .errors_report import (
|
||||
ErrorsReportView,
|
||||
ErrorsReportAPIView,
|
||||
ErrorsReportSaveAPIView,
|
||||
ErrorsReportDeleteAPIView,
|
||||
WeeklyHoursAPIView,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Base
|
||||
@@ -170,4 +177,10 @@ __all__ = [
|
||||
'SourceRequestDeleteView',
|
||||
'SourceRequestAPIView',
|
||||
'SourceRequestDetailAPIView',
|
||||
# Errors Report
|
||||
'ErrorsReportView',
|
||||
'ErrorsReportAPIView',
|
||||
'ErrorsReportSaveAPIView',
|
||||
'ErrorsReportDeleteAPIView',
|
||||
'WeeklyHoursAPIView',
|
||||
]
|
||||
|
||||
218
dbapp/mainapp/views/errors_report.py
Normal file
218
dbapp/mainapp/views/errors_report.py
Normal file
@@ -0,0 +1,218 @@
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from mainapp.permissions import PermissionRequiredMixin
|
||||
from django.views.generic import TemplateView, View
|
||||
from django.http import JsonResponse
|
||||
from django.db import transaction
|
||||
from django.db.models import Sum
|
||||
|
||||
from mainapp.models import IssueType, DailyReport, DowntimePeriod, IssueMark
|
||||
|
||||
|
||||
class ErrorsReportView(TemplateView, LoginRequiredMixin, PermissionRequiredMixin):
|
||||
"""Страница отчётов об ошибках"""
|
||||
template_name = 'mainapp/errors_report.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['issue_types'] = IssueType.objects.all().order_by('category', 'name')
|
||||
context['errors'] = context['issue_types'].filter(category='error')
|
||||
context['malfunctions'] = context['issue_types'].filter(category='malfunction')
|
||||
context['full_width_page'] = True
|
||||
return context
|
||||
|
||||
|
||||
class ErrorsReportAPIView(View, LoginRequiredMixin, PermissionRequiredMixin):
|
||||
"""API для получения данных отчётов"""
|
||||
|
||||
def get(self, request):
|
||||
# Получаем параметры фильтрации
|
||||
date_from = request.GET.get('date_from')
|
||||
date_to = request.GET.get('date_to')
|
||||
|
||||
reports = DailyReport.objects.all()
|
||||
|
||||
if date_from:
|
||||
reports = reports.filter(date__gte=date_from)
|
||||
if date_to:
|
||||
reports = reports.filter(date__lte=date_to)
|
||||
|
||||
reports = reports.prefetch_related('downtime_periods', 'issue_marks__issue_type')
|
||||
|
||||
# Получаем все типы ошибок/неисправностей
|
||||
issue_types = IssueType.objects.all().order_by('category', 'name')
|
||||
|
||||
data = []
|
||||
for report in reports:
|
||||
# Формируем строку простоев
|
||||
downtimes = []
|
||||
for dt in report.downtime_periods.all():
|
||||
downtimes.append({
|
||||
'id': dt.id,
|
||||
'start': dt.start_time.strftime('%H:%M'),
|
||||
'end': dt.end_time.strftime('%H:%M'),
|
||||
'reason': dt.reason
|
||||
})
|
||||
|
||||
# Формируем отметки
|
||||
marks_dict = {m.issue_type_id: m.is_present for m in report.issue_marks.all()}
|
||||
|
||||
row = {
|
||||
'id': report.id,
|
||||
'date': report.date.isoformat(),
|
||||
'downtimes': downtimes,
|
||||
'downtimes_display': '; '.join([f"{d['start']}-{d['end']} ({d['reason']})" for d in downtimes]),
|
||||
'daily_work_hours': float(report.daily_work_hours),
|
||||
'weekly_work_hours': float(report.weekly_work_hours),
|
||||
'explanation': report.explanation or '',
|
||||
'comment': report.comment or '',
|
||||
}
|
||||
|
||||
# Добавляем отметки по каждому типу
|
||||
for it in issue_types:
|
||||
row[f'issue_{it.id}'] = marks_dict.get(it.id, False)
|
||||
|
||||
data.append(row)
|
||||
|
||||
# Формируем информацию о колонках
|
||||
columns = []
|
||||
for it in issue_types:
|
||||
columns.append({
|
||||
'id': it.id,
|
||||
'name': it.name,
|
||||
'category': it.category,
|
||||
'field': f'issue_{it.id}'
|
||||
})
|
||||
|
||||
return JsonResponse({
|
||||
'data': data,
|
||||
'columns': columns
|
||||
})
|
||||
|
||||
|
||||
class ErrorsReportSaveAPIView(View):
|
||||
"""API для сохранения отчёта"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({'success': False, 'error': 'Invalid JSON'}, status=400)
|
||||
|
||||
date_str = data.get('date')
|
||||
if not date_str:
|
||||
return JsonResponse({'success': False, 'error': 'Дата обязательна'}, status=400)
|
||||
|
||||
try:
|
||||
report_date = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
except ValueError:
|
||||
return JsonResponse({'success': False, 'error': 'Неверный формат даты'}, status=400)
|
||||
|
||||
report_id = data.get('id')
|
||||
|
||||
# Проверка на дублирование даты при создании новой записи
|
||||
if not report_id:
|
||||
if DailyReport.objects.filter(date=report_date).exists():
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': f'Запись за {report_date.strftime("%d.%m.%Y")} уже существует'
|
||||
}, status=400)
|
||||
|
||||
with transaction.atomic():
|
||||
if report_id:
|
||||
# Обновление существующей записи
|
||||
try:
|
||||
report = DailyReport.objects.get(id=report_id)
|
||||
report.date = report_date
|
||||
report.daily_work_hours = Decimal(str(data.get('daily_work_hours', 0)))
|
||||
report.explanation = data.get('explanation', '')
|
||||
report.comment = data.get('comment', '')
|
||||
report.save()
|
||||
except DailyReport.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'error': 'Запись не найдена'}, status=404)
|
||||
else:
|
||||
# Создание новой записи
|
||||
report = DailyReport.objects.create(
|
||||
date=report_date,
|
||||
daily_work_hours=Decimal(str(data.get('daily_work_hours', 0))),
|
||||
explanation=data.get('explanation', ''),
|
||||
comment=data.get('comment', ''),
|
||||
created_by=request.user if request.user.is_authenticated else None,
|
||||
)
|
||||
|
||||
# Обновляем периоды простоя
|
||||
downtimes = data.get('downtimes', [])
|
||||
report.downtime_periods.all().delete()
|
||||
for dt in downtimes:
|
||||
if dt.get('start') and dt.get('end'):
|
||||
DowntimePeriod.objects.create(
|
||||
report=report,
|
||||
start_time=dt['start'],
|
||||
end_time=dt['end'],
|
||||
reason=dt.get('reason', '')
|
||||
)
|
||||
|
||||
# Обновляем отметки
|
||||
issue_marks = data.get('issue_marks', {})
|
||||
for issue_id_str, is_present in issue_marks.items():
|
||||
issue_id = int(issue_id_str)
|
||||
IssueMark.objects.update_or_create(
|
||||
report=report,
|
||||
issue_type_id=issue_id,
|
||||
defaults={'is_present': is_present}
|
||||
)
|
||||
|
||||
return JsonResponse({'success': True, 'id': report.id})
|
||||
|
||||
|
||||
class ErrorsReportDeleteAPIView(View):
|
||||
"""API для удаления отчёта"""
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({'success': False, 'error': 'Invalid JSON'}, status=400)
|
||||
|
||||
report_id = data.get('id')
|
||||
if not report_id:
|
||||
return JsonResponse({'success': False, 'error': 'ID отчёта обязателен'}, status=400)
|
||||
|
||||
try:
|
||||
report = DailyReport.objects.get(id=report_id)
|
||||
report.delete()
|
||||
return JsonResponse({'success': True})
|
||||
except DailyReport.DoesNotExist:
|
||||
return JsonResponse({'success': False, 'error': 'Отчёт не найден'}, status=404)
|
||||
|
||||
|
||||
class WeeklyHoursAPIView(View):
|
||||
"""API для расчёта часов за неделю"""
|
||||
|
||||
def get(self, request):
|
||||
date_str = request.GET.get('date')
|
||||
if not date_str:
|
||||
return JsonResponse({'error': 'Дата обязательна'}, status=400)
|
||||
|
||||
try:
|
||||
target_date = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
except ValueError:
|
||||
return JsonResponse({'error': 'Неверный формат даты'}, status=400)
|
||||
|
||||
# Находим начало недели (понедельник)
|
||||
week_start = target_date - timedelta(days=target_date.weekday())
|
||||
week_end = week_start + timedelta(days=6)
|
||||
|
||||
# Считаем сумму часов за неделю
|
||||
total = DailyReport.objects.filter(
|
||||
date__gte=week_start,
|
||||
date__lte=target_date
|
||||
).aggregate(total=Sum('daily_work_hours'))['total'] or 0
|
||||
|
||||
return JsonResponse({
|
||||
'week_start': week_start.isoformat(),
|
||||
'week_end': week_end.isoformat(),
|
||||
'total_hours': float(total)
|
||||
})
|
||||
@@ -59,6 +59,7 @@ class UserPermissionsEditView(LoginRequiredMixin, PermissionRequiredMixin, View)
|
||||
'Транспондеры': [],
|
||||
'Тех. анализ': [],
|
||||
'Отметки': [],
|
||||
'Журнал ошибок': [],
|
||||
'Прочее': [],
|
||||
}
|
||||
|
||||
@@ -85,6 +86,8 @@ class UserPermissionsEditView(LoginRequiredMixin, PermissionRequiredMixin, View)
|
||||
permission_groups['Тех. анализ'].append(perm_data)
|
||||
elif code.startswith('mark_'):
|
||||
permission_groups['Отметки'].append(perm_data)
|
||||
elif code.startswith('errors_report_'):
|
||||
permission_groups['Журнал ошибок'].append(perm_data)
|
||||
else:
|
||||
permission_groups['Прочее'].append(perm_data)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user