All checks were successful
Deploy MES Core / deploy (push) Successful in 13s
226 lines
8.8 KiB
Python
226 lines
8.8 KiB
Python
from django.db import models
|
||
|
||
|
||
class Operation(models.Model):
|
||
"""Операция техпроцесса.
|
||
|
||
Комментарий: справочник расширяется без изменений кода.
|
||
"""
|
||
|
||
name = models.CharField('Операция', max_length=200, unique=True)
|
||
code = models.SlugField(
|
||
'Код',
|
||
max_length=64,
|
||
unique=True,
|
||
help_text='Стабильный идентификатор (например welding, painting, laser_cutting).',
|
||
)
|
||
workshop = models.ForeignKey(
|
||
'shiftflow.Workshop',
|
||
on_delete=models.PROTECT,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name='Цех по умолчанию',
|
||
)
|
||
|
||
class Meta:
|
||
verbose_name = 'Операция'
|
||
verbose_name_plural = 'Операции'
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
|
||
class ProductEntity(models.Model):
|
||
"""Паспорт детали/сборки/изделия (КД).
|
||
|
||
planned_material:
|
||
- материал, заложенный в КД (для расчёта потребности и контроля замен при раскрое).
|
||
|
||
Нормы расхода (для BOM Explosion и MaterialRequirement):
|
||
- для листовой детали: blank_area_m2 (м² на 1 шт)
|
||
- для линейной (профиль/труба/круг): blank_length_mm (мм на 1 шт)
|
||
|
||
Примечание:
|
||
- категорию типа (лист/профиль) определяем по planned_material.category.
|
||
"""
|
||
|
||
ENTITY_TYPE = [
|
||
('product', 'Готовое изделие'),
|
||
('assembly', 'Сборочная единица'),
|
||
('part', 'Деталь'),
|
||
('purchased', 'Покупное'),
|
||
('casting', 'Литьё'),
|
||
('outsourced', 'Аутсорс'),
|
||
]
|
||
|
||
name = models.CharField("Наименование", max_length=255)
|
||
drawing_number = models.CharField("Обозначение/Чертёж", max_length=100, blank=True, default="")
|
||
entity_type = models.CharField("Тип", max_length=15, choices=ENTITY_TYPE, default='part')
|
||
|
||
planned_material = models.ForeignKey(
|
||
'warehouse.Material',
|
||
on_delete=models.PROTECT,
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Заложенный материал",
|
||
)
|
||
|
||
blank_area_m2 = models.FloatField("Норма: площадь заготовки (м²/шт)", null=True, blank=True)
|
||
blank_length_mm = models.FloatField("Норма: длина заготовки (мм/шт)", null=True, blank=True)
|
||
|
||
dxf_file = models.FileField("Исходник (DXF/IGES/STEP)", upload_to="drawings/%Y/%m/", blank=True, null=True)
|
||
pdf_main = models.FileField("Чертёж (PDF)", upload_to="drawings_pdf/%Y/%m/", blank=True, null=True)
|
||
preview = models.ImageField("Превью", upload_to="previews/%Y/%m/", blank=True, null=True)
|
||
|
||
passport_filled = models.BooleanField('Паспорт заполнен', default=False)
|
||
|
||
class Meta:
|
||
verbose_name = "КД (изделие/деталь)"
|
||
verbose_name_plural = "КД (изделия/детали)"
|
||
|
||
def __str__(self):
|
||
base = f"{self.drawing_number} {self.name}".strip()
|
||
return base if base else self.name
|
||
|
||
|
||
class EntityOperation(models.Model):
|
||
"""Операции техпроцесса для конкретной сущности (деталь/сборка/изделие)."""
|
||
|
||
entity = models.ForeignKey(ProductEntity, on_delete=models.CASCADE, related_name='operations', verbose_name='Сущность')
|
||
operation = models.ForeignKey(Operation, on_delete=models.PROTECT, verbose_name='Операция')
|
||
seq = models.PositiveSmallIntegerField('Порядок', default=1)
|
||
|
||
class Meta:
|
||
verbose_name = 'Операция сущности'
|
||
verbose_name_plural = 'Операции сущностей'
|
||
ordering = ('entity', 'seq', 'id')
|
||
unique_together = ('entity', 'seq')
|
||
|
||
def __str__(self):
|
||
return f"{self.entity}: {self.seq}. {self.operation}"
|
||
|
||
|
||
class BOM(models.Model):
|
||
"""Спецификация (BOM): parent состоит из child в количестве quantity."""
|
||
|
||
parent = models.ForeignKey(
|
||
ProductEntity,
|
||
related_name='components',
|
||
on_delete=models.CASCADE,
|
||
verbose_name="Куда входит (сборка)",
|
||
)
|
||
child = models.ForeignKey(
|
||
ProductEntity,
|
||
related_name='used_in',
|
||
on_delete=models.CASCADE,
|
||
verbose_name="Что входит (деталь)",
|
||
)
|
||
quantity = models.PositiveIntegerField("Кол-во в сборке", default=1)
|
||
|
||
class Meta:
|
||
unique_together = ('parent', 'child')
|
||
verbose_name = "Спецификация (BOM)"
|
||
verbose_name_plural = "Спецификации (BOM)"
|
||
|
||
def __str__(self):
|
||
return f"{self.parent} -> {self.child} x{self.quantity}"
|
||
|
||
|
||
class AssemblyPassport(models.Model):
|
||
entity = models.OneToOneField(ProductEntity, on_delete=models.CASCADE, related_name='assembly_passport')
|
||
|
||
requires_welding = models.BooleanField('Требуется сварка', default=False)
|
||
requires_painting = models.BooleanField('Требуется покраска', default=False)
|
||
|
||
weight_kg = models.FloatField('Масса, кг', null=True, blank=True)
|
||
coating = models.CharField('Покрытие', max_length=200, blank=True, default='')
|
||
coating_color = models.CharField('Цвет', max_length=100, blank=True, default='')
|
||
coating_area_m2 = models.FloatField('Площадь покрытия, м²', null=True, blank=True)
|
||
|
||
technical_requirements = models.TextField('Технические требования', blank=True, default='')
|
||
|
||
class Meta:
|
||
verbose_name = 'Паспорт сборки/изделия'
|
||
verbose_name_plural = 'Паспорта сборок/изделий'
|
||
|
||
def __str__(self):
|
||
return str(self.entity)
|
||
|
||
|
||
class WeldingSeam(models.Model):
|
||
passport = models.ForeignKey(AssemblyPassport, related_name='welding_seams', on_delete=models.CASCADE)
|
||
|
||
name = models.CharField('Наименование', max_length=255)
|
||
leg_mm = models.FloatField('Катет, мм')
|
||
length_mm = models.FloatField('Длина, мм')
|
||
quantity = models.PositiveIntegerField('Кол-во', default=1)
|
||
|
||
class Meta:
|
||
verbose_name = 'Сварной шов'
|
||
verbose_name_plural = 'Сварные швы'
|
||
|
||
def __str__(self):
|
||
return f"{self.name}"
|
||
|
||
|
||
class PartPassport(models.Model):
|
||
entity = models.OneToOneField(ProductEntity, on_delete=models.CASCADE, related_name='part_passport')
|
||
|
||
thickness_mm = models.FloatField('Толщина, мм', null=True, blank=True)
|
||
length_mm = models.FloatField('Длина, мм', null=True, blank=True)
|
||
mass_kg = models.FloatField('Масса, кг', null=True, blank=True)
|
||
|
||
cut_length_mm = models.FloatField('Длина реза, мм', null=True, blank=True)
|
||
pierce_count = models.PositiveIntegerField('Кол-во врезок', null=True, blank=True)
|
||
engraving = models.TextField('Гравировка', blank=True, default='')
|
||
|
||
technical_requirements = models.TextField('Технические требования', blank=True, default='')
|
||
|
||
class Meta:
|
||
verbose_name = 'Паспорт детали'
|
||
verbose_name_plural = 'Паспорта деталей'
|
||
|
||
def __str__(self):
|
||
return str(self.entity)
|
||
|
||
|
||
class PurchasedPassport(models.Model):
|
||
entity = models.OneToOneField(ProductEntity, on_delete=models.CASCADE, related_name='purchased_passport')
|
||
|
||
gost = models.CharField('ГОСТ/ТУ', max_length=255, blank=True, default='')
|
||
|
||
class Meta:
|
||
verbose_name = 'Паспорт покупного'
|
||
verbose_name_plural = 'Паспорта покупного'
|
||
|
||
def __str__(self):
|
||
return str(self.entity)
|
||
|
||
|
||
class CastingPassport(models.Model):
|
||
entity = models.OneToOneField(ProductEntity, on_delete=models.CASCADE, related_name='casting_passport')
|
||
|
||
casting_material = models.CharField('Материал литья', max_length=200, blank=True, default='')
|
||
mass_kg = models.FloatField('Масса, кг', null=True, blank=True)
|
||
|
||
class Meta:
|
||
verbose_name = 'Паспорт литья'
|
||
verbose_name_plural = 'Паспорта литья'
|
||
|
||
def __str__(self):
|
||
return str(self.entity)
|
||
|
||
|
||
class OutsourcedPassport(models.Model):
|
||
entity = models.OneToOneField(ProductEntity, on_delete=models.CASCADE, related_name='outsourced_passport')
|
||
|
||
technical_requirements = models.TextField('Технические требования', blank=True, default='')
|
||
notes = models.TextField('Пояснения', blank=True, default='')
|
||
|
||
class Meta:
|
||
verbose_name = 'Паспорт аутсорса'
|
||
verbose_name_plural = 'Паспорта аутсорса'
|
||
|
||
def __str__(self):
|
||
return str(self.entity)
|