Files
MES_Core/shiftflow/models.py
2026-04-03 01:10:05 +03:00

255 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 = 'Профили сотрудников'