This commit is contained in:
Flatlogic Bot 2026-01-27 19:35:13 +00:00
parent df45ad71cc
commit a5f35ca313
20 changed files with 1069 additions and 313 deletions

View File

@ -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='Тип ТО'),
),
]

View 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),
]

View File

@ -1,5 +1,10 @@
import qrcode
from io import BytesIO
from django.core.files import File
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.contrib.auth.models import User
from django.conf import settings
class Category(models.Model): class Category(models.Model):
name = models.CharField(max_length=100, verbose_name="Наименование") 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="Статус") status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active', verbose_name="Статус")
commissioning_date = models.DateField(verbose_name="Дата ввода в эксплуатацию") commissioning_date = models.DateField(verbose_name="Дата ввода в эксплуатацию")
notes = models.TextField(blank=True, null=True, 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) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
@ -55,12 +61,38 @@ class FleetUnit(models.Model):
} }
return colors.get(self.status, 'primary') 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): class Maintenance(models.Model):
TYPE_CHOICES = [ TYPE_CHOICES = [
('TO-250', 'ТО-250'), ('TO-250', 'ТО-250'),
('TO-500', 'ТО-500'), ('TO-500', 'ТО-500'),
('seasonal', 'Сезонное'), ('seasonal', 'Сезонное'),
('individual', 'Индивидуальное'), ('special', 'Спец'),
] ]
STATUS_CHOICES = [ STATUS_CHOICES = [
('planned', 'Планируется'), ('planned', 'Планируется'),
@ -72,10 +104,14 @@ class Maintenance(models.Model):
m_type = models.CharField(max_length=50, choices=TYPE_CHOICES, verbose_name="Тип ТО") m_type = models.CharField(max_length=50, choices=TYPE_CHOICES, verbose_name="Тип ТО")
planned_date = models.DateField(verbose_name="Плановая дата") planned_date = models.DateField(verbose_name="Плановая дата")
planned_runtime = models.PositiveIntegerField(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="Статус") 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_date = models.DateField(null=True, blank=True, verbose_name="Фактическая дата")
actual_runtime = models.PositiveIntegerField(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="Примечания") 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="Дата") date = models.DateField(auto_now_add=True, verbose_name="Дата")
system_node = models.CharField(max_length=255, verbose_name="Узел / система") system_node = models.CharField(max_length=255, verbose_name="Узел / система")
description = models.TextField(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="Статус") status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='reported', verbose_name="Статус")
repair_date = models.DateField(null=True, blank=True, 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="Стоимость") cost = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Стоимость")
class Meta: 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="Техника") 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="Наименование детали") part_name = models.CharField(max_length=255, verbose_name="Наименование детали")
article_number = models.CharField(max_length=100, blank=True, null=True, verbose_name="Артикул") article_number = models.CharField(max_length=100, blank=True, null=True, verbose_name="Артикул")
quantity = models.PositiveIntegerField(default=1, verbose_name="Количество") quantity = models.PositiveIntegerField(default=1, verbose_name="Количество")

View File

@ -26,10 +26,10 @@
<nav class="navbar navbar-expand-lg navbar-dark bg-slate-900 sticky-top shadow-sm"> <nav class="navbar navbar-expand-lg navbar-dark bg-slate-900 sticky-top shadow-sm">
<div class="container"> <div class="container">
<a class="navbar-brand d-flex align-items-center" href="{% url 'index' %}"> <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> <span class="fw-bold text-uppercase tracking-wider">Fleet<span class="text-blue-500">Manager</span></span>
</a> </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> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarSupportedContent"> <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> <a class="nav-link {% if request.resolver_match.url_name == 'index' %}active{% endif %}" href="{% url 'index' %}">Дашборд</a>
</li> </li>
<li class="nav-item"> <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>
<li class="nav-item"> <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> </li>
</ul> </ul>
<div class="d-flex align-items-center"> <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"> <div class="dropdown">
<a href="#" class="d-block link-light text-decoration-none dropdown-toggle" id="dropdownUser1" data-bs-toggle="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> </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><a class="dropdown-item" href="#">Профиль</a></li>
<li><hr class="dropdown-divider"></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> </ul>
</div> </div>
</div> </div>
@ -62,14 +68,12 @@
</nav> </nav>
<main class="py-4"> <main class="py-4">
<div class="container"> {% block content %}{% endblock %}
{% block content %}{% endblock %}
</div>
</main> </main>
<footer class="py-4 bg-white mt-auto border-top"> <footer class="py-4 bg-white mt-auto border-top">
<div class="container text-center"> <div class="container text-center">
<p class="text-muted mb-0">&copy; {% now "Y" %} Fleet Manager. Все права защищены.</p> <p class="text-muted small mb-0">&copy; {% now "Y" %} Fleet Manager. Разработано для управления парком техники.</p>
</div> </div>
</footer> </footer>
@ -77,4 +81,4 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
{% block extra_js %}{% endblock %} {% block extra_js %}{% endblock %}
</body> </body>
</html> </html>

