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="Наименование") class Meta: verbose_name = "Категория" verbose_name_plural = "Категории" def __str__(self): return self.name class Supplier(models.Model): name = models.CharField(max_length=255, verbose_name="Наименование компании") representative_name = models.CharField(max_length=255, blank=True, null=True, verbose_name="ФИО представителя/менеджера") phone = models.CharField(max_length=50, blank=True, null=True, verbose_name="Телефон") email = models.EmailField(blank=True, null=True, verbose_name="Электронная почта") contract_number = models.CharField(max_length=100, blank=True, null=True, verbose_name="Номер договора") class Meta: verbose_name = "Поставщик / Контрагент" verbose_name_plural = "Поставщики / Контрагенты" def __str__(self): return self.name class FleetUnit(models.Model): STATUS_CHOICES = [ ('active', 'В работе'), ('idle', 'Простаивает'), ('broken', 'Сломана'), ('repair', 'В ремонте'), ('waiting_parts', 'Ждёт деталь'), ] name = models.CharField(max_length=255, verbose_name="Наименование") category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Категория") model_name = models.CharField(max_length=255, verbose_name="Модель") vin = models.CharField(max_length=100, unique=True, verbose_name="VIN / Серийный номер") plate_number = models.CharField(max_length=50, blank=True, null=True, verbose_name="Госномер") year = models.PositiveIntegerField(verbose_name="Год выпуска") photo = models.ImageField(upload_to='fleet_photos/', blank=True, null=True, verbose_name="Фото") 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-код") # Insurance insurance_company = models.CharField(max_length=255, blank=True, null=True, verbose_name="Страховая компания") insurance_policy_number = models.CharField(max_length=100, blank=True, null=True, verbose_name="Номер страхового полиса") insurance_start_date = models.DateField(blank=True, null=True, verbose_name="Дата начала страховки") insurance_end_date = models.DateField(blank=True, null=True, verbose_name="Дата окончания страховки") # Supplier supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Поставщик / Контрагент") supplier_name = models.CharField(max_length=255, blank=True, null=True, verbose_name="Поставщик (старое)") supplier_contacts = models.TextField(blank=True, null=True, verbose_name="Контакты поставщика (старое)") vehicle_documents = models.FileField(upload_to="fleet_docs/", blank=True, null=True, verbose_name="Документы на авто") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = "Техника" verbose_name_plural = "Техника" ordering = ['-created_at'] def __str__(self): return f"{self.name} ({self.plate_number or self.vin})" def get_absolute_url(self): return reverse('fleet_detail', kwargs={'pk': self.pk}) def get_status_color(self): colors = { 'active': 'success', 'idle': 'secondary', 'broken': 'danger', 'repair': 'warning', 'waiting_parts': 'info', } 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): path = self.get_absolute_url() qr_data = path 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', 'Сезонное'), ('special', 'Спец'), ] STATUS_CHOICES = [ ('planned', 'Планируется'), ('in_progress', 'В процессе'), ('completed', 'Выполнено'), ] fleet_unit = models.ForeignKey(FleetUnit, on_delete=models.CASCADE, related_name='maintenances', verbose_name="Техника") 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="Примечания") class Meta: verbose_name = "ТО" verbose_name_plural = "ТО" def __str__(self): return f"{self.m_type} - {self.fleet_unit.name} ({self.planned_date})" class Breakdown(models.Model): STATUS_CHOICES = [ ('reported', 'Заявлено'), ('repaired', 'Отремонтировано'), ('need_part', 'Нужна деталь'), ] fleet_unit = models.ForeignKey(FleetUnit, on_delete=models.CASCADE, related_name='breakdowns', verbose_name="Техника") 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: verbose_name = "Поломка" verbose_name_plural = "Поломки" def __str__(self): return f"Поломка: {self.fleet_unit.name} - {self.system_node}" class PartRequest(models.Model): STATUS_CHOICES = [ ('draft', 'Черновик'), ('sent', 'Отправлено'), ('ordered', 'Заказано'), ('delivered', 'Доставлено'), ] 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="Количество") status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft', verbose_name="Статус") photo = models.ImageField(upload_to='part_photos/', blank=True, null=True, verbose_name="Фото") notes = models.TextField(blank=True, null=True, verbose_name="Примечание") created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = "Заявка на запчасть" verbose_name_plural = "Заявки на запчасти" def __str__(self): return f"{self.part_name} for {self.fleet_unit.name}" class Document(models.Model): TYPE_CHOICES = [ ('passport', 'Паспорт'), ('maintenance_act', 'Акт ТО'), ('photo', 'Фото'), ('invoice', 'Счет'), ] doc_type = models.CharField(max_length=50, choices=TYPE_CHOICES, verbose_name="Тип документа") fleet_unit = models.ForeignKey(FleetUnit, on_delete=models.CASCADE, related_name='documents', null=True, blank=True) maintenance = models.ForeignKey(Maintenance, on_delete=models.CASCADE, related_name='documents', null=True, blank=True) breakdown = models.ForeignKey(Breakdown, on_delete=models.CASCADE, related_name='documents', null=True, blank=True) part_request = models.ForeignKey(PartRequest, on_delete=models.CASCADE, related_name='documents', null=True, blank=True) file = models.FileField(upload_to='documents/', verbose_name="Файл") uploaded_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата добавления") class Meta: verbose_name = "Документ" verbose_name_plural = "Документы" def __str__(self): return f"{self.get_doc_type_display()} - {self.uploaded_at}"