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['full_width_page'] = True context['location_places'] = DailyReport.PLACES 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') location_place = request.GET.get('location_place') error_filters = request.GET.getlist('error_filter') malfunction_filters = request.GET.getlist('malfunction_filter') # location_place обязателен if not location_place: return JsonResponse({ 'data': [], 'columns': [], 'error': 'Выберите комплекс' }) reports = DailyReport.objects.filter(location_place=location_place) if date_from: reports = reports.filter(date__gte=date_from) if date_to: reports = reports.filter(date__lte=date_to) # Фильтрация по ошибкам/неисправностям if error_filters: reports = reports.filter(issue_marks__issue_type_id__in=error_filters, issue_marks__is_present=True) if malfunction_filters: reports = reports.filter(issue_marks__issue_type_id__in=malfunction_filters, issue_marks__is_present=True) reports = reports.prefetch_related('downtime_periods', 'issue_marks__issue_type').distinct() # Получаем типы ошибок/неисправностей для выбранного комплекса issue_types = IssueType.objects.filter(location_place=location_place).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 '', 'location_place': report.location_place or '', 'location_place_display': report.get_location_place_display() if report.location_place else '', } # Добавляем отметки по каждому типу (только для выбранного комплекса) 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') location_place = data.get('location_place', 'kr') # Проверка на дублирование даты + комплекса при создании новой записи if not report_id: if DailyReport.objects.filter(date=report_date, location_place=location_place).exists(): 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) 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) 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.location_place = data.get('location_place', 'kr') 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', ''), location_place=data.get('location_place', 'kr'), 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) })