View 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 %}

View File

@ -1,186 +1,185 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %}
{% block title %}{{ unit.name }} | Fleet Manager{% endblock %}
{% block content %} {% block content %}
<div class="mb-4"> <div class="container-fluid py-4">
<nav aria-label="breadcrumb"> <div class="row">
<ol class="breadcrumb mb-2"> <!-- Sidebar: Info & QR -->
<li class="breadcrumb-item"><a href="{% url 'index' %}" class="text-decoration-none">Дашборд</a></li> <div class="col-lg-4">
<li class="breadcrumb-item"><a href="{% url 'fleet_list' %}" class="text-decoration-none">Техника</a></li> <div class="card shadow-sm border-0 mb-4">
<li class="breadcrumb-item active" aria-current="page">{{ unit.name }}</li> {% if unit.photo %}
</ol> <img src="{{ unit.photo.url }}" class="card-img-top" alt="{{ unit.name }}" style="height: 250px; object-fit: cover;">
</nav> {% else %}
<div class="d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center gap-3"> <div class="bg-light text-center py-5">
<h2 class="mb-0">{{ unit.name }}</h2> <i class="bi bi-truck text-muted display-1"></i>
<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> </div>
<div class="col-md-7"> {% endif %}
<div class="card-body p-4"> <div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-start mb-3">
<span class="badge bg-{{ unit.get_status_color }} fs-6"> <div>
{{ unit.get_status_display }} <h4 class="fw-bold mb-0">{{ unit.name }}</h4>
</span> <span class="text-muted small">{{ unit.model_name }}</span>
<div class="text-muted">ID: {{ unit.pk }}</div>
</div> </div>
<span class="badge bg-{{ unit.get_status_color }} rounded-pill px-3 py-2">
<div class="row g-3 mb-4"> {{ unit.get_status_display }}
<div class="col-6"> </span>
<label class="text-muted small d-block">Категория</label> </div>
<span class="fw-medium">{{ unit.category|default:"Не указана" }}</span>
<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>
<div class="col-6"> <div class="table-responsive">
<label class="text-muted small d-block">Модель</label> <table class="table table-sm align-middle">
<span class="fw-medium">{{ unit.model_name }}</span> <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>
<div class="col-6"> </div>
<label class="text-muted small d-block">Госномер</label>
<span class="fw-medium">{{ unit.plate_number|default:"-" }}</span> <!-- 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>
<div class="col-6"> <div class="list-group list-group-flush">
<label class="text-muted small d-block">Год выпуска</label> {% for b in breakdowns %}
<span class="fw-medium">{{ unit.year }}</span> <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>
<div class="col-12"> </div>
<label class="text-muted small d-block">VIN / Серийный номер</label>
<code class="fw-bold text-dark fs-6">{{ unit.vin }}</code> <!-- 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>
<div class="col-12"> <div class="table-responsive">
<label class="text-muted small d-block">Ввод в эксплуатацию</label> <table class="table table-sm align-middle">
<span class="fw-medium">{{ unit.commissioning_date|date:"d.m.Y" }}</span> <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>
</div> </div>
</div> </div>
</div>
<div class="card shadow-sm border-0 mt-4">
<!-- History Tabs --> <div class="card-header bg-white fw-bold">Примечания</div>
<div class="card shadow-sm"> <div class="card-body">
<div class="card-header bg-white border-bottom-0 p-0"> <p class="mb-0">{{ unit.notes|default:"Нет примечаний" }}</p>
<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> </div>
</div> </div>
</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> </div>
{% endblock %} {% endblock %}

View File

