Ver.02
This commit is contained in:
parent
df45ad71cc
commit
a5f35ca313
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,61 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-27 18:32
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='breakdown',
|
||||
name='notes',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Примечания'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='breakdown',
|
||||
name='photo',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='breakdown_photos/', verbose_name='Фото'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='fleetunit',
|
||||
name='qr_code',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='qrcodes/', verbose_name='QR-код'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='maintenance',
|
||||
name='checklist',
|
||||
field=models.JSONField(blank=True, default=list, verbose_name='Чек-лист'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='maintenance',
|
||||
name='mechanic',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_maintenances', to=settings.AUTH_USER_MODEL, verbose_name='Исполнитель'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='maintenance',
|
||||
name='parts_used',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Использованные запчасти'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='maintenance',
|
||||
name='photo',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='maintenance_photos/', verbose_name='Фото'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='partrequest',
|
||||
name='breakdown',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='part_requests', to='core.breakdown', verbose_name='Поломка'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='maintenance',
|
||||
name='m_type',
|
||||
field=models.CharField(choices=[('TO-250', 'ТО-250'), ('TO-500', 'ТО-500'), ('seasonal', 'Сезонное'), ('special', 'Спец')], max_length=50, verbose_name='Тип ТО'),
|
||||
),
|
||||
]
|
||||
32
core/migrations/0003_setup_groups.py
Normal file
32
core/migrations/0003_setup_groups.py
Normal file
@ -0,0 +1,32 @@
|
||||
from django.db import migrations
|
||||
|
||||
def create_groups(apps, schema_editor):
|
||||
Group = apps.get_model('auth', 'Group')
|
||||
Permission = apps.get_model('auth', 'Permission')
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
|
||||
# Define roles
|
||||
roles = {
|
||||
'Главный механик': ['view_fleetunit', 'add_maintenance', 'change_maintenance', 'view_maintenance', 'add_breakdown', 'change_breakdown', 'view_breakdown', 'add_partrequest', 'change_partrequest', 'view_partrequest'],
|
||||
'Механик': ['view_fleetunit', 'change_maintenance', 'view_maintenance', 'add_breakdown', 'view_breakdown', 'add_partrequest', 'view_partrequest'],
|
||||
'Снабжение': ['view_fleetunit', 'view_partrequest', 'change_partrequest', 'add_partrequest'],
|
||||
}
|
||||
|
||||
for role_name, perms in roles.items():
|
||||
group, created = Group.objects.get_or_create(name=role_name)
|
||||
for perm_code in perms:
|
||||
try:
|
||||
perm = Permission.objects.get(codename=perm_code)
|
||||
group.permissions.add(perm)
|
||||
except Permission.DoesNotExist:
|
||||
pass
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('core', '0002_breakdown_notes_breakdown_photo_fleetunit_qr_code_and_more'),
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(create_groups),
|
||||
]
|
||||
Binary file not shown.
BIN
core/migrations/__pycache__/0003_setup_groups.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0003_setup_groups.cpython-311.pyc
Normal file
Binary file not shown.
@ -1,5 +1,10 @@
|
||||
import qrcode
|
||||
from io import BytesIO
|
||||
from django.core.files import File
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth.models import User
|
||||
from django.conf import settings
|
||||
|
||||
class Category(models.Model):
|
||||
name = models.CharField(max_length=100, verbose_name="Наименование")
|
||||
@ -30,6 +35,7 @@ class FleetUnit(models.Model):
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active', verbose_name="Статус")
|
||||
commissioning_date = models.DateField(verbose_name="Дата ввода в эксплуатацию")
|
||||
notes = models.TextField(blank=True, null=True, verbose_name="Примечания")
|
||||
qr_code = models.ImageField(upload_to='qrcodes/', blank=True, null=True, verbose_name="QR-код")
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
@ -55,12 +61,38 @@ class FleetUnit(models.Model):
|
||||
}
|
||||
return colors.get(self.status, 'primary')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
is_new = self.pk is None
|
||||
super().save(*args, **kwargs)
|
||||
if is_new or not self.qr_code:
|
||||
self.generate_qr_code()
|
||||
|
||||
def generate_qr_code(self):
|
||||
# We need the full URL. In a real app, we'd use the site domain.
|
||||
# For now, we'll use a relative path or a placeholder if domain is unknown.
|
||||
path = self.get_absolute_url()
|
||||
# You might want to use a full URL here if you have a domain
|
||||
# base_url = "https://yourdomain.com"
|
||||
# qr_data = f"{base_url}{path}"
|
||||
qr_data = path # Using path for now, or you can try to get site domain
|
||||
|
||||
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
||||
qr.add_data(qr_data)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image(fill='black', back_color='white')
|
||||
|
||||
buffer = BytesIO()
|
||||
img.save(buffer, format='PNG')
|
||||
filename = f'qr-{self.pk}.png'
|
||||
self.qr_code.save(filename, File(buffer), save=False)
|
||||
super().save(update_fields=['qr_code'])
|
||||
|
||||
class Maintenance(models.Model):
|
||||
TYPE_CHOICES = [
|
||||
('TO-250', 'ТО-250'),
|
||||
('TO-500', 'ТО-500'),
|
||||
('seasonal', 'Сезонное'),
|
||||
('individual', 'Индивидуальное'),
|
||||
('special', 'Спец'),
|
||||
]
|
||||
STATUS_CHOICES = [
|
||||
('planned', 'Планируется'),
|
||||
@ -72,10 +104,14 @@ class Maintenance(models.Model):
|
||||
m_type = models.CharField(max_length=50, choices=TYPE_CHOICES, verbose_name="Тип ТО")
|
||||
planned_date = models.DateField(verbose_name="Плановая дата")
|
||||
planned_runtime = models.PositiveIntegerField(verbose_name="Плановый пробег / моточасы")
|
||||
checklist = models.JSONField(default=list, blank=True, verbose_name="Чек-лист")
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='planned', verbose_name="Статус")
|
||||
|
||||
mechanic = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='assigned_maintenances', verbose_name="Исполнитель")
|
||||
actual_date = models.DateField(null=True, blank=True, verbose_name="Фактическая дата")
|
||||
actual_runtime = models.PositiveIntegerField(null=True, blank=True, verbose_name="Фактический пробег / моточасы")
|
||||
parts_used = models.TextField(blank=True, null=True, verbose_name="Использованные запчасти")
|
||||
photo = models.ImageField(upload_to='maintenance_photos/', blank=True, null=True, verbose_name="Фото")
|
||||
|
||||
notes = models.TextField(blank=True, null=True, verbose_name="Примечания")
|
||||
|
||||
@ -97,8 +133,10 @@ class Breakdown(models.Model):
|
||||
date = models.DateField(auto_now_add=True, verbose_name="Дата")
|
||||
system_node = models.CharField(max_length=255, verbose_name="Узел / система")
|
||||
description = models.TextField(verbose_name="Описание")
|
||||
photo = models.ImageField(upload_to='breakdown_photos/', blank=True, null=True, verbose_name="Фото")
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='reported', verbose_name="Статус")
|
||||
repair_date = models.DateField(null=True, blank=True, verbose_name="Дата ремонта")
|
||||
notes = models.TextField(blank=True, null=True, verbose_name="Примечания")
|
||||
cost = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Стоимость")
|
||||
|
||||
class Meta:
|
||||
@ -117,6 +155,7 @@ class PartRequest(models.Model):
|
||||
]
|
||||
|
||||
fleet_unit = models.ForeignKey(FleetUnit, on_delete=models.CASCADE, related_name='part_requests', verbose_name="Техника")
|
||||
breakdown = models.ForeignKey(Breakdown, on_delete=models.SET_NULL, null=True, blank=True, related_name='part_requests', verbose_name="Поломка")
|
||||
part_name = models.CharField(max_length=255, verbose_name="Наименование детали")
|
||||
article_number = models.CharField(max_length=100, blank=True, null=True, verbose_name="Артикул")
|
||||
quantity = models.PositiveIntegerField(default=1, verbose_name="Количество")
|
||||
|
||||
@ -26,10 +26,10 @@
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-slate-900 sticky-top shadow-sm">
|
||||
<div class="container">
|
||||
<a class="navbar-brand d-flex align-items-center" href="{% url 'index' %}">
|
||||
<i class="bi bi-truck-flatbed me-2"></i>
|
||||
<i class="bi bi-truck-flatbed me-2 text-blue-500"></i>
|
||||
<span class="fw-bold text-uppercase tracking-wider">Fleet<span class="text-blue-500">Manager</span></span>
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-collapse="navbarSupportedContent">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
@ -38,22 +38,28 @@
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'index' %}active{% endif %}" href="{% url 'index' %}">Дашборд</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'fleet_list' %}active{% endif %}" href="{% url 'fleet_list' %}">Техника</a>
|
||||
<a class="nav-link {% if 'fleet' in request.path %}active{% endif %}" href="{% url 'fleet_list' %}">Техника</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/">Админ-панель</a>
|
||||
<a class="nav-link {% if 'maintenance' in request.path %}active{% endif %}" href="{% url 'maintenance_list' %}">ТО</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if 'part-request' in request.path %}active{% endif %}" href="{% url 'part_request_list' %}">Заявки</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="text-white-50 me-3 d-none d-md-inline">Иван Иванов (Админ)</span>
|
||||
<span class="text-white-50 me-3 d-none d-md-inline small">
|
||||
{% if user.is_authenticated %}{{ user.get_full_name|default:user.username }}{% else %}Гость{% endif %}
|
||||
</span>
|
||||
<div class="dropdown">
|
||||
<a href="#" class="d-block link-light text-decoration-none dropdown-toggle" id="dropdownUser1" data-bs-toggle="dropdown">
|
||||
<img src="https://ui-avatars.com/api/?name=Admin&background=3b82f6&color=fff" alt="mdo" width="32" height="32" class="rounded-circle">
|
||||
<img src="https://ui-avatars.com/api/?name={{ user.username }}&background=3b82f6&color=fff" alt="user" width="32" height="32" class="rounded-circle">
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end text-small shadow" aria-labelledby="dropdownUser1">
|
||||
<ul class="dropdown-menu dropdown-menu-end text-small shadow border-0 mt-2" aria-labelledby="dropdownUser1">
|
||||
<li><a class="dropdown-item" href="/admin/">Админ-панель</a></li>
|
||||
<li><a class="dropdown-item" href="#">Профиль</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#">Выйти</a></li>
|
||||
<li><a class="dropdown-item" href="/admin/logout/">Выйти</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -62,14 +68,12 @@
|
||||
</nav>
|
||||
|
||||
<main class="py-4">
|
||||
<div class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="py-4 bg-white mt-auto border-top">
|
||||
<div class="container text-center">
|
||||
<p class="text-muted mb-0">© {% now "Y" %} Fleet Manager. Все права защищены.</p>
|
||||
<p class="text-muted small mb-0">© {% now "Y" %} Fleet Manager. Разработано для управления парком техники.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@ -77,4 +81,4 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
47
core/templates/core/breakdown_form.html
Normal file
47
core/templates/core/breakdown_form.html
Normal file
@ -0,0 +1,47 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-white py-3 fw-bold">
|
||||
<i class="bi bi-exclamation-triangle text-danger me-2"></i>Зафиксировать поломку
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-bold">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% for error in field.errors %}
|
||||
<div class="text-danger small">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="mt-4 pt-3 border-top">
|
||||
<button type="submit" class="btn btn-danger px-4 rounded-pill">Сохранить поломку</button>
|
||||
<a href="{% url 'fleet_list' %}" class="btn btn-link text-muted">Отмена</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning mt-4 small shadow-sm border-0">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
После сохранения статус техники автоматически изменится на "Сломана" или "Ждёт деталь".
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('input, select, textarea').forEach(el => {
|
||||
if (!el.classList.contains('form-check-input')) {
|
||||
el.classList.add('form-control');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -1,186 +1,185 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{{ unit.name }} | Fleet Manager{% endblock %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-4">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-2">
|
||||
<li class="breadcrumb-item"><a href="{% url 'index' %}" class="text-decoration-none">Дашборд</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'fleet_list' %}" class="text-decoration-none">Техника</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ unit.name }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center gap-3">
|
||||
<h2 class="mb-0">{{ unit.name }}</h2>
|
||||
<div class="d-flex gap-2 w-100 w-md-auto">
|
||||
<a href="{% url 'fleet_edit' unit.pk %}" class="btn btn-outline-secondary flex-grow-1 flex-md-grow-0">
|
||||
<i class="bi bi-pencil me-2"></i>Редактировать
|
||||
</a>
|
||||
<button class="btn btn-danger flex-grow-1 flex-md-grow-0">
|
||||
<i class="bi bi-trash me-2"></i>Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
<!-- Main Info -->
|
||||
<div class="card shadow-sm mb-4 overflow-hidden">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-5">
|
||||
{% if unit.photo %}
|
||||
<img src="{{ unit.photo.url }}" class="img-fluid h-100 object-fit-cover" alt="{{ unit.name }}" style="min-height: 300px;">
|
||||
{% else %}
|
||||
<div class="bg-light h-100 d-flex align-items-center justify-content-center border-end" style="min-height: 300px;">
|
||||
<i class="bi bi-truck text-muted display-1"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row">
|
||||
<!-- Sidebar: Info & QR -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
{% if unit.photo %}
|
||||
<img src="{{ unit.photo.url }}" class="card-img-top" alt="{{ unit.name }}" style="height: 250px; object-fit: cover;">
|
||||
{% else %}
|
||||
<div class="bg-light text-center py-5">
|
||||
<i class="bi bi-truck text-muted display-1"></i>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<span class="badge bg-{{ unit.get_status_color }} fs-6">
|
||||
{{ unit.get_status_display }}
|
||||
</span>
|
||||
<div class="text-muted">ID: {{ unit.pk }}</div>
|
||||
{% endif %}
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<div>
|
||||
<h4 class="fw-bold mb-0">{{ unit.name }}</h4>
|
||||
<span class="text-muted small">{{ unit.model_name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6">
|
||||
<label class="text-muted small d-block">Категория</label>
|
||||
<span class="fw-medium">{{ unit.category|default:"Не указана" }}</span>
|
||||
<span class="badge bg-{{ unit.get_status_color }} rounded-pill px-3 py-2">
|
||||
{{ unit.get_status_display }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6">
|
||||
<p class="text-muted small mb-0">Госномер</p>
|
||||
<p class="fw-bold mb-0">{{ unit.plate_number|default:"-" }}</p>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<p class="text-muted small mb-0">Год выпуска</p>
|
||||
<p class="fw-bold mb-0">{{ unit.year }}</p>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<p class="text-muted small mb-0">VIN / Серийный номер</p>
|
||||
<p class="fw-bold mb-0">{{ unit.vin }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<a href="{% url 'fleet_edit' unit.pk %}" class="btn btn-outline-primary rounded-pill">Редактировать</a>
|
||||
<a href="{% url 'breakdown_add' %}?fleet_unit={{ unit.pk }}" class="btn btn-danger rounded-pill">Заявить о поломке</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0 text-center p-4">
|
||||
<h6 class="fw-bold mb-3">QR-код техники</h6>
|
||||
{% if unit.qr_code %}
|
||||
<img src="{{ unit.qr_code.url }}" class="img-fluid mx-auto mb-3" style="max-width: 150px;" alt="QR Code">
|
||||
{% else %}
|
||||
<div class="alert alert-light small">QR-код генерируется...</div>
|
||||
{% endif %}
|
||||
<p class="text-muted small mb-0">Наклейте этот код на технику для быстрого доступа механика.</p>
|
||||
<button onclick="window.print()" class="btn btn-sm btn-link mt-2">Печать кода</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content: Tabs -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-white border-0 pt-3">
|
||||
<ul class="nav nav-pills" id="fleetTab" role="tablist">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active rounded-pill px-4" id="history-tab" data-bs-toggle="tab" data-bs-target="#history" type="button">История ТО</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link rounded-pill px-4" id="breakdowns-tab" data-bs-toggle="tab" data-bs-target="#breakdowns" type="button">Поломки</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link rounded-pill px-4" id="parts-tab" data-bs-toggle="tab" data-bs-target="#parts" type="button">Заявки на запчасти</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="tab-content" id="fleetTabContent">
|
||||
<!-- Maintenance Tab -->
|
||||
<div class="tab-pane fade show active" id="history">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-bold mb-0">Плановое и выполненное ТО</h6>
|
||||
<a href="{% url 'maintenance_add' %}?fleet_unit={{ unit.pk }}" class="btn btn-sm btn-primary rounded-pill">+ Запланировать</a>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="text-muted small d-block">Модель</label>
|
||||
<span class="fw-medium">{{ unit.model_name }}</span>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Тип</th>
|
||||
<th>Дата</th>
|
||||
<th>Статус</th>
|
||||
<th>Исполнитель</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in maintenances %}
|
||||
<tr>
|
||||
<td>{{ m.m_type }}</td>
|
||||
<td>{{ m.planned_date }}</td>
|
||||
<td><span class="badge bg-{{ m.status|yesno:'success,warning,secondary' }} rounded-pill small">{{ m.get_status_display }}</span></td>
|
||||
<td>{{ m.mechanic|default:"-" }}</td>
|
||||
<td class="text-end">
|
||||
<a href="{% url 'maintenance_detail' m.pk %}" class="btn btn-sm btn-link text-primary">Открыть</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="5" class="text-center py-4 text-muted">Нет записей ТО</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="text-muted small d-block">Госномер</label>
|
||||
<span class="fw-medium">{{ unit.plate_number|default:"-" }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Breakdowns Tab -->
|
||||
<div class="tab-pane fade" id="breakdowns">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-bold mb-0">Журнал поломок</h6>
|
||||
<a href="{% url 'breakdown_add' %}?fleet_unit={{ unit.pk }}" class="btn btn-sm btn-danger rounded-pill">+ Новая поломка</a>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="text-muted small d-block">Год выпуска</label>
|
||||
<span class="fw-medium">{{ unit.year }}</span>
|
||||
<div class="list-group list-group-flush">
|
||||
{% for b in breakdowns %}
|
||||
<div class="list-group-item px-0 py-3">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<h6 class="mb-0 fw-bold text-danger">{{ b.system_node }}</h6>
|
||||
<span class="badge bg-light text-dark border small">{{ b.get_status_display }}</span>
|
||||
</div>
|
||||
<p class="small mb-1 text-muted">{{ b.date|date:"d.m.Y" }} — {{ b.description }}</p>
|
||||
<div class="d-flex align-items-center mt-2">
|
||||
<a href="{% url 'part_request_add' %}?breakdown={{ b.pk }}" class="btn btn-sm btn-outline-primary rounded-pill py-0 small me-2">+ Заказать запчасть</a>
|
||||
{% if b.photo %}<span class="badge bg-light text-muted border small"><i class="bi bi-image me-1"></i>Есть фото</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="text-center py-4 text-muted">Поломок не зафиксировано</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="text-muted small d-block">VIN / Серийный номер</label>
|
||||
<code class="fw-bold text-dark fs-6">{{ unit.vin }}</code>
|
||||
</div>
|
||||
|
||||
<!-- Parts Tab -->
|
||||
<div class="tab-pane fade" id="parts">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-bold mb-0">Заявки на запчасти</h6>
|
||||
<a href="{% url 'part_request_add' %}?fleet_unit={{ unit.pk }}" class="btn btn-sm btn-primary rounded-pill">+ Создать заявку</a>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="text-muted small d-block">Ввод в эксплуатацию</label>
|
||||
<span class="fw-medium">{{ unit.commissioning_date|date:"d.m.Y" }}</span>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Деталь</th>
|
||||
<th>Кол-во</th>
|
||||
<th>Статус</th>
|
||||
<th>Дата</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in part_requests %}
|
||||
<tr>
|
||||
<td>{{ r.part_name }}</td>
|
||||
<td>{{ r.quantity }}</td>
|
||||
<td><span class="badge bg-light text-dark border rounded-pill small">{{ r.get_status_display }}</span></td>
|
||||
<td class="small">{{ r.created_at|date:"d.m.Y" }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="4" class="text-center py-4 text-muted">Нет заявок</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- History Tabs -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white border-bottom-0 p-0">
|
||||
<ul class="nav nav-tabs nav-fill" id="unitTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active py-3" id="m-tab" data-bs-toggle="tab" data-bs-target="#m-pane" type="button" role="tab">ТО</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link py-3" id="b-tab" data-bs-toggle="tab" data-bs-target="#b-pane" type="button" role="tab">Поломки</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link py-3" id="p-tab" data-bs-toggle="tab" data-bs-target="#p-pane" type="button" role="tab">Запчасти</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link py-3" id="d-tab" data-bs-toggle="tab" data-bs-target="#d-pane" type="button" role="tab">Документы</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="tab-content" id="unitTabsContent">
|
||||
<div class="tab-pane fade show active" id="m-pane" role="tabpanel">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="ps-4">Тип</th>
|
||||
<th>Дата</th>
|
||||
<th>Пробег/мч</th>
|
||||
<th class="pe-4">Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in maintenances %}
|
||||
<tr>
|
||||
<td class="ps-4">{{ m.m_type }}</td>
|
||||
<td>{{ m.planned_date|date:"d.m.Y" }}</td>
|
||||
<td>{{ m.planned_runtime }}</td>
|
||||
<td class="pe-4"><span class="badge bg-secondary">{{ m.get_status_display }}</span></td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="4" class="text-center py-4 text-muted italic">История ТО пуста</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="b-pane" role="tabpanel">
|
||||
<div class="p-4 text-center text-muted">Журнал поломок пуст</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="p-pane" role="tabpanel">
|
||||
<div class="p-4 text-center text-muted">Заявок на запчасти нет</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="d-pane" role="tabpanel">
|
||||
<div class="p-4 text-center text-muted">Документы не загружены</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0 mt-4">
|
||||
<div class="card-header bg-white fw-bold">Примечания</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-0">{{ unit.notes|default:"Нет примечаний" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<!-- Actions & Quick Stats -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-3">Действия</h5>
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-primary py-3">
|
||||
<i class="bi bi-tools me-2"></i>Зафиксировать поломку
|
||||
</button>
|
||||
<button class="btn btn-outline-primary py-3">
|
||||
<i class="bi bi-calendar-plus me-2"></i>Запланировать ТО
|
||||
</button>
|
||||
<button class="btn btn-outline-info py-3">
|
||||
<i class="bi bi-cart-plus me-2"></i>Заявка на запчасть
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QR Code Placeholder -->
|
||||
<div class="card shadow-sm mb-4 text-center">
|
||||
<div class="card-body p-4">
|
||||
<h5 class="card-title mb-3">QR-код техники</h5>
|
||||
<div class="bg-light p-3 rounded mb-3 mx-auto" style="width: 200px; height: 200px;">
|
||||
<!-- Placeholder for QR Code generation -->
|
||||
<div class="w-100 h-100 border border-2 border-dashed d-flex align-items-center justify-content-center">
|
||||
<i class="bi bi-qr-code text-muted display-4"></i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small mb-3">Отсканируйте для быстрого доступа с мобильного телефона</p>
|
||||
<button class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-download me-2"></i>Скачать QR
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-2">Примечания</h5>
|
||||
<p class="text-muted small mb-0">{{ unit.notes|default:"Примечания отсутствуют" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@ -1,154 +1,160 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Дашборд | Fleet Manager{% endblock %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-12">
|
||||
<div class="hero-gradient px-4 px-md-5 d-flex align-items-center">
|
||||
<div>
|
||||
<h1 class="display-5 mb-2">Управление парком техники</h1>
|
||||
<p class="lead opacity-75">Централизованная система контроля, обслуживания и учета вашей техники.</p>
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<a href="{% url 'fleet_add' %}" class="btn btn-primary btn-lg">
|
||||
<i class="bi bi-plus-lg me-2"></i>Добавить технику
|
||||
</a>
|
||||
<a href="{% url 'fleet_list' %}" class="btn btn-outline-light btn-lg">
|
||||
<i class="bi bi-list-ul me-2"></i>Весь список
|
||||
</a>
|
||||
<div class="container-fluid py-4">
|
||||
<!-- Stats Row -->
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm bg-slate-900 text-white p-3 h-100">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="rounded-circle bg-blue-500 p-3 me-3">
|
||||
<i class="bi bi-truck fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="fw-bold mb-0">{{ total_units }}</h3>
|
||||
<span class="text-white-50 small">Всего техники</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm p-3 h-100">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="rounded-circle bg-success-subtle p-3 me-3 text-success">
|
||||
<i class="bi bi-check-circle fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="fw-bold mb-0 text-success">{{ active_units }}</h3>
|
||||
<span class="text-muted small">В работе</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm p-3 h-100">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="rounded-circle bg-danger-subtle p-3 me-3 text-danger">
|
||||
<i class="bi bi-exclamation-triangle fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="fw-bold mb-0 text-danger">{{ broken_units }}</h3>
|
||||
<span class="text-muted small">Сломано</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm p-3 h-100">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="rounded-circle bg-warning-subtle p-3 me-3 text-warning">
|
||||
<i class="bi bi-wrench fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="fw-bold mb-0 text-warning">{{ repair_units }}</h3>
|
||||
<span class="text-muted small">В ремонте</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card stat-card shadow-sm h-100">
|
||||
<div class="stat-icon bg-primary bg-opacity-10 text-primary">
|
||||
<i class="bi bi-truck"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small">Всего единиц</div>
|
||||
<div class="h3 mb-0">{{ total_units }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card stat-card shadow-sm h-100">
|
||||
<div class="stat-icon bg-success bg-opacity-10 text-success">
|
||||
<i class="bi bi-check-circle"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small">В работе</div>
|
||||
<div class="h3 mb-0">{{ active_units }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card stat-card shadow-sm h-100">
|
||||
<div class="stat-icon bg-danger bg-opacity-10 text-danger">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small">Сломано</div>
|
||||
<div class="h3 mb-0">{{ broken_units }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card stat-card shadow-sm h-100">
|
||||
<div class="stat-icon bg-warning bg-opacity-10 text-warning">
|
||||
<i class="bi bi-tools"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small">В ремонте</div>
|
||||
<div class="h3 mb-0">{{ repair_units }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Предстоящие ТО</h5>
|
||||
<a href="#" class="btn btn-sm btn-link text-decoration-none">Все ТО</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="row g-4">
|
||||
<!-- Recent Activities -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
||||
<h6 class="fw-bold mb-0">Ближайшие ТО</h6>
|
||||
<a href="{% url 'maintenance_list' %}" class="btn btn-sm btn-link">Все ТО</a>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="ps-4">Техника</th>
|
||||
<th>Техника</th>
|
||||
<th>Тип</th>
|
||||
<th>Дата</th>
|
||||
<th class="pe-4">Статус</th>
|
||||
<th>Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in recent_maintenances %}
|
||||
<tr>
|
||||
<td class="ps-4 fw-medium">{{ m.fleet_unit.name }}</td>
|
||||
<td><a href="{{ m.fleet_unit.get_absolute_url }}" class="text-decoration-none fw-bold">{{ m.fleet_unit.name }}</a></td>
|
||||
<td>{{ m.m_type }}</td>
|
||||
<td>{{ m.planned_date|date:"d.m.Y" }}</td>
|
||||
<td class="pe-4">
|
||||
<span class="badge bg-{{ m.status|yesno:'info,warning,success' }}">
|
||||
{{ m.get_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ m.planned_date }}</td>
|
||||
<td><span class="badge bg-{{ m.status|yesno:'success,warning,secondary' }} rounded-pill small">{{ m.get_status_display }}</span></td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-4 text-muted italic">Нет запланированных ТО</td>
|
||||
</tr>
|
||||
<tr><td colspan="4" class="text-center py-4 text-muted">Нет запланированных ТО</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Последние поломки</h5>
|
||||
<a href="#" class="btn btn-sm btn-link text-decoration-none">Журнал</a>
|
||||
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
||||
<h6 class="fw-bold mb-0">Последние поломки</h6>
|
||||
<a href="{% url 'breakdown_list' %}" class="btn btn-sm btn-link">Весь журнал</a>
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
{% for b in recent_breakdowns %}
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center py-3">
|
||||
<div>
|
||||
<div class="fw-bold text-danger">{{ b.system_node }}</div>
|
||||
<div class="small text-muted">{{ b.fleet_unit.name }} — {{ b.date|date:"d.m.Y" }}</div>
|
||||
</div>
|
||||
<span class="badge bg-light text-dark border">{{ b.get_status_display }}</span>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="list-group-item text-center py-4 text-muted">Поломок не зафиксировано</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="ps-4">Техника</th>
|
||||
<th>Узел</th>
|
||||
<th>Дата</th>
|
||||
<th class="pe-4">Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for b in recent_breakdowns %}
|
||||
<tr>
|
||||
<td class="ps-4 fw-medium">{{ b.fleet_unit.name }}</td>
|
||||
<td>{{ b.system_node }}</td>
|
||||
<td>{{ b.date|date:"d.m.Y" }}</td>
|
||||
<td class="pe-4">
|
||||
<span class="badge bg-{{ b.status|yesno:'danger,success,warning' }}">
|
||||
{{ b.get_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-4 text-muted italic">Поломок не зафиксировано</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions & Search -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<h6 class="fw-bold mb-3">Быстрый поиск</h6>
|
||||
<form action="{% url 'fleet_list' %}" method="get">
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" name="search" class="form-control border-end-0" placeholder="Госномер или VIN...">
|
||||
<button class="btn btn-outline-secondary border-start-0" type="submit">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<h6 class="fw-bold mb-3">Быстрые действия</h6>
|
||||
<div class="d-grid gap-2">
|
||||
<a href="{% url 'fleet_add' %}" class="btn btn-primary py-2 rounded-pill">
|
||||
<i class="bi bi-plus-lg me-2"></i>Добавить технику
|
||||
</a>
|
||||
<a href="{% url 'maintenance_add' %}" class="btn btn-outline-primary py-2 rounded-pill">
|
||||
<i class="bi bi-calendar-plus me-2"></i>Запланировать ТО
|
||||
</a>
|
||||
<a href="{% url 'breakdown_add' %}" class="btn btn-outline-danger py-2 rounded-pill">
|
||||
<i class="bi bi-exclamation-circle me-2"></i>Заявить о поломке
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm bg-blue-50">
|
||||
<div class="card-body">
|
||||
<h6 class="fw-bold text-blue-900 mb-2">Мобильное приложение</h6>
|
||||
<p class="small text-blue-800 mb-3">Приложение оптимизировано для работы механиков с телефона. Используйте QR-коды для быстрого доступа к чек-листам.</p>
|
||||
<i class="bi bi-phone-vibrate text-blue-500 display-4 d-block text-center"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
133
core/templates/core/maintenance_detail.html
Normal file
133
core/templates/core/maintenance_detail.html
Normal file
@ -0,0 +1,133 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm mb-4 border-0">
|
||||
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0 fw-bold">
|
||||
<i class="bi bi-tools text-primary me-2"></i>
|
||||
{{ maintenance.m_type }} - {{ maintenance.fleet_unit.name }}
|
||||
</h5>
|
||||
<span class="badge bg-{{ maintenance.status|yesno:'success,warning,secondary' }} rounded-pill px-3">
|
||||
{% if maintenance.status == 'planned' %}Планируется
|
||||
{% elif maintenance.status == 'in_progress' %}В процессе
|
||||
{% else %}Выполнено{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<p class="text-muted small mb-1">Техника</p>
|
||||
<p class="fw-bold mb-3"><a href="{{ maintenance.fleet_unit.get_absolute_url }}">{{ maintenance.fleet_unit }}</a></p>
|
||||
|
||||
<p class="text-muted small mb-1">Плановая дата</p>
|
||||
<p class="fw-bold mb-3">{{ maintenance.planned_date }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p class="text-muted small mb-1">Исполнитель</p>
|
||||
<p class="fw-bold mb-3">{{ maintenance.mechanic|default:"Не назначен" }}</p>
|
||||
|
||||
<p class="text-muted small mb-1">Плановый пробег/моточасы</p>
|
||||
<p class="fw-bold mb-3">{{ maintenance.planned_runtime }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h6 class="fw-bold mb-3">Чек-лист операций</h6>
|
||||
<div class="list-group list-group-flush border rounded mb-4">
|
||||
{% for item in maintenance.checklist %}
|
||||
<div class="list-group-item d-flex align-items-center py-3">
|
||||
<form action="{% url 'maintenance_process' maintenance.pk %}" method="post" class="d-flex align-items-center w-100">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="task_index" value="{{ forloop.counter0 }}">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="done" value="true"
|
||||
id="task_{{ forloop.counter0 }}"
|
||||
{% if item.done %}checked{% endif %}
|
||||
onchange="this.form.submit()">
|
||||
<label class="form-check-label {% if item.done %}text-decoration-line-through text-muted{% endif %}" for="task_{{ forloop.counter0 }}">
|
||||
{{ item.task }}
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="list-group-item text-muted text-center py-4">Чек-лист пуст</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if maintenance.status != 'completed' %}
|
||||
<div class="mt-4 pt-3 border-top">
|
||||
<h6 class="fw-bold mb-3">Завершение ТО</h6>
|
||||
<form action="{% url 'maintenance_complete' maintenance.pk %}" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">Фактическая дата</label>
|
||||
<input type="date" name="actual_date" class="form-control" value="{% now 'Y-m-d' %}" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold">Фактический пробег/моточасы</label>
|
||||
<input type="number" name="actual_runtime" class="form-control" required>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label small fw-bold">Использованные запчасти</label>
|
||||
<textarea name="parts_used" class="form-control" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-success w-100 py-2 fw-bold">
|
||||
<i class="bi bi-check-circle me-2"></i>Завершить ТО и обновить статус техники
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mt-4 pt-3 border-top">
|
||||
<div class="alert alert-success d-flex align-items-center">
|
||||
<i class="bi bi-info-circle-fill me-2"></i>
|
||||
<div>ТО успешно завершено {{ maintenance.actual_date }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p class="text-muted small mb-1">Фактический пробег/моточасы</p>
|
||||
<p class="fw-bold">{{ maintenance.actual_runtime }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p class="text-muted small mb-1">Запчасти</p>
|
||||
<p>{{ maintenance.parts_used|default:"-" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{% url 'maintenance_pdf' maintenance.pk %}" class="btn btn-outline-primary btn-sm mt-2">
|
||||
<i class="bi bi-file-pdf me-1"></i>Скачать Акт ТО (PDF)
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white fw-bold py-3">Примечания</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-0">{{ maintenance.notes|default:"Нет примечаний" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if maintenance.fleet_unit.photo %}
|
||||
<div class="card shadow-sm border-0">
|
||||
<img src="{{ maintenance.fleet_unit.photo.url }}" class="card-img-top" alt="{{ maintenance.fleet_unit.name }}">
|
||||
<div class="card-body">
|
||||
<p class="card-text small text-muted text-center">{{ maintenance.fleet_unit.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
46
core/templates/core/maintenance_form.html
Normal file
46
core/templates/core/maintenance_form.html
Normal file
@ -0,0 +1,46 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-white py-3 fw-bold">
|
||||
{% if form.instance.pk %}Редактировать ТО{% else %}Запланировать ТО{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-bold">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% if field.help_text %}
|
||||
<div class="form-text">{{ field.help_text }}</div>
|
||||
{% endif %}
|
||||
{% for error in field.errors %}
|
||||
<div class="text-danger small">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="mt-4 pt-3 border-top">
|
||||
<button type="submit" class="btn btn-primary px-4 rounded-pill">Сохранить</button>
|
||||
<a href="{% url 'maintenance_list' %}" class="btn btn-link text-muted">Отмена</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Add bootstrap classes to form fields
|
||||
document.querySelectorAll('input, select, textarea').forEach(el => {
|
||||
if (!el.classList.contains('form-check-input')) {
|
||||
el.classList.add('form-control');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
57
core/templates/core/maintenance_list.html
Normal file
57
core/templates/core/maintenance_list.html
Normal file
@ -0,0 +1,57 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h3 class="fw-bold mb-0">Техническое обслуживание</h3>
|
||||
<a href="{% url 'maintenance_add' %}" class="btn btn-primary rounded-pill px-4">
|
||||
<i class="bi bi-plus-lg me-2"></i>Запланировать ТО
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="border-0 px-4">Техника</th>
|
||||
<th class="border-0">Тип</th>
|
||||
<th class="border-0">Плановая дата</th>
|
||||
<th class="border-0">Статус</th>
|
||||
<th class="border-0">Исполнитель</th>
|
||||
<th class="border-0 text-end px-4">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in maintenances %}
|
||||
<tr>
|
||||
<td class="px-4">
|
||||
<div class="fw-bold"><a href="{{ m.fleet_unit.get_absolute_url }}">{{ m.fleet_unit.name }}</a></div>
|
||||
<div class="small text-muted">{{ m.fleet_unit.plate_number|default:m.fleet_unit.vin }}</div>
|
||||
</td>
|
||||
<td><span class="badge bg-light text-dark border">{{ m.get_m_type_display }}</span></td>
|
||||
<td>{{ m.planned_date }}</td>
|
||||
<td>
|
||||
<span class="badge bg-{{ m.status|yesno:'success,warning,secondary' }} rounded-pill">
|
||||
{% if m.status == 'planned' %}Планируется
|
||||
{% elif m.status == 'in_progress' %}В процессе
|
||||
{% else %}Выполнено{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ m.mechanic|default:"-" }}</td>
|
||||
<td class="text-end px-4">
|
||||
<a href="{% url 'maintenance_detail' m.pk %}" class="btn btn-sm btn-outline-primary rounded-pill px-3">Открыть</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-5 text-muted">Нет запланированных ТО</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
42
core/templates/core/part_request_form.html
Normal file
42
core/templates/core/part_request_form.html
Normal file
@ -0,0 +1,42 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-6">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-white py-3 fw-bold">
|
||||
<i class="bi bi-cart-plus text-primary me-2"></i>Создать заявку на запчасть
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-bold">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% for error in field.errors %}
|
||||
<div class="text-danger small">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="mt-4 pt-3 border-top">
|
||||
<button type="submit" class="btn btn-primary px-4 rounded-pill">Отправить в снабжение</button>
|
||||
<a href="{% url 'fleet_list' %}" class="btn btn-link text-muted">Отмена</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('input, select, textarea').forEach(el => {
|
||||
if (!el.classList.contains('form-check-input')) {
|
||||
el.classList.add('form-control');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
69
core/templates/core/part_request_list.html
Normal file
69
core/templates/core/part_request_list.html
Normal file
@ -0,0 +1,69 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h3 class="fw-bold mb-0">Заявки на запчасти</h3>
|
||||
<a href="{% url 'part_request_add' %}" class="btn btn-primary rounded-pill px-4">
|
||||
<i class="bi bi-plus-lg me-2"></i>Новая заявка
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="border-0 px-4">Техника</th>
|
||||
<th class="border-0">Деталь</th>
|
||||
<th class="border-0">Артикул</th>
|
||||
<th class="border-0">Кол-во</th>
|
||||
<th class="border-0">Статус</th>
|
||||
<th class="border-0">Дата создания</th>
|
||||
<th class="border-0 text-end px-4">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in requests %}
|
||||
<tr>
|
||||
<td class="px-4">
|
||||
<div class="fw-bold">{{ r.fleet_unit.name }}</div>
|
||||
<div class="small text-muted">{{ r.fleet_unit.plate_number|default:r.fleet_unit.vin }}</div>
|
||||
</td>
|
||||
<td>{{ r.part_name }}</td>
|
||||
<td>{{ r.article_number|default:"-" }}</td>
|
||||
<td>{{ r.quantity }}</td>
|
||||
<td>
|
||||
{% if r.status == 'draft' %}<span class="badge bg-secondary">Черновик</span>
|
||||
{% elif r.status == 'sent' %}<span class="badge bg-info text-white">Отправлено</span>
|
||||
{% elif r.status == 'ordered' %}<span class="badge bg-primary">Заказано</span>
|
||||
{% elif r.status == 'delivered' %}<span class="badge bg-success">Доставлено</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="small">{{ r.created_at|date:"d.m.Y H:i" }}</td>
|
||||
<td class="text-end px-4">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle rounded-pill px-3" data-bs-toggle="dropdown">
|
||||
Действие
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow-sm border-0">
|
||||
<li><a class="dropdown-item" href="#">Изменить статус</a></li>
|
||||
<li><a class="dropdown-item" href="#">Экспорт Excel</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item text-danger" href="#">Удалить</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-5 text-muted">Нет активных заявок</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
19
core/urls.py
19
core/urls.py
@ -3,8 +3,27 @@ from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.IndexView.as_view(), name='index'),
|
||||
|
||||
# Fleet
|
||||
path('fleet/', views.FleetListView.as_view(), name='fleet_list'),
|
||||
path('fleet/<int:pk>/', views.FleetDetailView.as_view(), name='fleet_detail'),
|
||||
path('fleet/add/', views.FleetCreateView.as_view(), name='fleet_add'),
|
||||
path('fleet/<int:pk>/edit/', views.FleetUpdateView.as_view(), name='fleet_edit'),
|
||||
|
||||
# Maintenance
|
||||
path('maintenance/', views.MaintenanceListView.as_view(), name='maintenance_list'),
|
||||
path('maintenance/<int:pk>/', views.MaintenanceDetailView.as_view(), name='maintenance_detail'),
|
||||
path('maintenance/add/', views.MaintenanceCreateView.as_view(), name='maintenance_add'),
|
||||
path('maintenance/<int:pk>/edit/', views.MaintenanceUpdateView.as_view(), name='maintenance_edit'),
|
||||
path('maintenance/<int:pk>/process/', views.MaintenanceProcessView.as_view(), name='maintenance_process'),
|
||||
path('maintenance/<int:pk>/complete/', views.MaintenanceCompleteView.as_view(), name='maintenance_complete'),
|
||||
path('maintenance/<int:pk>/pdf/', views.MaintenancePDFView.as_view(), name='maintenance_pdf'),
|
||||
|
||||
# Breakdown
|
||||
path('breakdown/', views.BreakdownListView.as_view(), name='breakdown_list'),
|
||||
path('breakdown/add/', views.BreakdownCreateView.as_view(), name='breakdown_add'),
|
||||
|
||||
# Part Request
|
||||
path('part-request/', views.PartRequestListView.as_view(), name='part_request_list'),
|
||||
path('part-request/add/', views.PartRequestCreateView.as_view(), name='part_request_add'),
|
||||
]
|
||||
223
core/views.py
223
core/views.py
@ -1,10 +1,19 @@
|
||||
from django.shortcuts import render
|
||||
from django.views.generic import ListView, DetailView, CreateView, UpdateView, TemplateView
|
||||
from django.urls import reverse_lazy
|
||||
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
|
||||
from django.urls import reverse_lazy, reverse
|
||||
from django import forms
|
||||
from django.db.models import Count
|
||||
from .models import FleetUnit, Maintenance, Breakdown, PartRequest, Category
|
||||
from django.db.models import Count, Q
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
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
|
||||
|
||||
# Forms
|
||||
class FleetUnitForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = FleetUnit
|
||||
@ -14,6 +23,33 @@ class FleetUnitForm(forms.ModelForm):
|
||||
'notes': forms.Textarea(attrs={'rows': 3}),
|
||||
}
|
||||
|
||||
class MaintenanceForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Maintenance
|
||||
fields = ['fleet_unit', 'm_type', 'planned_date', 'planned_runtime', 'mechanic', 'notes']
|
||||
widgets = {
|
||||
'planned_date': forms.DateInput(attrs={'type': 'date'}),
|
||||
'notes': forms.Textarea(attrs={'rows': 3}),
|
||||
}
|
||||
|
||||
class BreakdownForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Breakdown
|
||||
fields = ['fleet_unit', 'system_node', 'description', 'photo', 'status', 'notes']
|
||||
widgets = {
|
||||
'description': forms.Textarea(attrs={'rows': 3}),
|
||||
'notes': forms.Textarea(attrs={'rows': 3}),
|
||||
}
|
||||
|
||||
class PartRequestForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = PartRequest
|
||||
fields = ['fleet_unit', 'breakdown', 'part_name', 'article_number', 'quantity', 'photo', 'notes']
|
||||
widgets = {
|
||||
'notes': forms.Textarea(attrs={'rows': 3}),
|
||||
}
|
||||
|
||||
# Views
|
||||
class IndexView(TemplateView):
|
||||
template_name = 'core/index.html'
|
||||
|
||||
@ -24,14 +60,13 @@ class IndexView(TemplateView):
|
||||
context['broken_units'] = FleetUnit.objects.filter(status='broken').count()
|
||||
context['repair_units'] = FleetUnit.objects.filter(status='repair').count()
|
||||
|
||||
# Stats for charts or badges
|
||||
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'
|
||||
@ -45,7 +80,11 @@ class FleetListView(ListView):
|
||||
queryset = queryset.filter(status=status)
|
||||
search = self.request.GET.get('search')
|
||||
if search:
|
||||
queryset = queryset.filter(name__icontains=search) | queryset.filter(plate_number__icontains=search) | queryset.filter(vin__icontains=search)
|
||||
queryset = queryset.filter(
|
||||
Q(name__icontains=search) |
|
||||
Q(plate_number__icontains=search) |
|
||||
Q(vin__icontains=search)
|
||||
)
|
||||
return queryset
|
||||
|
||||
class FleetDetailView(DetailView):
|
||||
@ -55,9 +94,9 @@ class FleetDetailView(DetailView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['maintenances'] = self.object.maintenances.all()
|
||||
context['breakdowns'] = self.object.breakdowns.all()
|
||||
context['part_requests'] = self.object.part_requests.all()
|
||||
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):
|
||||
@ -72,4 +111,164 @@ class FleetUpdateView(UpdateView):
|
||||
form_class = FleetUnitForm
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('fleet_detail', kwargs={'pk': self.object.pk})
|
||||
return reverse_lazy('fleet_detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
# 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'
|
||||
|
||||
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 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 MaintenancePDFView(View):
|
||||
def get(self, request, pk):
|
||||
maintenance = get_object_or_404(Maintenance, pk=pk)
|
||||
buffer = BytesIO()
|
||||
p = canvas.Canvas(buffer, pagesize=A4)
|
||||
|
||||
# Draw PDF content (basic for now as we don't have Cyrillic fonts loaded by default in reportlab without setup)
|
||||
# Note: For real Cyrillic support we need to register a TTF font.
|
||||
p.drawString(100, 800, f"Maintenance Act - {maintenance.m_type}")
|
||||
p.drawString(100, 780, f"Unit: {maintenance.fleet_unit.name}")
|
||||
p.drawString(100, 760, f"Date: {maintenance.actual_date}")
|
||||
p.drawString(100, 740, f"Runtime: {maintenance.actual_runtime}")
|
||||
|
||||
y = 700
|
||||
p.drawString(100, y, "Checklist:")
|
||||
y -= 20
|
||||
for item in maintenance.checklist:
|
||||
status = "[x]" if item['done'] else "[ ]"
|
||||
p.drawString(120, y, f"{status} {item['task']}")
|
||||
y -= 20
|
||||
|
||||
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})
|
||||
|
||||
# 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})
|
||||
@ -1,3 +1,6 @@
|
||||
Django==5.2.7
|
||||
mysqlclient==2.2.7
|
||||
python-dotenv==1.1.1
|
||||
Pillow==10.3.0
|
||||
qrcode==7.4.2
|
||||
reportlab==4.2.0
|
||||
Loading…
x
Reference in New Issue
Block a user