diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index ad86ac6..91a030d 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 8e219cc..7d8dc91 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index a7e7f85..b0cfab8 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/migrations/0002_breakdown_notes_breakdown_photo_fleetunit_qr_code_and_more.py b/core/migrations/0002_breakdown_notes_breakdown_photo_fleetunit_qr_code_and_more.py new file mode 100644 index 0000000..3ef5eab --- /dev/null +++ b/core/migrations/0002_breakdown_notes_breakdown_photo_fleetunit_qr_code_and_more.py @@ -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='Тип ТО'), + ), + ] diff --git a/core/migrations/0003_setup_groups.py b/core/migrations/0003_setup_groups.py new file mode 100644 index 0000000..8f6815e --- /dev/null +++ b/core/migrations/0003_setup_groups.py @@ -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), + ] \ No newline at end of file diff --git a/core/migrations/__pycache__/0002_breakdown_notes_breakdown_photo_fleetunit_qr_code_and_more.cpython-311.pyc b/core/migrations/__pycache__/0002_breakdown_notes_breakdown_photo_fleetunit_qr_code_and_more.cpython-311.pyc new file mode 100644 index 0000000..6be3e12 Binary files /dev/null and b/core/migrations/__pycache__/0002_breakdown_notes_breakdown_photo_fleetunit_qr_code_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0003_setup_groups.cpython-311.pyc b/core/migrations/__pycache__/0003_setup_groups.cpython-311.pyc new file mode 100644 index 0000000..3ca8cf1 Binary files /dev/null and b/core/migrations/__pycache__/0003_setup_groups.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 35097bc..41ce768 100644 --- a/core/models.py +++ b/core/models.py @@ -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="Количество") diff --git a/core/templates/base.html b/core/templates/base.html index c5be69c..7f6a241 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -26,10 +26,10 @@
-
- {% block content %}{% endblock %} -
+ {% block content %}{% endblock %}
@@ -77,4 +81,4 @@ {% block extra_js %}{% endblock %} - \ No newline at end of file + diff --git a/core/templates/core/breakdown_form.html b/core/templates/core/breakdown_form.html new file mode 100644 index 0000000..e4c41ac --- /dev/null +++ b/core/templates/core/breakdown_form.html @@ -0,0 +1,47 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+
+
+
+ Зафиксировать поломку +
+
+
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endfor %} +
+ + Отмена +
+
+
+
+ +
+ + После сохранения статус техники автоматически изменится на "Сломана" или "Ждёт деталь". +
+
+
+
+ + +{% endblock %} diff --git a/core/templates/core/fleet_detail.html b/core/templates/core/fleet_detail.html index 13657bc..448a6d1 100644 --- a/core/templates/core/fleet_detail.html +++ b/core/templates/core/fleet_detail.html @@ -1,186 +1,185 @@ {% extends 'base.html' %} - -{% block title %}{{ unit.name }} | Fleet Manager{% endblock %} +{% load static %} {% block content %} -
- -
-

{{ unit.name }}

-
- - Редактировать - - -
-
-
- -
-
- -
-
-
- {% if unit.photo %} - {{ unit.name }} - {% else %} -
- -
- {% endif %} +
+
+ +
+
+ {% if unit.photo %} + {{ unit.name }} + {% else %} +
+
-
-
-
- - {{ unit.get_status_display }} - -
ID: {{ unit.pk }}
+ {% endif %} +
+
+
+

{{ unit.name }}

+ {{ unit.model_name }}
- -
-
- - {{ unit.category|default:"Не указана" }} + + {{ unit.get_status_display }} + +
+ +
+
+

Госномер

+

{{ unit.plate_number|default:"-" }}

+
+
+

Год выпуска

+

{{ unit.year }}

+
+
+

VIN / Серийный номер

+

{{ unit.vin }}

+
+
+ + +
+
+ +
+
QR-код техники
+ {% if unit.qr_code %} + QR Code + {% else %} +
QR-код генерируется...
+ {% endif %} +