@ -1,154 +1,160 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %}
{% block title %}Дашборд | Fleet Manager{% endblock %}
{% block content %} {% block content %}
<div class="row mb-4"> <div class="container-fluid py-4">
<div class="col-md-12"> <!-- Stats Row -->
<div class="hero-gradient px-4 px-md-5 d-flex align-items-center"> <div class="row g-4 mb-4">
<div> <div class="col-md-3">
<h1 class="display-5 mb-2">Управление парком техники</h1> <div class="card border-0 shadow-sm bg-slate-900 text-white p-3 h-100">
<p class="lead opacity-75">Централизованная система контроля, обслуживания и учета вашей техники.</p> <div class="d-flex align-items-center">
<div class="d-flex gap-2 mt-4"> <div class="rounded-circle bg-blue-500 p-3 me-3">
<a href="{% url 'fleet_add' %}" class="btn btn-primary btn-lg"> <i class="bi bi-truck fs-4"></i>
<i class="bi bi-plus-lg me-2"></i>Добавить технику </div>
</a> <div>
<a href="{% url 'fleet_list' %}" class="btn btn-outline-light btn-lg"> <h3 class="fw-bold mb-0">{{ total_units }}</h3>
<i class="bi bi-list-ul me-2"></i>Весь список <span class="text-white-50 small">Всего техники</span>
</a> </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>
</div> </div>
</div>
<div class="row g-4 mb-4"> <div class="row g-4">
<div class="col-6 col-md-3"> <!-- Recent Activities -->
<div class="card stat-card shadow-sm h-100"> <div class="col-lg-8">
<div class="stat-icon bg-primary bg-opacity-10 text-primary"> <div class="card border-0 shadow-sm mb-4">
<i class="bi bi-truck"></i> <div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
</div> <h6 class="fw-bold mb-0">Ближайшие ТО</h6>
<div> <a href="{% url 'maintenance_list' %}" class="btn btn-sm btn-link">Все ТО</a>
<div class="text-muted small">Всего единиц</div> </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="table-responsive"> <div class="table-responsive">
<table class="table table-hover align-middle mb-0"> <table class="table table-hover align-middle mb-0">
<thead class="bg-light"> <thead class="bg-light">
<tr> <tr>
<th class="ps-4">Техника</th> <th>Техника</th>
<th>Тип</th> <th>Тип</th>
<th>Дата</th> <th>Дата</th>
<th class="pe-4">Статус</th> <th>Статус</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for m in recent_maintenances %} {% for m in recent_maintenances %}
<tr> <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.m_type }}</td>
<td>{{ m.planned_date|date:"d.m.Y" }}</td> <td>{{ m.planned_date }}</td>
<td class="pe-4"> <td><span class="badge bg-{{ m.status|yesno:'success,warning,secondary' }} rounded-pill small">{{ m.get_status_display }}</span></td>
<span class="badge bg-{{ m.status|yesno:'info,warning,success' }}">
{{ m.get_status_display }}
</span>
</td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr><td colspan="4" class="text-center py-4 text-muted">Нет запланированных ТО</td></tr>
<td colspan="4" class="text-center py-4 text-muted italic">Нет запланированных ТО</td>
</tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div>
</div> <div class="card border-0 shadow-sm">
<div class="col-lg-6"> <div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
<div class="card shadow-sm h-100"> <h6 class="fw-bold mb-0">Последние поломки</h6>
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center"> <a href="{% url 'breakdown_list' %}" class="btn btn-sm btn-link">Весь журнал</a>
<h5 class="mb-0">Последние поломки</h5> </div>
<a href="#" class="btn btn-sm btn-link text-decoration-none">Журнал</a> <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>
<div class="card-body p-0"> </div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0"> <!-- Quick Actions & Search -->
<thead class="bg-light"> <div class="col-lg-4">
<tr> <div class="card border-0 shadow-sm mb-4">
<th class="ps-4">Техника</th> <div class="card-body">
<th>Узел</th> <h6 class="fw-bold mb-3">Быстрый поиск</h6>
<th>Дата</th> <form action="{% url 'fleet_list' %}" method="get">
<th class="pe-4">Статус</th> <div class="input-group mb-3">
</tr> <input type="text" name="search" class="form-control border-end-0" placeholder="Госномер или VIN...">
</thead> <button class="btn btn-outline-secondary border-start-0" type="submit">
<tbody> <i class="bi bi-search"></i>
{% for b in recent_breakdowns %} </button>
<tr> </div>
<td class="ps-4 fw-medium">{{ b.fleet_unit.name }}</td> </form>
<td>{{ b.system_node }}</td> </div>
<td>{{ b.date|date:"d.m.Y" }}</td> </div>
<td class="pe-4">
<span class="badge bg-{{ b.status|yesno:'danger,success,warning' }}"> <div class="card border-0 shadow-sm mb-4">
{{ b.get_status_display }} <div class="card-body">
</span> <h6 class="fw-bold mb-3">Быстрые действия</h6>
</td> <div class="d-grid gap-2">
</tr> <a href="{% url 'fleet_add' %}" class="btn btn-primary py-2 rounded-pill">
{% empty %} <i class="bi bi-plus-lg me-2"></i>Добавить технику
<tr> </a>
<td colspan="4" class="text-center py-4 text-muted italic">Поломок не зафиксировано</td> <a href="{% url 'maintenance_add' %}" class="btn btn-outline-primary py-2 rounded-pill">
</tr> <i class="bi bi-calendar-plus me-2"></i>Запланировать ТО
{% endfor %} </a>
</tbody> <a href="{% url 'breakdown_add' %}" class="btn btn-outline-danger py-2 rounded-pill">
</table> <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> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View File

