Files
ProdMan/bom_manager/models.py
2026-02-14 23:20:44 +03:00

303 lines
13 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.urls import reverse
from mptt.models import MPTTModel, TreeForeignKey
from polymorphic.models import PolymorphicModel
from django.core.exceptions import ValidationError
# Create your models here.
# Базовая модель операции
class BaseOperation(PolymorphicModel):
item = models.ForeignKey('Item', on_delete=models.CASCADE, related_name='operations', verbose_name="Компонент")
order = models.PositiveIntegerField("Номер операции", default=10, help_text="Например: 10, 20, 30...")
work_center = models.ForeignKey('WorkCenter', on_delete=models.CASCADE, related_name='operations', verbose_name="Станок/Участок")
class Meta:
ordering = ['order']
verbose_name = "Технологическая операция"
verbose_name_plural = "Технологический маршрут"
def __str__(self):
return f"{self.order}. {self._meta.verbose_name}"
def get_absolute_url(self):
return reverse("operation_detail", kwargs={"pk": self.pk})
def clean(self):
if self.order < 1:
raise ValidationError("Номер операции должен быть больше 0!")
if self.work_center is None:
raise ValidationError("Станок/Участок не может быть пустым!")
# Операция лазерной резки листа
class LaserCutSheet(BaseOperation):
thickness = models.IntegerField("Толщина листа, мм", default=3)
cut_length = models.IntegerField("Длина реза, мм", default=0)
pierces = models.IntegerField("Количество проколов", default=1)
dxf_file = models.FileField("DXF файл", upload_to='dxf_files/%Y/%m', null=True, blank=True)
#todo: добавить использование азота
def clean(self):
if self.cut_length < 1:
raise ValidationError("Длина реза должна быть больше 0!")
if self.pierces < 1:
raise ValidationError("Количество проколов должно быть больше 0!")
if self.item.entity_type != EntityType.PART:
raise ValidationError("Компонент должен быть деталью!")
def get_absolute_url(self):
return reverse("lrl_detail", kwargs={"pk": self.pk})
class Meta:
verbose_name = "Лазерная резка листа"
verbose_name_plural = "ЛРЛ"
def __str__(self):
return f"{self.order}. {self._meta.verbose_name}"
# Операция лазерной резки трубы
class LaserCutTube(BaseOperation):
thinckness = models.IntegerField("Толщина трубы, мм", default=3)
cut_length = models.IntegerField("Длина реза, мм", default=0)
pierces = models.IntegerField("Количество проколов", default=1)
iges_file = models.FileField("IGES файл", upload_to='iges_files/%Y/%m', null=True, blank=True)
def clean(self):
if self.cut_length < 1:
raise ValidationError("Длина реза должна быть больше 0!")
if self.pierces < 1:
raise ValidationError("Количество проколов должно быть больше 0!")
if self.item.entity_type != EntityType.PART:
raise ValidationError("Компонент должен быть деталью!")
def get_absolute_url(self):
return reverse("lrt_detail", kwargs={"pk": self.pk})
class Meta:
verbose_name = "Лазерная резка трубы"
verbose_name_plural = "ЛРТ"
def __str__(self):
return f"{self.order}. {self._meta.verbose_name}"
# Операция токарной обработки
class Turning(BaseOperation):
work_time = models.IntegerField("Время работы, мин", default=0)
def clean(self):
if self.work_time < 1:
raise ValidationError("Время работы должно быть больше 0!")
def get_absolute_url(self):
return reverse("turning_detail", kwargs={"pk": self.pk})
class Meta:
verbose_name = "Токарная обработка"
verbose_name_plural = "ТО"
def __str__(self):
return f"{self.order}. {self._meta.verbose_name}"
# Операция сварки
class Weld(BaseOperation):
total_weld_length = models.IntegerField("Общая длина сварки, мм", default=0)
avg_leg = models.IntegerField("Средний катет, мм", default=0)
# todo: добавить реализацию швов с разными катетами
def clean(self):
if self.total_weld_length < 1:
raise ValidationError("Общая длина сварки должна быть больше 0!")
if self.avg_leg < 1:
raise ValidationError("Средний катет должен быть больше 0!")
if self.item.entity_type not in [EntityType.ASSEMBLY, EntityType.COMPLEX, EntityType.UNIT]:
raise ValidationError("Компонент должен быть составным изделием!")
def get_absolute_url(self):
return reverse("weld_detail", kwargs={"pk": self.pk})
class Meta:
verbose_name = "Сварка"
verbose_name_plural = "Сварки"
def __str__(self):
return f"{self.order}. {self._meta.verbose_name}"
# Операция покраски
class Paint(BaseOperation):
"""Покраска"""
# площадь покраски
area = models.DecimalField("Площадь покраски, м2", max_digits=10, decimal_places=2, default=0)
# цвет по RAL
color = models.CharField("Код RAL", max_length=100, blank=True, null=True)
# число слоев
number_of_layers = models.IntegerField("Число слоев", default=1)
# покрытие из другой таблицы
coating = models.ForeignKey('Coating', on_delete=models.CASCADE, related_name='paints', verbose_name="Покрытие")
class Meta:
verbose_name = "Покраска"
verbose_name_plural = "Покраски"
def __str__(self):
return f"{self.order}. {self._meta.verbose_name}"
class Coating(models.Model):
"""Покрытие"""
# наименование краски/грунта
name = models.CharField("Название покрытия", max_length=100)
# расход покрытия
consumption = models.DecimalField("Расход покрытия, м2/л", max_digits=10, decimal_places=2, default=0)
class Meta:
verbose_name = "Покрытие"
verbose_name_plural = "Покрытия"
def __str__(self):
return self.name
# todo: добавить операции гибки, зачистки, перемещения и лентопильного станка
class EntityType(models.TextChoices):
"""Тип изделия"""
UNIT = 'UNIT', 'Изделие'
ASSEMBLY = 'ASSY', 'Сборка'
PART = 'PART', 'Деталь'
STANDARD = 'STD', 'Стандартное изделие'
COMPLEX = 'CPLX', 'Комплекс'
# class OperationType(models.TextChoices):
# """Тип операции"""
# LRL = 'LRL', 'Лазерная резка листа (ЛРЛ)' # todo: add LRL
# LRT = 'LRT', 'Лазерная резка трубы (ЛРТ)' # todo: add LRT
# TO = 'TO', 'Токарная обработка (ТО)'
# WELD = 'WELD', 'Сварка'
# PAINT = 'PAINT', 'Покраска'
# BEND = 'BEND', 'Гибка'
# CLEAN = 'CLEAN', 'Зачистка'
# BANDSAW = 'BANDSAW', 'Лентопильный станок'
# MOVE = 'MOVE', 'Перемещение'
class WorkCenter(models.Model):
"""Справочник станков или участков"""
name = models.CharField("Название станка/участка", max_length=100)
rate_per_hour = models.DecimalField("Стоимость часа", max_digits=10, decimal_places=2, default=0)
class Meta:
verbose_name = "Станок/участок"
verbose_name_plural = "Станки/участки"
def __str__(self):
return self.name
class Item(models.Model):
# Децимальный номер
designation = models.CharField(max_length=30, verbose_name="Децимальный номер", db_index=True, blank=True, null=True)
# Обозначение
title = models.CharField(max_length=100, verbose_name="Обозначение", blank=False, null=False)
# Тип изделия
entity_type = models.CharField(max_length=4, choices=EntityType.choices, default=EntityType.PART)
# Флаг сборки
is_assembly = models.BooleanField("Сборка", default=False)
# Технические данные
drawing = models.FileField("Чертеж", upload_to='drawings/', blank=True, null=True)
class Meta:
verbose_name = "Компонент"
verbose_name_plural = "Компоненты"
def __str__(self):
if self.designation:
return f"{self.designation} {self.title}"
return self.title # Если номера нет, выводим только название без None
def get_absolute_url(self):
return reverse("item_detail", kwargs={"pk": self.pk})
class BOMNode(MPTTModel):
# Связь с компонентом
item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='bom_nodes')
# Связь с родительским компонентом
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children')
# Количество
quantity = models.IntegerField(default=1, verbose_name="Количество")
# Разрешаем добавление компонентов только в сборки и изделия
def clean(self):
if self.parent and self.parent.item.entity_type not in [EntityType.UNIT, EntityType.ASSEMBLY, EntityType.COMPLEX]:
raise ValidationError(
f"Нельзя добавлять компоненты в '{self.parent.item.title}', так как это не сборка или изделие!"
)
class MPTTMeta:
order_insertion_by = [] # todo: add order_insertion_by
verbose_name = "Дерево материала"
verbose_name_plural = "Деревья материала"
class Meta:
verbose_name = "Изделие"
verbose_name_plural = "Изделия"
def __str__(self):
res = f"{self.item.designation or ''} {self.item.title}"
if self.parent:
res += f"{self.parent.item.title})"
else:
res += " (Корень изделия)"
return res
def get_absolute_url(self):
return reverse("bom_node_detail", kwargs={"pk": self.pk})
# class RoutingStep(models.Model):
# """Технологическая операция"""
# # Связь с компонентом
# item = models.ForeignKey('Item', on_delete=models.CASCADE, related_name='routing_steps', verbose_name="Деталь/Сборка")
# # Тип операции
# operation_type = models.CharField("Тип операции", max_length=10, choices=OperationType.choices)
# # Связь с станком
# work_center = models.ForeignKey(WorkCenter, on_delete=models.SET_NULL, null=True, verbose_name="Станок/Участок")
# # Номер операции
# order = models.PositiveIntegerField("Номер операции", default=10, help_text="Например: 10, 20, 30...")
# # Файлы
# drawing_file = models.FileField("Тех. файл (DXF/IGES)", upload_to='tech_files/%Y/%m', null=True, blank=True)
# # Гибкие данные (JSON)
# # Сюда будем писать: {"cut_length": 1500, "pierces": 20} или {"welds": [{"leg": 5, "length": 100}]}
# tech_params = models.JSONField("Технологические параметры", default=dict, blank=True)
# # Общие поля для всех операций
# setup_time = models.DurationField("Время наладки", null=True, blank=True)
# cycle_time = models.DurationField("Время цикла (на 1 шт)", null=True, blank=True)
# def clean(self):
# if self.operation_type == OperationType.LRL:
# if 'cut_length' not in self.tech_params:
# raise ValidationError("Для ЛРЛ обязательно укажите 'cut_length' в параметрах!")
# if 'pierces' not in self.tech_params:
# raise ValidationError("Для ЛРЛ обязательно укажите 'pierces' в параметрах!")
# if self.operation_type == OperationType.LRT:
# if 'cut_length' not in self.tech_params:
# raise ValidationError("Для ЛРТ обязательно укажите 'cut_length' в параметрах!")
# class Meta:
# ordering = ['order']
# verbose_name = "Технологическая операция"
# verbose_name_plural = "Технологический маршрут"
# def __str__(self):
# return f"{self.order}. {self.get_operation_type_display()}"