493 lines
19 KiB
Python
493 lines
19 KiB
Python
import json
|
||
from io import BytesIO
|
||
from django.shortcuts import render, redirect, get_object_or_404
|
||
from django.views.generic import ListView, DetailView, CreateView, UpdateView, TemplateView, View, DeleteView
|
||
from django.urls import reverse_lazy, reverse
|
||
from django import forms
|
||
from django.db.models import Count, Q
|
||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||
from django.http import HttpResponse
|
||
from reportlab.pdfgen import canvas
|
||
from reportlab.lib.pagesizes import A4
|
||
from reportlab.pdfbase import pdfmetrics
|
||
from reportlab.pdfbase.ttfonts import TTFont
|
||
from .models import FleetUnit, Maintenance, Breakdown, PartRequest, Category, Document, Supplier, MaintenancePart
|
||
|
||
# Mixins
|
||
class StaffRequiredMixin(UserPassesTestMixin):
|
||
def test_func(self):
|
||
return self.request.user.is_staff
|
||
|
||
# Forms
|
||
class FleetUnitForm(forms.ModelForm):
|
||
class Meta:
|
||
model = FleetUnit
|
||
fields = [
|
||
'name', 'category', 'model_name', 'vin', 'plate_number', 'year', 'photo', 'status',
|
||
'commissioning_date', 'notes',
|
||
'insurance_company', 'insurance_policy_number', 'insurance_start_date', 'insurance_end_date',
|
||
'supplier', 'contract_number', 'vehicle_documents'
|
||
]
|
||
widgets = {
|
||
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||
'category': forms.Select(attrs={'class': 'form-select'}),
|
||
'model_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||
'vin': forms.TextInput(attrs={'class': 'form-control'}),
|
||
'plate_number': forms.TextInput(attrs={'class': 'form-control'}),
|
||
'year': forms.NumberInput(attrs={'class': 'form-control'}),
|
||
'photo': forms.FileInput(attrs={'class': 'form-control'}),
|
||
'status': forms.Select(attrs={'class': 'form-select'}),
|
||
'commissioning_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
|
||
'notes': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
|
||
'insurance_company': forms.TextInput(attrs={'class': 'form-control'}),
|
||
'insurance_policy_number': forms.TextInput(attrs={'class': 'form-control'}),
|
||
'insurance_start_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
|
||
'insurance_end_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
|
||
'supplier': forms.Select(attrs={'class': 'form-select'}),
|
||
'contract_number': forms.TextInput(attrs={'class': 'form-control'}),
|
||
'vehicle_documents': forms.FileInput(attrs={'class': 'form-control'}),
|
||
}
|
||
|
||
class SupplierForm(forms.ModelForm):
|
||
class Meta:
|
||
model = Supplier
|
||
fields = ['name', 'representative_name', 'phone', 'email', 'contract_number']
|
||
widgets = {
|
||
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||
'representative_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||
'phone': forms.TextInput(attrs={'class': 'form-control'}),
|
||
'email': forms.EmailInput(attrs={'class': 'form-control'}),
|
||
'contract_number': forms.TextInput(attrs={'class': 'form-control'}),
|
||
}
|
||
|
||
class MaintenanceForm(forms.ModelForm):
|
||
class Meta:
|
||
model = Maintenance
|
||
fields = ['fleet_unit', 'm_type', 'planned_date', 'planned_runtime', 'mechanic', 'notes']
|
||
widgets = {
|
||
'fleet_unit': forms.Select(attrs={'class': 'form-select'}),
|
||
'm_type': forms.Select(attrs={'class': 'form-select'}),
|
||
'planned_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
|
||
'planned_runtime': forms.NumberInput(attrs={'class': 'form-control'}),
|
||
'mechanic': forms.Select(attrs={'class': 'form-select'}),
|
||
'notes': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
|
||
}
|
||
|
||
class MaintenancePartForm(forms.ModelForm):
|
||
class Meta:
|
||
model = MaintenancePart
|
||
fields = ['part_name', 'article_number', 'quantity']
|
||
widgets = {
|
||
'part_name': forms.TextInput(attrs={'class': 'form-control form-control-sm', 'placeholder': 'Например: Масло 5W-30'}),
|
||
'article_number': forms.TextInput(attrs={'class': 'form-control form-control-sm', 'placeholder': 'Артикул'}),
|
||
'quantity': forms.TextInput(attrs={'class': 'form-control form-control-sm', 'placeholder': 'Кол-во'}),
|
||
}
|
||
|
||
class BreakdownForm(forms.ModelForm):
|
||
class Meta:
|
||
model = Breakdown
|
||
fields = ['fleet_unit', 'system_node', 'description', 'photo', 'status', 'notes']
|
||
widgets = {
|
||
'fleet_unit': forms.Select(attrs={'class': 'form-select'}),
|
||
'system_node': forms.TextInput(attrs={'class': 'form-control'}),
|
||
'description': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
|
||
'photo': forms.FileInput(attrs={'class': 'form-control'}),
|
||
'status': forms.Select(attrs={'class': 'form-select'}),
|
||
'notes': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
|
||
}
|
||
|
||
class PartRequestForm(forms.ModelForm):
|
||
class Meta:
|
||
model = PartRequest
|
||
fields = ['fleet_unit', 'breakdown', 'part_name', 'article_number', 'quantity', 'status', 'photo', 'notes']
|
||
widgets = {
|
||
'fleet_unit': forms.Select(attrs={'class': 'form-select'}),
|
||
'breakdown': forms.Select(attrs={'class': 'form-select'}),
|
||
'part_name': forms.TextInput(attrs={'class': 'form-control'}),
|
||
'article_number': forms.TextInput(attrs={'class': 'form-control'}),
|
||
'quantity': forms.NumberInput(attrs={'class': 'form-control'}),
|
||
'status': forms.Select(attrs={'class': 'form-select'}),
|
||
'photo': forms.FileInput(attrs={'class': 'form-control'}),
|
||
'notes': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
|
||
}
|
||
|
||
# Views
|
||
class IndexView(TemplateView):
|
||
template_name = 'core/index.html'
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
context['total_units'] = FleetUnit.objects.count()
|
||
context['active_units'] = FleetUnit.objects.filter(status='active').count()
|
||
context['broken_units'] = FleetUnit.objects.filter(status='broken').count()
|
||
context['repair_units'] = FleetUnit.objects.filter(status='repair').count()
|
||
|
||
context['status_counts'] = FleetUnit.objects.values('status').annotate(total=Count('status'))
|
||
context['recent_maintenances'] = Maintenance.objects.all().order_by('-planned_date')[:5]
|
||
context['recent_breakdowns'] = Breakdown.objects.all().order_by('-date')[:5]
|
||
|
||
return context
|
||
|
||
# Fleet Views
|
||
class FleetListView(ListView):
|
||
model = FleetUnit
|
||
template_name = 'core/fleet_list.html'
|
||
context_object_name = 'units'
|
||
paginate_by = 12
|
||
|
||
def get_queryset(self):
|
||
queryset = super().get_queryset()
|
||
status = self.request.GET.get('status')
|
||
if status:
|
||
queryset = queryset.filter(status=status)
|
||
search = self.request.GET.get('search')
|
||
if search:
|
||
queryset = queryset.filter(
|
||
Q(name__icontains=search) |
|
||
Q(plate_number__icontains=search) |
|
||
Q(vin__icontains=search)
|
||
)
|
||
return queryset
|
||
|
||
class FleetDetailView(DetailView):
|
||
model = FleetUnit
|
||
template_name = 'core/fleet_detail.html'
|
||
context_object_name = 'unit'
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
context['maintenances'] = self.object.maintenances.all().order_by('-planned_date')
|
||
context['breakdowns'] = self.object.breakdowns.all().order_by('-date')
|
||
context['part_requests'] = self.object.part_requests.all().order_by('-created_at')
|
||
return context
|
||
|
||
class FleetCreateView(CreateView):
|
||
model = FleetUnit
|
||
template_name = 'core/fleet_form.html'
|
||
form_class = FleetUnitForm
|
||
success_url = reverse_lazy('fleet_list')
|
||
|
||
class FleetUpdateView(UpdateView):
|
||
model = FleetUnit
|
||
template_name = 'core/fleet_form.html'
|
||
form_class = FleetUnitForm
|
||
|
||
def get_success_url(self):
|
||
return reverse_lazy('fleet_detail', kwargs={'pk': self.object.pk})
|
||
|
||
class FleetUnitDeleteView(LoginRequiredMixin, StaffRequiredMixin, DeleteView):
|
||
model = FleetUnit
|
||
success_url = reverse_lazy('fleet_list')
|
||
|
||
# Maintenance Views
|
||
class MaintenanceListView(ListView):
|
||
model = Maintenance
|
||
template_name = 'core/maintenance_list.html'
|
||
context_object_name = 'maintenances'
|
||
paginate_by = 15
|
||
|
||
class MaintenanceDetailView(DetailView):
|
||
model = Maintenance
|
||
template_name = 'core/maintenance_detail.html'
|
||
context_object_name = 'maintenance'
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
context['part_form'] = MaintenancePartForm()
|
||
return context
|
||
|
||
class MaintenanceCreateView(CreateView):
|
||
model = Maintenance
|
||
template_name = 'core/maintenance_form.html'
|
||
form_class = MaintenanceForm
|
||
success_url = reverse_lazy('maintenance_list')
|
||
|
||
def get_initial(self):
|
||
initial = super().get_initial()
|
||
fleet_id = self.request.GET.get('fleet_unit')
|
||
if fleet_id:
|
||
initial['fleet_unit'] = get_object_or_404(FleetUnit, pk=fleet_id)
|
||
return initial
|
||
|
||
def form_valid(self, form):
|
||
m_type = form.cleaned_data.get('m_type')
|
||
checklist = []
|
||
if m_type == 'TO-250':
|
||
checklist = [{"task": "Замена масла в двигателе", "done": False}, {"task": "Замена масляного фильтра", "done": False}]
|
||
elif m_type == 'TO-500':
|
||
checklist = [{"task": "Замена масла в двигателе", "done": False}, {"task": "Замена фильтров", "done": False}, {"task": "Осмотр ходовой", "done": False}]
|
||
else:
|
||
checklist = [{"task": "Общий осмотр", "done": False}]
|
||
|
||
form.instance.checklist = checklist
|
||
return super().form_valid(form)
|
||
|
||
class MaintenanceUpdateView(UpdateView):
|
||
model = Maintenance
|
||
template_name = 'core/maintenance_form.html'
|
||
form_class = MaintenanceForm
|
||
|
||
def get_success_url(self):
|
||
return reverse('maintenance_detail', kwargs={'pk': self.object.pk})
|
||
|
||
class MaintenanceDeleteView(LoginRequiredMixin, StaffRequiredMixin, DeleteView):
|
||
model = Maintenance
|
||
success_url = reverse_lazy('maintenance_list')
|
||
|
||
class MaintenanceProcessView(View):
|
||
def post(self, request, pk):
|
||
maintenance = get_object_or_404(Maintenance, pk=pk)
|
||
task_index = request.POST.get('task_index')
|
||
done = request.POST.get('done') == 'true'
|
||
|
||
if task_index is not None:
|
||
idx = int(task_index)
|
||
if 0 <= idx < len(maintenance.checklist):
|
||
maintenance.checklist[idx]['done'] = done
|
||
maintenance.save()
|
||
|
||
return redirect('maintenance_detail', pk=pk)
|
||
|
||
class MaintenanceCompleteView(View):
|
||
def post(self, request, pk):
|
||
maintenance = get_object_or_404(Maintenance, pk=pk)
|
||
maintenance.status = 'completed'
|
||
maintenance.actual_date = request.POST.get('actual_date') or None
|
||
maintenance.actual_runtime = request.POST.get('actual_runtime') or None
|
||
maintenance.parts_used = request.POST.get('parts_used')
|
||
maintenance.save()
|
||
|
||
unit = maintenance.fleet_unit
|
||
unit.status = 'active'
|
||
unit.save()
|
||
|
||
return redirect('maintenance_detail', pk=pk)
|
||
|
||
class MaintenancePartAddView(View):
|
||
def post(self, request, pk):
|
||
maintenance = get_object_or_404(Maintenance, pk=pk)
|
||
form = MaintenancePartForm(request.POST)
|
||
if form.is_valid():
|
||
part = form.save(commit=False)
|
||
part.maintenance = maintenance
|
||
part.save()
|
||
return redirect('maintenance_detail', pk=pk)
|
||
|
||
class MaintenancePartDeleteView(View):
|
||
def post(self, request, pk):
|
||
part = get_object_or_404(MaintenancePart, pk=pk)
|
||
maintenance_pk = part.maintenance.pk
|
||
part.delete()
|
||
return redirect('maintenance_detail', pk=maintenance_pk)
|
||
|
||
class MaintenancePDFView(View):
|
||
def get(self, request, pk):
|
||
maintenance = get_object_or_404(Maintenance, pk=pk)
|
||
unit = maintenance.fleet_unit
|
||
buffer = BytesIO()
|
||
p = canvas.Canvas(buffer, pagesize=A4)
|
||
|
||
# Register Cyrillic font
|
||
font_path = "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf"
|
||
try:
|
||
pdfmetrics.registerFont(TTFont('LiberationSans', font_path))
|
||
p.setFont('LiberationSans', 12)
|
||
except:
|
||
pass # Fallback to default if font not found
|
||
|
||
# Header
|
||
p.setFont('LiberationSans', 16)
|
||
p.drawString(100, 800, f"Акт технического обслуживания: {maintenance.m_type}")
|
||
|
||
p.setFont('LiberationSans', 10)
|
||
p.drawString(100, 780, f"Статус ТО: {maintenance.get_status_display()}")
|
||
|
||
p.setFont('LiberationSans', 12)
|
||
p.setStrokeColorRGB(0.7, 0.7, 0.7)
|
||
p.line(100, 770, 500, 770)
|
||
|
||
# Unit Info (The Header requested by user)
|
||
y = 750
|
||
p.setFont('LiberationSans', 11)
|
||
p.drawString(100, y, f"ТЕХНИКА: {unit.name}")
|
||
y -= 15
|
||
p.drawString(100, y, f"Модель: {unit.model_name}")
|
||
y -= 15
|
||
p.drawString(100, y, f"VIN / Серийный номер: {unit.vin}")
|
||
y -= 15
|
||
p.drawString(100, y, f"Госномер: {unit.plate_number or '-'}")
|
||
y -= 15
|
||
p.drawString(100, y, f"Год выпуска: {unit.year}")
|
||
|
||
y -= 25
|
||
p.line(100, y, 500, y)
|
||
y -= 15
|
||
|
||
# Execution info
|
||
p.drawString(100, y, f"Дата проведения: {maintenance.actual_date or '-'}")
|
||
y -= 15
|
||
p.drawString(100, y, f"Наработка (моточасы/км): {maintenance.actual_runtime or '-'}")
|
||
y -= 15
|
||
p.drawString(100, y, f"Исполнитель: {maintenance.mechanic.get_full_name() or maintenance.mechanic.username if maintenance.mechanic else '-'}")
|
||
|
||
y -= 30
|
||
p.setFont('LiberationSans', 14)
|
||
p.drawString(100, y, "Чек-лист операций:")
|
||
y -= 20
|
||
p.setFont('LiberationSans', 10)
|
||
for item in maintenance.checklist:
|
||
status = "[x]" if item['done'] else "[ ]"
|
||
p.drawString(120, y, f"{status} {item['task']}")
|
||
y -= 15
|
||
if y < 100:
|
||
p.showPage()
|
||
y = 800
|
||
p.setFont('LiberationSans', 10)
|
||
|
||
# Used Parts and Fluids (The specific list requested by user)
|
||
y -= 20
|
||
if y < 150:
|
||
p.showPage()
|
||
y = 800
|
||
|
||
p.setFont('LiberationSans', 14)
|
||
p.drawString(100, y, "Использованные запчасти и жидкости:")
|
||
y -= 20
|
||
|
||
# Table Header for parts
|
||
p.setFont('LiberationSans', 10)
|
||
p.drawString(100, y, "Наименование")
|
||
p.drawString(300, y, "Артикул")
|
||
p.drawString(450, y, "Кол-во")
|
||
y -= 5
|
||
p.line(100, y, 500, y)
|
||
y -= 15
|
||
|
||
parts = maintenance.used_parts.all()
|
||
if parts:
|
||
for part in parts:
|
||
p.drawString(100, y, f"{part.part_name}")
|
||
p.drawString(300, y, f"{part.article_number or '-'}")
|
||
p.drawString(450, y, f"{part.quantity}")
|
||
y -= 15
|
||
if y < 100:
|
||
p.showPage()
|
||
y = 800
|
||
else:
|
||
p.drawString(100, y, "Нет записей")
|
||
y -= 15
|
||
|
||
y -= 20
|
||
if maintenance.notes:
|
||
p.setFont('LiberationSans', 11)
|
||
p.drawString(100, y, "Примечания:")
|
||
y -= 15
|
||
p.setFont('LiberationSans', 9)
|
||
# Simple text wrapping for notes
|
||
lines = maintenance.notes.split('\n')
|
||
for line in lines:
|
||
p.drawString(120, y, line[:80])
|
||
y -= 12
|
||
if y < 100:
|
||
p.showPage()
|
||
y = 800
|
||
|
||
p.showPage()
|
||
p.save()
|
||
|
||
buffer.seek(0)
|
||
return HttpResponse(buffer, content_type='application/pdf',
|
||
headers={'Content-Disposition': f'attachment; filename="maintenance-{pk}.pdf"'})
|
||
|
||
# Breakdown Views
|
||
class BreakdownListView(ListView):
|
||
model = Breakdown
|
||
template_name = 'core/breakdown_list.html'
|
||
context_object_name = 'breakdowns'
|
||
|
||
class BreakdownCreateView(CreateView):
|
||
model = Breakdown
|
||
form_class = BreakdownForm
|
||
template_name = 'core/breakdown_form.html'
|
||
|
||
def get_initial(self):
|
||
initial = super().get_initial()
|
||
fleet_id = self.request.GET.get('fleet_unit')
|
||
if fleet_id:
|
||
initial['fleet_unit'] = get_object_or_404(FleetUnit, pk=fleet_id)
|
||
return initial
|
||
|
||
def form_valid(self, form):
|
||
response = super().form_valid(form)
|
||
unit = self.object.fleet_unit
|
||
if self.object.status == 'reported':
|
||
unit.status = 'broken'
|
||
elif self.object.status == 'need_part':
|
||
unit.status = 'waiting_parts'
|
||
unit.save()
|
||
return response
|
||
|
||
def get_success_url(self):
|
||
return reverse('fleet_detail', kwargs={'pk': self.object.fleet_unit.pk})
|
||
|
||
class BreakdownDeleteView(LoginRequiredMixin, StaffRequiredMixin, DeleteView):
|
||
model = Breakdown
|
||
def get_success_url(self):
|
||
return reverse('fleet_detail', kwargs={'pk': self.object.fleet_unit.pk})
|
||
|
||
# Part Request Views
|
||
class PartRequestListView(ListView):
|
||
model = PartRequest
|
||
template_name = 'core/part_request_list.html'
|
||
context_object_name = 'requests'
|
||
|
||
class PartRequestCreateView(CreateView):
|
||
model = PartRequest
|
||
form_class = PartRequestForm
|
||
template_name = 'core/part_request_form.html'
|
||
|
||
def get_initial(self):
|
||
initial = super().get_initial()
|
||
breakdown_id = self.request.GET.get('breakdown')
|
||
if breakdown_id:
|
||
breakdown = get_object_or_404(Breakdown, pk=breakdown_id)
|
||
initial['breakdown'] = breakdown
|
||
initial['fleet_unit'] = breakdown.fleet_unit
|
||
fleet_id = self.request.GET.get('fleet_unit')
|
||
if fleet_id:
|
||
initial['fleet_unit'] = get_object_or_404(FleetUnit, pk=fleet_id)
|
||
return initial
|
||
|
||
def get_success_url(self):
|
||
return reverse('fleet_detail', kwargs={'pk': self.object.fleet_unit.pk})
|
||
|
||
class PartRequestDeleteView(LoginRequiredMixin, StaffRequiredMixin, DeleteView):
|
||
model = PartRequest
|
||
def get_success_url(self):
|
||
return reverse('supply_list')
|
||
|
||
class SupplyListView(ListView):
|
||
model = PartRequest
|
||
template_name = 'core/supply_list.html'
|
||
context_object_name = 'requests'
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
context['suppliers'] = Supplier.objects.all()
|
||
return context
|
||
|
||
class SupplierCreateView(CreateView):
|
||
model = Supplier
|
||
form_class = SupplierForm
|
||
template_name = 'core/supplier_form.html'
|
||
success_url = reverse_lazy('supply_list')
|
||
|
||
class SupplierUpdateView(UpdateView):
|
||
model = Supplier
|
||
form_class = SupplierForm
|
||
template_name = 'core/supplier_form.html'
|
||
success_url = reverse_lazy('supply_list')
|
||
|
||
class SupplierDeleteView(LoginRequiredMixin, StaffRequiredMixin, DeleteView):
|
||
model = Supplier
|
||
success_url = reverse_lazy('supply_list') |