Пофиксил журнал ошибок

This commit is contained in:
2025-12-16 11:44:56 +03:00
parent 0b34fbd720
commit b6359d08cd
5 changed files with 258 additions and 94 deletions

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-12-16 08:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0029_dailyreport_location_place'),
]
operations = [
migrations.AddField(
model_name='issuetype',
name='location_place',
field=models.CharField(choices=[('kr', 'КР'), ('dv', 'ДВ')], default='kr', help_text='К какому комплексу принадлежит журнал', max_length=30, null=True, verbose_name='Комплекс'),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 5.2.7 on 2025-12-16 08:19
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0030_issuetype_location_place'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='dailyreport',
name='date',
field=models.DateField(db_index=True, help_text='Дата отчёта', verbose_name='Дата'),
),
migrations.AddConstraint(
model_name='dailyreport',
constraint=models.UniqueConstraint(fields=('date', 'location_place'), name='unique_daily_report_date_location'),
),
]

View File

@@ -10,7 +10,18 @@ class IssueType(models.Model):
('error', 'Ошибка'), ('error', 'Ошибка'),
('malfunction', 'Неисправность'), ('malfunction', 'Неисправность'),
] ]
PLACES = [
("kr", "КР"),
("dv", "ДВ")
]
location_place = models.CharField(
max_length=30,
choices=PLACES,
null=True,
default="kr",
verbose_name="Комплекс",
help_text="К какому комплексу принадлежит журнал",
)
name = models.CharField(max_length=255, verbose_name="Название") name = models.CharField(max_length=255, verbose_name="Название")
category = models.CharField( category = models.CharField(
max_length=20, max_length=20,
@@ -35,7 +46,6 @@ class DailyReport(models.Model):
("dv", "ДВ") ("dv", "ДВ")
] ]
date = models.DateField( date = models.DateField(
unique=True,
verbose_name="Дата", verbose_name="Дата",
db_index=True, db_index=True,
help_text="Дата отчёта" help_text="Дата отчёта"
@@ -81,6 +91,12 @@ class DailyReport(models.Model):
verbose_name = "Ежедневный отчёт" verbose_name = "Ежедневный отчёт"
verbose_name_plural = "Ежедневные отчёты" verbose_name_plural = "Ежедневные отчёты"
ordering = ["-date"] ordering = ["-date"]
constraints = [
models.UniqueConstraint(
fields=['date', 'location_place'],
name='unique_daily_report_date_location'
)
]
class DowntimePeriod(models.Model): class DowntimePeriod(models.Model):

View File

@@ -86,27 +86,37 @@
<h4>Журнал ошибок и неисправностей</h4> <h4>Журнал ошибок и неисправностей</h4>
<!-- Toolbar --> <!-- Toolbar -->
<div class="row mb-3"> <div class="row mb-3" id="toolbar">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="d-flex flex-wrap align-items-center gap-3"> <div class="d-flex flex-wrap align-items-center gap-3">
<!-- Выбор комплекса -->
<div class="d-flex align-items-center gap-2">
<label class="form-label mb-0 text-muted">Комплекс:</label>
<select id="locationSelect" class="form-select form-select-sm" style="width: auto;">
<option value="">-- Выберите --</option>
{% for code, name in location_places %}
<option value="{{ code }}">{{ name }}</option>
{% endfor %}
</select>
</div>
<div class="vr"></div>
<!-- Action buttons --> <!-- Action buttons -->
<div class="d-flex gap-2"> <div class="d-flex gap-2">
{% if user|has_perm:'errors_report_create' %} {% if user|has_perm:'errors_report_create' %}
<button class="btn btn-success btn-sm" onclick="openCreateModal()"> <button class="btn btn-success btn-sm" onclick="openCreateModal()" id="btnAddRecord" disabled>
<i class="bi bi-plus-lg"></i> Добавить запись <i class="bi bi-plus-lg"></i> Добавить запись
</button> </button>
{% endif %} {% endif %}
<!-- <button class="btn btn-primary btn-sm" onclick="loadData()">
<i class="bi bi-arrow-clockwise"></i> Обновить
</button> -->
</div> </div>
<!-- Filter Toggle Button --> <!-- Filter Toggle Button -->
<div> <div>
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas" <button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="offcanvas"
data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters"> data-bs-target="#offcanvasFilters" aria-controls="offcanvasFilters" id="btnFilters" disabled>
<i class="bi bi-funnel"></i> Фильтры <i class="bi bi-funnel"></i> Фильтры
<span id="filterCounter" class="badge bg-danger" style="display: none;">0</span> <span id="filterCounter" class="badge bg-danger" style="display: none;">0</span>
</button> </button>
@@ -139,22 +149,6 @@
<input type="date" id="dateTo" name="date_to" class="form-control form-control-sm"> <input type="date" id="dateTo" name="date_to" class="form-control form-control-sm">
</div> </div>
</div> </div>
<!-- Location Filter -->
<div class="mb-3">
<label class="form-label">
Комплекс
</label>
<div class="d-flex justify-content-between mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('locationFilter', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
onclick="selectAllOptions('locationFilter', false)">Снять</button>
</div>
<select id="locationFilter" name="location_place" class="form-select form-select-sm" multiple size="2">
<option value="kr">КР</option>
<option value="dv">ДВ</option>
</select>
</div>
<!-- Error Filter --> <!-- Error Filter -->
<div class="mb-3"> <div class="mb-3">
<label class="form-label"> <label class="form-label">
@@ -167,9 +161,6 @@
onclick="selectAllOptions('errorFilter', false)">Снять</button> onclick="selectAllOptions('errorFilter', false)">Снять</button>
</div> </div>
<select id="errorFilter" name="error_filter" class="form-select form-select-sm" multiple size="6"> <select id="errorFilter" name="error_filter" class="form-select form-select-sm" multiple size="6">
{% for e in errors %}
<option value="{{ e.id }}">{{ e.name }}</option>
{% endfor %}
</select> </select>
</div> </div>
@@ -185,9 +176,6 @@
onclick="selectAllOptions('malfunctionFilter', false)">Снять</button> onclick="selectAllOptions('malfunctionFilter', false)">Снять</button>
</div> </div>
<select id="malfunctionFilter" name="malfunction_filter" class="form-select form-select-sm" multiple size="6"> <select id="malfunctionFilter" name="malfunction_filter" class="form-select form-select-sm" multiple size="6">
{% for m in malfunctions %}
<option value="{{ m.id }}">{{ m.name }}</option>
{% endfor %}
</select> </select>
</div> </div>
@@ -204,7 +192,12 @@
</div> </div>
</div> </div>
<div class="table-responsive"> <!-- Подсказка до выбора комплекса -->
<div class="text-center text-muted py-5" id="selectLocationHint">
<p class="mt-2">Выберите комплекс для отображения данных</p>
</div>
<div class="table-responsive" id="tableContainer" style="display: none;">
<table class="table table-bordered table-hover table-sm" id="reportTable"> <table class="table table-bordered table-hover table-sm" id="reportTable">
<thead> <thead>
<tr id="headerRowTop"> <tr id="headerRowTop">
@@ -293,27 +286,31 @@
{% block extra_js %} {% block extra_js %}
<script> <script>
const issueTypes = { const locationPlaces = {
errors: [ {% for code, name in location_places %}
{% for e in errors %} '{{ code }}': '{{ name }}',
{id: {{ e.id }}, name: "{{ e.name|escapejs }}"}, {% endfor %}
{% endfor %} };
],
malfunctions: [ let issueTypes = {
{% for m in malfunctions %} errors: [],
{id: {{ m.id }}, name: "{{ m.name|escapejs }}"}, malfunctions: []
{% endfor %}
]
}; };
let currentData = []; let currentData = [];
let currentColumns = []; let currentColumns = [];
let currentLocation = null;
let editModal = null; let editModal = null;
// Инициализация // Инициализация
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
editModal = new bootstrap.Modal(document.getElementById('editModal')); editModal = new bootstrap.Modal(document.getElementById('editModal'));
loadData();
// Обработчик выбора комплекса
const locationSelect = document.getElementById('locationSelect');
locationSelect.addEventListener('change', function() {
onLocationChange(this.value);
});
// Добавляем обработчики для обновления счетчика фильтров // Добавляем обработчики для обновления счетчика фильтров
const form = document.getElementById('filter-form'); const form = document.getElementById('filter-form');
@@ -331,15 +328,63 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
// Обработка смены комплекса
function onLocationChange(location) {
const hint = document.getElementById('selectLocationHint');
const tableContainer = document.getElementById('tableContainer');
const btnAdd = document.getElementById('btnAddRecord');
const btnFilters = document.getElementById('btnFilters');
if (!location) {
// Сбрасываем состояние
currentLocation = null;
currentData = [];
currentColumns = [];
issueTypes = { errors: [], malfunctions: [] };
// Показываем hint, скрываем таблицу
hint.style.display = 'block';
tableContainer.style.display = 'none';
// Очищаем таблицу
document.getElementById('reportBody').innerHTML = '';
document.getElementById('headerRowTop').innerHTML = '';
document.getElementById('headerRowBottom').innerHTML = '';
// Отключаем кнопки
if (btnAdd) btnAdd.disabled = true;
if (btnFilters) btnFilters.disabled = true;
return;
}
currentLocation = location;
// Скрываем hint, показываем таблицу
hint.style.display = 'none';
tableContainer.style.display = 'block';
// Включаем кнопки
if (btnAdd) btnAdd.disabled = false;
if (btnFilters) btnFilters.disabled = false;
// Сбрасываем фильтры при смене комплекса
document.getElementById('dateFrom').value = '';
document.getElementById('dateTo').value = '';
selectAllOptions('errorFilter', false);
selectAllOptions('malfunctionFilter', false);
// Загружаем данные
loadData();
}
// Загрузка данных // Загрузка данных
async function loadData() { async function loadData() {
if (!currentLocation) return;
const dateFrom = document.getElementById('dateFrom').value; const dateFrom = document.getElementById('dateFrom').value;
const dateTo = document.getElementById('dateTo').value; const dateTo = document.getElementById('dateTo').value;
// Получаем выбранные значения из мультиселектов
const locationSelect = document.getElementById('locationFilter');
const locationPlaces = Array.from(locationSelect.selectedOptions).map(opt => opt.value);
const errorSelect = document.getElementById('errorFilter'); const errorSelect = document.getElementById('errorFilter');
const errorFilters = Array.from(errorSelect.selectedOptions).map(opt => opt.value); const errorFilters = Array.from(errorSelect.selectedOptions).map(opt => opt.value);
@@ -347,14 +392,10 @@ async function loadData() {
const malfunctionFilters = Array.from(malfunctionSelect.selectedOptions).map(opt => opt.value); const malfunctionFilters = Array.from(malfunctionSelect.selectedOptions).map(opt => opt.value);
let url = '{% url "mainapp:errors_report_api" %}?'; let url = '{% url "mainapp:errors_report_api" %}?';
url += `location_place=${currentLocation}&`;
if (dateFrom) url += `date_from=${dateFrom}&`; if (dateFrom) url += `date_from=${dateFrom}&`;
if (dateTo) url += `date_to=${dateTo}&`; if (dateTo) url += `date_to=${dateTo}&`;
// Добавляем множественные значения
locationPlaces.forEach(loc => {
url += `location_place=${loc}&`;
});
errorFilters.forEach(err => { errorFilters.forEach(err => {
url += `error_filter=${err}&`; url += `error_filter=${err}&`;
}); });
@@ -370,6 +411,13 @@ async function loadData() {
currentData = result.data; currentData = result.data;
currentColumns = result.columns; currentColumns = result.columns;
// Обновляем issueTypes для модального окна
issueTypes.errors = currentColumns.filter(c => c.category === 'error');
issueTypes.malfunctions = currentColumns.filter(c => c.category === 'malfunction');
// Обновляем фильтры
updateFilterOptions();
renderTable(); renderTable();
updateFilterCounter(); updateFilterCounter();
} catch (error) { } catch (error) {
@@ -378,6 +426,36 @@ async function loadData() {
} }
} }
// Обновление опций фильтров на основе текущего комплекса
function updateFilterOptions() {
const errorSelect = document.getElementById('errorFilter');
const malfunctionSelect = document.getElementById('malfunctionFilter');
// Сохраняем текущий выбор
const selectedErrors = Array.from(errorSelect.selectedOptions).map(opt => opt.value);
const selectedMalfunctions = Array.from(malfunctionSelect.selectedOptions).map(opt => opt.value);
// Обновляем ошибки
errorSelect.innerHTML = '';
issueTypes.errors.forEach(e => {
const opt = document.createElement('option');
opt.value = e.id;
opt.textContent = e.name;
if (selectedErrors.includes(String(e.id))) opt.selected = true;
errorSelect.appendChild(opt);
});
// Обновляем неисправности
malfunctionSelect.innerHTML = '';
issueTypes.malfunctions.forEach(m => {
const opt = document.createElement('option');
opt.value = m.id;
opt.textContent = m.name;
if (selectedMalfunctions.includes(String(m.id))) opt.selected = true;
malfunctionSelect.appendChild(opt);
});
}
// Функция для выбора/снятия всех опций в select // Функция для выбора/снятия всех опций в select
function selectAllOptions(selectId, selectAll) { function selectAllOptions(selectId, selectAll) {
const selectElement = document.getElementById(selectId); const selectElement = document.getElementById(selectId);
@@ -404,11 +482,12 @@ function resetFilters() {
document.getElementById('dateTo').value = ''; document.getElementById('dateTo').value = '';
// Снимаем выбор со всех мультиселектов // Снимаем выбор со всех мультиселектов
selectAllOptions('locationFilter', false);
selectAllOptions('errorFilter', false); selectAllOptions('errorFilter', false);
selectAllOptions('malfunctionFilter', false); selectAllOptions('malfunctionFilter', false);
loadData(); if (currentLocation) {
loadData();
}
// Закрыть offcanvas // Закрыть offcanvas
const offcanvas = bootstrap.Offcanvas.getInstance(document.getElementById('offcanvasFilters')); const offcanvas = bootstrap.Offcanvas.getInstance(document.getElementById('offcanvasFilters'));
@@ -425,10 +504,6 @@ function updateFilterCounter() {
if (document.getElementById('dateFrom').value) filterCount++; if (document.getElementById('dateFrom').value) filterCount++;
if (document.getElementById('dateTo').value) filterCount++; if (document.getElementById('dateTo').value) filterCount++;
// Проверяем мультиселекты
const locationSelect = document.getElementById('locationFilter');
if (locationSelect.selectedOptions.length > 0) filterCount++;
const errorSelect = document.getElementById('errorFilter'); const errorSelect = document.getElementById('errorFilter');
if (errorSelect.selectedOptions.length > 0) filterCount++; if (errorSelect.selectedOptions.length > 0) filterCount++;
@@ -517,7 +592,6 @@ function renderTable() {
topHtml += ` topHtml += `
<th rowspan="2" style="width: 80px;">Раб. ч/день</th> <th rowspan="2" style="width: 80px;">Раб. ч/день</th>
<th rowspan="2" style="width: 100px;">Раб. ч/нед.</th> <th rowspan="2" style="width: 100px;">Раб. ч/нед.</th>
<th rowspan="2" style="width: 80px;">Комплекс</th>
<th rowspan="2" style="min-width: 150px;">Пояснение</th> <th rowspan="2" style="min-width: 150px;">Пояснение</th>
<th rowspan="2" style="min-width: 150px;">Комментарий</th> <th rowspan="2" style="min-width: 150px;">Комментарий</th>
`; `;
@@ -583,7 +657,6 @@ function renderTable() {
bodyHtml += `<td class="text-center align-middle weekly-hours-cell" rowspan="${info.rowspan}">${info.weeklyTotal}</td>`; bodyHtml += `<td class="text-center align-middle weekly-hours-cell" rowspan="${info.rowspan}">${info.weeklyTotal}</td>`;
} }
bodyHtml += `<td class="text-center">${row.location_place_display || ''}</td>`;
bodyHtml += `<td>${row.explanation || ''}</td>`; bodyHtml += `<td>${row.explanation || ''}</td>`;
bodyHtml += `<td>${row.comment || ''}</td>`; bodyHtml += `<td>${row.comment || ''}</td>`;
bodyHtml += `</tr>`; bodyHtml += `</tr>`;
@@ -604,11 +677,17 @@ function renderMarkCell(value) {
// Открыть модальное окно для создания // Открыть модальное окно для создания
function openCreateModal() { function openCreateModal() {
if (!currentLocation) {
alert('Сначала выберите комплекс');
return;
}
document.getElementById('modalTitle').textContent = 'Новая запись'; document.getElementById('modalTitle').textContent = 'Новая запись';
document.getElementById('editId').value = ''; document.getElementById('editId').value = '';
document.getElementById('editDate').value = new Date().toISOString().split('T')[0]; document.getElementById('editDate').value = new Date().toISOString().split('T')[0];
document.getElementById('editDailyHours').value = 0; document.getElementById('editDailyHours').value = 0;
document.getElementById('editLocationPlace').value = 'kr'; document.getElementById('editLocationPlace').value = currentLocation;
document.getElementById('editLocationPlace').disabled = true; // Комплекс фиксирован
document.getElementById('editExplanation').value = ''; document.getElementById('editExplanation').value = '';
document.getElementById('editComment').value = ''; document.getElementById('editComment').value = '';
@@ -633,7 +712,8 @@ async function openEditModal(id) {
document.getElementById('editId').value = row.id; document.getElementById('editId').value = row.id;
document.getElementById('editDate').value = row.date; document.getElementById('editDate').value = row.date;
document.getElementById('editDailyHours').value = row.daily_work_hours || 0; document.getElementById('editDailyHours').value = row.daily_work_hours || 0;
document.getElementById('editLocationPlace').value = row.location_place || 'kr'; document.getElementById('editLocationPlace').value = row.location_place || currentLocation;
document.getElementById('editLocationPlace').disabled = true; // Комплекс фиксирован
document.getElementById('editExplanation').value = row.explanation || ''; document.getElementById('editExplanation').value = row.explanation || '';
document.getElementById('editComment').value = row.comment || ''; document.getElementById('editComment').value = row.comment || '';
@@ -694,27 +774,35 @@ function removeDowntimeRow(btn) {
// Рендер чекбоксов ошибок/неисправностей // Рендер чекбоксов ошибок/неисправностей
function renderIssueCheckboxes(row) { function renderIssueCheckboxes(row) {
let errorsHtml = ''; let errorsHtml = '';
issueTypes.errors.forEach(e => { if (issueTypes.errors.length === 0) {
const checked = row[`issue_${e.id}`] ? 'checked' : ''; errorsHtml = '<p class="text-muted small">Нет ошибок для этого комплекса</p>';
errorsHtml += ` } else {
<div class="form-check"> issueTypes.errors.forEach(e => {
<input class="form-check-input issue-check" type="checkbox" id="issue_${e.id}" data-issue-id="${e.id}" ${checked}> const checked = row[`issue_${e.id}`] ? 'checked' : '';
<label class="form-check-label" for="issue_${e.id}">${e.name}</label> errorsHtml += `
</div> <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; document.getElementById('editErrors').innerHTML = errorsHtml;
let malfHtml = ''; let malfHtml = '';
issueTypes.malfunctions.forEach(m => { if (issueTypes.malfunctions.length === 0) {
const checked = row[`issue_${m.id}`] ? 'checked' : ''; malfHtml = '<p class="text-muted small">Нет неисправностей для этого комплекса</p>';
malfHtml += ` } else {
<div class="form-check"> issueTypes.malfunctions.forEach(m => {
<input class="form-check-input issue-check" type="checkbox" id="issue_${m.id}" data-issue-id="${m.id}" ${checked}> const checked = row[`issue_${m.id}`] ? 'checked' : '';
<label class="form-check-label" for="issue_${m.id}">${m.name}</label> malfHtml += `
</div> <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; document.getElementById('editMalfunctions').innerHTML = malfHtml;
} }
@@ -751,7 +839,7 @@ async function saveRecord() {
id: document.getElementById('editId').value || null, id: document.getElementById('editId').value || null,
date: date, date: date,
daily_work_hours: parseFloat(document.getElementById('editDailyHours').value) || 0, daily_work_hours: parseFloat(document.getElementById('editDailyHours').value) || 0,
location_place: document.getElementById('editLocationPlace').value, location_place: currentLocation, // Используем текущий выбранный комплекс
explanation: document.getElementById('editExplanation').value, explanation: document.getElementById('editExplanation').value,
comment: document.getElementById('editComment').value, comment: document.getElementById('editComment').value,
downtimes: collectDowntimes(), downtimes: collectDowntimes(),

View File

@@ -17,10 +17,8 @@ class ErrorsReportView(TemplateView, LoginRequiredMixin, PermissionRequiredMixin
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**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 context['full_width_page'] = True
context['location_places'] = DailyReport.PLACES
return context return context
@@ -31,18 +29,24 @@ class ErrorsReportAPIView(View, LoginRequiredMixin, PermissionRequiredMixin):
# Получаем параметры фильтрации # Получаем параметры фильтрации
date_from = request.GET.get('date_from') date_from = request.GET.get('date_from')
date_to = request.GET.get('date_to') date_to = request.GET.get('date_to')
location_places = request.GET.getlist('location_place') location_place = request.GET.get('location_place')
error_filters = request.GET.getlist('error_filter') error_filters = request.GET.getlist('error_filter')
malfunction_filters = request.GET.getlist('malfunction_filter') malfunction_filters = request.GET.getlist('malfunction_filter')
reports = DailyReport.objects.all() # location_place обязателен
if not location_place:
return JsonResponse({
'data': [],
'columns': [],
'error': 'Выберите комплекс'
})
reports = DailyReport.objects.filter(location_place=location_place)
if date_from: if date_from:
reports = reports.filter(date__gte=date_from) reports = reports.filter(date__gte=date_from)
if date_to: if date_to:
reports = reports.filter(date__lte=date_to) reports = reports.filter(date__lte=date_to)
if location_places:
reports = reports.filter(location_place__in=location_places)
# Фильтрация по ошибкам/неисправностям # Фильтрация по ошибкам/неисправностям
if error_filters: if error_filters:
@@ -52,8 +56,8 @@ class ErrorsReportAPIView(View, LoginRequiredMixin, PermissionRequiredMixin):
reports = reports.prefetch_related('downtime_periods', 'issue_marks__issue_type').distinct() reports = reports.prefetch_related('downtime_periods', 'issue_marks__issue_type').distinct()
# Получаем все типы ошибок/неисправностей # Получаем типы ошибок/неисправностей для выбранного комплекса
issue_types = IssueType.objects.all().order_by('category', 'name') issue_types = IssueType.objects.filter(location_place=location_place).order_by('category', 'name')
data = [] data = []
for report in reports: for report in reports:
@@ -83,7 +87,7 @@ class ErrorsReportAPIView(View, LoginRequiredMixin, PermissionRequiredMixin):
'location_place_display': report.get_location_place_display() if report.location_place else '', 'location_place_display': report.get_location_place_display() if report.location_place else '',
} }
# Добавляем отметки по каждому типу # Добавляем отметки по каждому типу (только для выбранного комплекса)
for it in issue_types: for it in issue_types:
row[f'issue_{it.id}'] = marks_dict.get(it.id, False) row[f'issue_{it.id}'] = marks_dict.get(it.id, False)
@@ -124,13 +128,27 @@ class ErrorsReportSaveAPIView(View):
return JsonResponse({'success': False, 'error': 'Неверный формат даты'}, status=400) return JsonResponse({'success': False, 'error': 'Неверный формат даты'}, status=400)
report_id = data.get('id') report_id = data.get('id')
location_place = data.get('location_place', 'kr')
# Проверка на дублирование даты при создании новой записи # Проверка на дублирование даты + комплекса при создании новой записи
if not report_id: if not report_id:
if DailyReport.objects.filter(date=report_date).exists(): if DailyReport.objects.filter(date=report_date, location_place=location_place).exists():
place_display = dict(DailyReport.PLACES).get(location_place, location_place)
return JsonResponse({ return JsonResponse({
'success': False, 'success': False,
'error': f'Запись за {report_date.strftime("%d.%m.%Y")} уже существует' 'error': f'Запись за {report_date.strftime("%d.%m.%Y")} для комплекса {place_display} уже существует'
}, status=400)
else:
# При обновлении проверяем, не занята ли комбинация другой записью
existing = DailyReport.objects.filter(
date=report_date,
location_place=location_place
).exclude(id=report_id).first()
if existing:
place_display = dict(DailyReport.PLACES).get(location_place, location_place)
return JsonResponse({
'success': False,
'error': f'Запись за {report_date.strftime("%d.%m.%Y")} для комплекса {place_display} уже существует'
}, status=400) }, status=400)
with transaction.atomic(): with transaction.atomic():