Наклейте этот код на технику для быстрого доступа механика.

+ +
+
+ + +
+
+
+ +
+
+
+ +
+
+
Плановое и выполненное ТО
+ + Запланировать
-
- - {{ unit.model_name }} +
+ + + + + + + + + + + + {% for m in maintenances %} + + + + + + + + {% empty %} + + {% endfor %} + +
ТипДатаСтатусИсполнитель
{{ m.m_type }}{{ m.planned_date }}{{ m.get_status_display }}{{ m.mechanic|default:"-" }} + Открыть +
Нет записей ТО
-
- - {{ unit.plate_number|default:"-" }} +
+ + +
+
+
Журнал поломок
+ + Новая поломка
-
- - {{ unit.year }} +
+ {% for b in breakdowns %} +
+
+
{{ b.system_node }}
+ {{ b.get_status_display }} +
+

{{ b.date|date:"d.m.Y" }} — {{ b.description }}

+
+ + Заказать запчасть + {% if b.photo %}Есть фото{% endif %} +
+
+ {% empty %} +
Поломок не зафиксировано
+ {% endfor %}
-
- - {{ unit.vin }} +
+ + +
+
+
Заявки на запчасти
+ + Создать заявку
-
- - {{ unit.commissioning_date|date:"d.m.Y" }} +
+ + + + + + + + + + + {% for r in part_requests %} + + + + + + + {% empty %} + + {% endfor %} + +
ДетальКол-воСтатусДата
{{ r.part_name }}{{ r.quantity }}{{ r.get_status_display }}{{ r.created_at|date:"d.m.Y" }}
Нет заявок
-
- - -
-
- -
-
-
-
-
- - - - - - - - - - - {% for m in maintenances %} - - - - - - - {% empty %} - - {% endfor %} - -
ТипДатаПробег/мчСтатус
{{ m.m_type }}{{ m.planned_date|date:"d.m.Y" }}{{ m.planned_runtime }}{{ m.get_status_display }}
История ТО пуста
-
-
-
-
Журнал поломок пуст
-
-
-
Заявок на запчасти нет
-
-
-
Документы не загружены
-
+ +
+
Примечания
+
+

{{ unit.notes|default:"Нет примечаний" }}

- -
- -
-
-
Действия
-
- - - -
-
-
- - -
-
-
QR-код техники
-
- -
- -
-
-

Отсканируйте для быстрого доступа с мобильного телефона

- -
-
- - -
-
-
Примечания
-

{{ unit.notes|default:"Примечания отсутствуют" }}

