from django.db import models from django.utils import timezone from django.contrib.auth.models import User from warehouse.models import Material as WarehouseMaterial class Company(models.Model): """ Справочник контрагентов/заказчиков. Позволяет группировать сделки по компаниям и избегать дублей в названиях. """ name = models.CharField("Название компании", max_length=255, unique=True) description = models.TextField("Краткое описание / Примечание", blank=True) def __str__(self): return self.name class Meta: verbose_name = "Компания"; verbose_name_plural = "Компании" class Machine(models.Model): """ Список производственных участков (станков). Используется для фильтрации сменных заданий для конкретных операторов. """ MACHINE_TYPE_CHOICES = [ ('linear', 'Линейный'), ('sheet', 'Листовой'), ] name = models.CharField("Название станка", max_length=100) machine_type = models.CharField("Тип станка", max_length=10, choices=MACHINE_TYPE_CHOICES, default='linear') def __str__(self): return self.name class Meta: verbose_name = "Станок"; verbose_name_plural = "Станки" class Deal(models.Model): """ Заказ или проект. Номер парсится из пути к файлам. Служит контейнером для группы деталей (позиций). """ STATUS_CHOICES = [ ('lead', 'Зашла'), ('work', 'В работе'), ('done', 'Завершена'), ] number = models.CharField("№ Сделки", max_length=100, unique=True) status = models.CharField("Статус", max_length=10, choices=STATUS_CHOICES, default='work') company = models.ForeignKey(Company, on_delete=models.PROTECT, verbose_name="Заказчик", null=True, blank=True) description = models.TextField("Описание сделки", blank=True, help_text="Общая информация по заказу") def __str__(self): return f"Сделка №{self.number} ({self.company})" class Meta: verbose_name = "Сделка"; verbose_name_plural = "Сделки" class ProductionTask(models.Model): """ Основание для производства. Определяет ЧТО делать. Создается технологом или мастером на основе заказа. """ deal = models.ForeignKey(Deal, on_delete=models.CASCADE, verbose_name="Сделка") drawing_name = models.CharField("Название детали", max_length=255, blank=True, default="Б/ч") size_value = models.FloatField("Размер детали", help_text="Длина (мм) или Толщина (мм)") drawing_file = models.FileField("Исходник (DXF/IGES)", upload_to="drawings/%Y/%m/", blank=True, null=True) extra_drawing = models.FileField("Доп. чертеж (PDF)", upload_to="extra_drawings/%Y/%m/", blank=True, null=True) preview_image = models.ImageField("Превью DXF (PNG)", upload_to="task_previews/%Y/%m/", blank=True, null=True) blank_dimensions = models.CharField("Габариты заготовки", max_length=64, blank=True, default="") material = models.ForeignKey(WarehouseMaterial, on_delete=models.PROTECT, verbose_name="Материал") quantity_ordered = models.PositiveIntegerField("Заказано всего, шт") is_bend = models.BooleanField("Гибка", default=False) created_at = models.DateTimeField("Дата создания", auto_now_add=True) class Meta: verbose_name = "Задание на деталь"; verbose_name_plural = "Задания на детали" ordering = ['-created_at'] def __str__(self): return f"{self.drawing_name} (Заказ {self.deal.number})" class DxfPreviewSettings(models.Model): """Настройки генерации превью для DXF. Храним в БД, чтобы админ мог менять параметры через страницу «Обслуживание сервера» без правок кода. Сделано как singleton: ожидается одна строка (обычно pk=1). """ line_color = models.CharField( "Цвет линий превью (HEX)", max_length=16, default="#006400", help_text="Напр: #006400 (тёмно-зелёный)", ) lineweight_scaling = models.FloatField( "Коэффициент толщины линий", default=1.0, help_text="1.0 = как в DXF, 2.0 = толще, 0.5 = тоньше", ) min_lineweight = models.FloatField( "Минимальная толщина (мм)", default=0.1, help_text="Если в DXF нет lineweight — используем минимум, чтобы линии были видимы", ) keep_original_colors = models.BooleanField( "Оставить цвета оригинальные", default=False, help_text="Если включено — не перекрашиваем линии, берём цвета из DXF", ) per_task_timeout_sec = models.PositiveIntegerField( "Таймаут на 1 DXF (сек)", default=45, help_text="Если конкретный DXF завис — убиваем обработку этой детали и идём дальше", ) updated_at = models.DateTimeField("Обновлено", auto_now=True) class Meta: verbose_name = "Настройки превью DXF" verbose_name_plural = "Настройки превью DXF" def __str__(self): return "Настройки превью DXF" class DxfPreviewJob(models.Model): """Фоновая задача пакетной регенерации превью DXF. Зачем нужна: - генерация превью и bbox может быть тяжёлой и в синхронном POST «вешает» ответ; - поэтому мы запускаем задачу в фоне и пишем прогресс в БД; - UI может показывать статус/счётчики без ожидания завершения. Важно: - это не Celery и не очередь, а простой «фон» для текущего процесса Django; - для продакшена лучше вынести в полноценный воркер, но этот вариант уже убирает зависания UI. """ STATUS_CHOICES = [ ('queued', 'В очереди'), ('running', 'Выполняется'), ('done', 'Готово'), ('failed', 'Ошибка'), ('cancelled', 'Остановлено'), ] status = models.CharField("Статус", max_length=16, choices=STATUS_CHOICES, default='queued') created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Запустил") cancel_requested = models.BooleanField( "Запрошена остановка", default=False, help_text="Если включено — воркер завершит задачу после текущей детали", ) pid = models.PositiveIntegerField( "PID процесса", null=True, blank=True, help_text="Номер процесса, который выполняет задачу (для диагностики)", ) started_at = models.DateTimeField("Начато", null=True, blank=True) finished_at = models.DateTimeField("Завершено", null=True, blank=True) total = models.PositiveIntegerField("Всего задач", default=0) processed = models.PositiveIntegerField("Обработано", default=0) updated = models.PositiveIntegerField("Обновлено", default=0) skipped = models.PositiveIntegerField("Пропущено", default=0) errors = models.PositiveIntegerField("Ошибок", default=0) last_message = models.CharField("Сообщение", max_length=255, blank=True, default='') created_at = models.DateTimeField("Создано", auto_now_add=True) class Meta: verbose_name = "Задача превью DXF" verbose_name_plural = "Задачи превью DXF" ordering = ['-id'] def __str__(self): return f"DXF превью: {self.get_status_display()}" class Item(models.Model): """ Единица сменного задания. Определяет КТО, КОГДА и СКОЛЬКО сделал. """ STATUS_CHOICES = [ ('work', 'В работе'), ('done', 'Выполнено'), ('partial', 'Частично'), ('leftover', 'Недодел'), ('imported', 'Импортировано'), ] # --- Ссылка на основу (временно null=True для миграции старых данных) --- task = models.ForeignKey(ProductionTask, on_delete=models.CASCADE, related_name='items', verbose_name="Задание", null=True, blank=True) # --- Смена (заполняет мастер) --- date = models.DateField("Дата смены", default=timezone.localdate) machine = models.ForeignKey(Machine, on_delete=models.PROTECT, verbose_name="Станок") quantity_plan = models.PositiveIntegerField("План на смену, шт") # --- Исполнение (заполняет оператор) --- quantity_fact = models.PositiveIntegerField("Факт, шт", default=0) material_taken = models.TextField("Взятый материал", blank=True, help_text="Напр: 3 трубы по 12м") usable_waste = models.TextField("Деловой отход", blank=True, help_text="Напр: кусок 1500мм") scrap_weight = models.FloatField("Лом (кг)", default=0.0) # --- Статусы и учет --- status = models.CharField("Статус", max_length=10, choices=STATUS_CHOICES, default='work') is_synced_1c = models.BooleanField("Учтено в 1С", default=False) class Meta: verbose_name = "Пункт сменки"; verbose_name_plural = "Реестр сменных заданий" ordering = ['-date', 'task__deal'] def __str__(self): if self.task: return f"{self.task.drawing_name} - {self.date}" return f"Без задания - {self.date}" class EmployeeProfile(models.Model): ROLE_CHOICES = [ ('admin', 'Администратор'), ('technologist', 'Технолог'), ('master', 'Мастер'), ('operator', 'Оператор'), ('clerk', 'Учетчик'), ] # Связь 1 к 1 со стандартным юзером Django user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile', verbose_name='Пользователь') role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='operator', verbose_name='Должность') # Привязка станков (можно выбрать несколько для одного оператора) machines = models.ManyToManyField('Machine', blank=True, verbose_name='Закрепленные станки') def __str__(self): return f"{self.user.username} - {self.get_role_display()}" class Meta: verbose_name = 'Профиль сотрудника' verbose_name_plural = 'Профили сотрудников'