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)