-
-
-
-{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/index.html b/core/templates/core/index.html index 5018798..bbef08f 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,154 +1,160 @@ {% extends 'base.html' %} - -{% block title %}Дашборд | Fleet Manager{% endblock %} +{% load static %} {% block content %} -
-
-
-
-

Управление парком техники

-

Централизованная система контроля, обслуживания и учета вашей техники.

-
- - Добавить технику - - - Весь список - +
+ +
+
+
+
+
+ +
+
+

{{ total_units }}

+ Всего техники +
+
+
+
+
+
+
+
+ +
+
+

{{ active_units }}

+ В работе +
+
+
+
+
+
+
+
+ +
+
+

{{ broken_units }}

+ Сломано +
+
+
+
+
+
+
+
+ +
+
+

{{ repair_units }}

+ В ремонте +
-
-
-
-
-
- -
-
-
Всего единиц
-
{{ total_units }}
-
-
-
-
-
-
- -
-
-
В работе
-
{{ active_units }}
-
-
-
-
-
-
- -
-
-
Сломано
-
{{ broken_units }}
-
-
-
-
-
-
- -
-
-
В ремонте
-
{{ repair_units }}
-
-
-
-
- -
-
-
-
-
Предстоящие ТО
- Все ТО -
-
+
+ +
+
+
+
Ближайшие ТО
+ Все ТО +
- + - + {% for m in recent_maintenances %} - + - - + + {% empty %} - - - + {% endfor %}
ТехникаТехника Тип ДатаСтатусСтатус
{{ m.fleet_unit.name }}{{ m.fleet_unit.name }} {{ m.m_type }}{{ m.planned_date|date:"d.m.Y" }} - - {{ m.get_status_display }} - - {{ m.planned_date }}{{ m.get_status_display }}
Нет запланированных ТО
Нет запланированных ТО
-
-
-
-
-
-
Последние поломки
- Журнал + +
+
+
Последние поломки
+ Весь журнал +
+
+ {% for b in recent_breakdowns %} +
+
+
{{ b.system_node }}
+
{{ b.fleet_unit.name }} — {{ b.date|date:"d.m.Y" }}
+
+ {{ b.get_status_display }} +
+ {% empty %} +
Поломок не зафиксировано
+ {% endfor %} +
-
-
- - - - - - - - - - - {% for b in recent_breakdowns %} - - - - - - - {% empty %} - - - - {% endfor %} - -
ТехникаУзелДатаСтатус
{{ b.fleet_unit.name }}{{ b.system_node }}{{ b.date|date:"d.m.Y" }} - - {{ b.get_status_display }} - -
Поломок не зафиксировано
+
+ + +
+
+
+
Быстрый поиск
+
+
+ + +
+
+
+
+ + + +
+
+
Мобильное приложение
+

Приложение оптимизировано для работы механиков с телефона. Используйте QR-коды для быстрого доступа к чек-листам.

+
-{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/maintenance_detail.html b/core/templates/core/maintenance_detail.html new file mode 100644 index 0000000..b042939 --- /dev/null +++ b/core/templates/core/maintenance_detail.html @@ -0,0 +1,133 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+
+
+
+
+ + {{ maintenance.m_type }} - {{ maintenance.fleet_unit.name }} +
+ + {% if maintenance.status == 'planned' %}Планируется + {% elif maintenance.status == 'in_progress' %}В процессе + {% else %}Выполнено{% endif %} + +
+
+
+
+

Техника

+

{{ maintenance.fleet_unit }}

+ +

Плановая дата

+

{{ maintenance.planned_date }}

+
+
+

Исполнитель

+

{{ maintenance.mechanic|default:"Не назначен" }}

+ +

Плановый пробег/моточасы

+

{{ maintenance.planned_runtime }}

+
+
+ +
+ +
Чек-лист операций
+
+ {% for item in maintenance.checklist %} +
+
+ {% csrf_token %} + +
+ + +
+
+
+ {% empty %} +
Чек-лист пуст
+ {% endfor %} +
+ + {% if maintenance.status != 'completed' %} +
+
Завершение ТО
+
+ {% csrf_token %} +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ {% else %} +
+
+ +
ТО успешно завершено {{ maintenance.actual_date }}
+
+
+
+

Фактический пробег/моточасы

+

{{ maintenance.actual_runtime }}

+
+
+

Запчасти

+

{{ maintenance.parts_used|default:"-" }}

+
+
+ + Скачать Акт ТО (PDF) + +
+ {% endif %} +
+
+
+ +
+
+
Примечания
+
+

{{ maintenance.notes|default:"Нет примечаний" }}

+
+
+ + {% if maintenance.fleet_unit.photo %} +
+ {{ maintenance.fleet_unit.name }} +
+

{{ maintenance.fleet_unit.name }}

+
+
+ {% endif %} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/core/maintenance_form.html b/core/templates/core/maintenance_form.html new file mode 100644 index 0000000..01c54b5 --- /dev/null +++ b/core/templates/core/maintenance_form.html @@ -0,0 +1,46 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+
+
+
+ {% if form.instance.pk %}Редактировать ТО{% else %}Запланировать ТО{% endif %} +
+
+
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endfor %} +
+ + Отмена +
+
+
+
+
+
+
+ + +{% endblock %} diff --git a/core/templates/core/maintenance_list.html b/core/templates/core/maintenance_list.html new file mode 100644 index 0000000..39d2c81 --- /dev/null +++ b/core/templates/core/maintenance_list.html @@ -0,0 +1,57 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+

Техническое обслуживание

+ + Запланировать ТО + +
+ +
+
+ + + + + + + + + + + + + {% for m in maintenances %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
ТехникаТипПлановая датаСтатусИсполнительДействия
+ +
{{ m.fleet_unit.plate_number|default:m.fleet_unit.vin }}
+
{{ m.get_m_type_display }}{{ m.planned_date }} + + {% if m.status == 'planned' %}Планируется + {% elif m.status == 'in_progress' %}В процессе + {% else %}Выполнено{% endif %} + + {{ m.mechanic|default:"-" }} + Открыть +
Нет запланированных ТО
+
+
+
+{% endblock %} diff --git a/core/templates/core/part_request_form.html b/core/templates/core/part_request_form.html new file mode 100644 index 0000000..b7f7b99 --- /dev/null +++ b/core/templates/core/part_request_form.html @@ -0,0 +1,42 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+
+
+
+ Создать заявку на запчасть +
+
+
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endfor %} +
+ + Отмена +
+
+
+
+
+
+
+ + +{% endblock %} diff --git a/core/templates/core/part_request_list.html b/core/templates/core/part_request_list.html new file mode 100644 index 0000000..4dfb5ba --- /dev/null +++ b/core/templates/core/part_request_list.html @@ -0,0 +1,69 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+

Заявки на запчасти

+ + Новая заявка + +
+ +
+
+ + + + + + + + + + + + + + {% for r in requests %} + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
ТехникаДетальАртикулКол-воСтатусДата созданияДействия
+
{{ r.fleet_unit.name }}
+
{{ r.fleet_unit.plate_number|default:r.fleet_unit.vin }}
+
{{ r.part_name }}{{ r.article_number|default:"-" }}{{ r.quantity }} + {% if r.status == 'draft' %}Черновик + {% elif r.status == 'sent' %}Отправлено + {% elif r.status == 'ordered' %}Заказано + {% elif r.status == 'delivered' %}Доставлено + {% endif %} + {{ r.created_at|date:"d.m.Y H:i" }} + +
Нет активных заявок
+
+
+
+{% endblock %} diff --git a/core/urls.py b/core/urls.py index 8a75e00..50809ad 100644 --- a/core/urls.py +++ b/core/urls.py @@ -3,8 +3,27 @@ from . import views urlpatterns = [ path('', views.IndexView.as_view(), name='index'), + + # Fleet path('fleet/', views.FleetListView.as_view(), name='fleet_list'), path('fleet//', views.FleetDetailView.as_view(), name='fleet_detail'), path('fleet/add/', views.FleetCreateView.as_view(), name='fleet_add'), path('fleet//edit/', views.FleetUpdateView.as_view(), name='fleet_edit'), + + # Maintenance + path('maintenance/', views.MaintenanceListView.as_view(), name='maintenance_list'), + path('maintenance//', views.MaintenanceDetailView.as_view(), name='maintenance_detail'), + path('maintenance/add/', views.MaintenanceCreateView.as_view(), name='maintenance_add'), + path('maintenance//edit/', views.MaintenanceUpdateView.as_view(), name='maintenance_edit'), + path('maintenance//process/', views.MaintenanceProcessView.as_view(), name='maintenance_process'), + path('maintenance//complete/', views.MaintenanceCompleteView.as_view(), name='maintenance_complete'), + path('maintenance//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'), ] \ No newline at end of file diff --git a/core/views.py b/core/views.py index c649a25..6706a39 100644 --- a/core/views.py +++ b/core/views.py @@ -1,10 +1,19 @@ -from django.shortcuts import render -from django.views.generic import ListView, DetailView, CreateView, UpdateView, TemplateView -from django.urls import reverse_lazy +import json +from io import BytesIO +from django.shortcuts import render, redirect, get_object_or_404 +from django.views.generic import ListView, DetailView, CreateView, UpdateView, TemplateView, View +from django.urls import reverse_lazy, reverse from django import forms -from django.db.models import Count -from .models import FleetUnit, Maintenance, Breakdown, PartRequest, Category +from django.db.models import Count, Q +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpResponse +from reportlab.pdfgen import canvas +from reportlab.lib.pagesizes import A4 +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +from .models import FleetUnit, Maintenance, Breakdown, PartRequest, Category, Document +# Forms class FleetUnitForm(forms.ModelForm): class Meta: model = FleetUnit @@ -14,6 +23,33 @@ class FleetUnitForm(forms.ModelForm): 'notes': forms.Textarea(attrs={'rows': 3}), } +class MaintenanceForm(forms.ModelForm): + class Meta: + model = Maintenance + fields = ['fleet_unit', 'm_type', 'planned_date', 'planned_runtime', 'mechanic', 'notes'] + widgets = { + 'planned_date': forms.DateInput(attrs={'type': 'date'}), + 'notes': forms.Textarea(attrs={'rows': 3}), + } + +class BreakdownForm(forms.ModelForm): + class Meta: + model = Breakdown + fields = ['fleet_unit', 'system_node', 'description', 'photo', 'status', 'notes'] + widgets = { + 'description': forms.Textarea(attrs={'rows': 3}), + 'notes': forms.Textarea(attrs={'rows': 3}), + } + +class PartRequestForm(forms.ModelForm): + class Meta: + model = PartRequest + fields = ['fleet_unit', 'breakdown', 'part_name', 'article_number', 'quantity', 'photo', 'notes'] + widgets = { + 'notes': forms.Textarea(attrs={'rows': 3}), + } + +# Views class IndexView(TemplateView): template_name = 'core/index.html' @@ -24,14 +60,13 @@ class IndexView(TemplateView): context['broken_units'] = FleetUnit.objects.filter(status='broken').count() context['repair_units'] = FleetUnit.objects.filter(status='repair').count() - # Stats for charts or badges context['status_counts'] = FleetUnit.objects.values('status').annotate(total=Count('status')) - context['recent_maintenances'] = Maintenance.objects.all().order_by('-planned_date')[:5] context['recent_breakdowns'] = Breakdown.objects.all().order_by('-date')[:5] return context +# Fleet Views class FleetListView(ListView): model = FleetUnit template_name = 'core/fleet_list.html' @@ -45,7 +80,11 @@ class FleetListView(ListView): queryset = queryset.filter(status=status) search = self.request.GET.get('search') if search: - queryset = queryset.filter(name__icontains=search) | queryset.filter(plate_number__icontains=search) | queryset.filter(vin__icontains=search) + queryset = queryset.filter( + Q(name__icontains=search) | + Q(plate_number__icontains=search) | + Q(vin__icontains=search) + ) return queryset class FleetDetailView(DetailView): @@ -55,9 +94,9 @@ class FleetDetailView(DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['maintenances'] = self.object.maintenances.all() - context['breakdowns'] = self.object.breakdowns.all() - context['part_requests'] = self.object.part_requests.all() + context['maintenances'] = self.object.maintenances.all().order_by('-planned_date') + context['breakdowns'] = self.object.breakdowns.all().order_by('-date') + context['part_requests'] = self.object.part_requests.all().order_by('-created_at') return context class FleetCreateView(CreateView): @@ -72,4 +111,164 @@ class FleetUpdateView(UpdateView): form_class = FleetUnitForm def get_success_url(self): - return reverse_lazy('fleet_detail', kwargs={'pk': self.object.pk}) \ No newline at end of file + 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}) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e22994c..ff3e246 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file