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.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="Количество")

View File

@ -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">&copy; {% now "Y" %} Fleet Manager. Все права защищены.</p>
<p class="text-muted small mb-0">&copy; {% 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>

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' %}
{% 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 %}

View File

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

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 = [
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'),
]

View File

@ -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})

View File

@ -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