@ -3,8 +3,27 @@ from . import views
urlpatterns = [ urlpatterns = [
path('', views.IndexView.as_view(), name='index'), path('', views.IndexView.as_view(), name='index'),
# Fleet
path('fleet/', views.FleetListView.as_view(), name='fleet_list'), path('fleet/', views.FleetListView.as_view(), name='fleet_list'),
path('fleet/<int:pk>/', views.FleetDetailView.as_view(), name='fleet_detail'), path('fleet/<int:pk>/', views.FleetDetailView.as_view(), name='fleet_detail'),
path('fleet/add/', views.FleetCreateView.as_view(), name='fleet_add'), path('fleet/add/', views.FleetCreateView.as_view(), name='fleet_add'),
path('fleet/<int:pk>/edit/', views.FleetUpdateView.as_view(), name='fleet_edit'), 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'),
] ]

View File

@ -1,10 +1,19 @@
from django.shortcuts import render import json
from django.views.generic import ListView, DetailView, CreateView, UpdateView, TemplateView from io import BytesIO
from django.urls import reverse_lazy 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 import forms
from django.db.models import Count from django.db.models import Count, Q
from .models import FleetUnit, Maintenance, Breakdown, PartRequest, Category 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 FleetUnitForm(forms.ModelForm):
class Meta: class Meta:
model = FleetUnit model = FleetUnit
@ -14,6 +23,33 @@ class FleetUnitForm(forms.ModelForm):
'notes': forms.Textarea(attrs={'rows': 3}), '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): class IndexView(TemplateView):
template_name = 'core/index.html' template_name = 'core/index.html'
@ -24,14 +60,13 @@ class IndexView(TemplateView):
context['broken_units'] = FleetUnit.objects.filter(status='broken').count() context['broken_units'] = FleetUnit.objects.filter(status='broken').count()
context['repair_units'] = FleetUnit.objects.filter(status='repair').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['status_counts'] = FleetUnit.objects.values('status').annotate(total=Count('status'))
context['recent_maintenances'] = Maintenance.objects.all().order_by('-planned_date')[:5] context['recent_maintenances'] = Maintenance.objects.all().order_by('-planned_date')[:5]
context['recent_breakdowns'] = Breakdown.objects.all().order_by('-date')[:5] context['recent_breakdowns'] = Breakdown.objects.all().order_by('-date')[:5]
return context return context
# Fleet Views
class FleetListView(ListView): class FleetListView(ListView):
model = FleetUnit model = FleetUnit
template_name = 'core/fleet_list.html' template_name = 'core/fleet_list.html'
@ -45,7 +80,11 @@ class FleetListView(ListView):
queryset = queryset.filter(status=status) queryset = queryset.filter(status=status)
search = self.request.GET.get('search') search = self.request.GET.get('search')
if 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 return queryset
class FleetDetailView(DetailView): class FleetDetailView(DetailView):
@ -55,9 +94,9 @@ class FleetDetailView(DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['maintenances'] = self.object.maintenances.all() context['maintenances'] = self.object.maintenances.all().order_by('-planned_date')
context['breakdowns'] = self.object.breakdowns.all() context['breakdowns'] = self.object.breakdowns.all().order_by('-date')
context['part_requests'] = self.object.part_requests.all() context['part_requests'] = self.object.part_requests.all().order_by('-created_at')
return context return context
class FleetCreateView(CreateView): class FleetCreateView(CreateView):
@ -72,4 +111,164 @@ class FleetUpdateView(UpdateView):
form_class = FleetUnitForm form_class = FleetUnitForm
def get_success_url(self): 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})

View File

@ -1,3 +1,6 @@
Django==5.2.7 Django==5.2.7
mysqlclient==2.2.7 mysqlclient==2.2.7
python-dotenv==1.1.1 python-dotenv==1.1.1
Pillow==10.3.0
qrcode==7.4.2
reportlab==4.2.0