Огромная замена логики
All checks were successful
Deploy MES Core / deploy (push) Successful in 11s

This commit is contained in:
2026-04-06 08:06:37 +03:00
parent 0e8497ab1f
commit e88b861f68
48 changed files with 3833 additions and 175 deletions

View File

@@ -15,10 +15,31 @@ class Company(models.Model):
class Meta:
verbose_name = "Компания"; verbose_name_plural = "Компании"
class Machine(models.Model):
class Workshop(models.Model):
"""Цех/участок верхнего уровня.
Логика доступа к складу:
- оператор и станок работают со складом цеха;
- перемещения между складами (центральный <-> цех <-> следующий цех) выполняются через «Перемещение».
"""
Список производственных участков (станков).
Используется для фильтрации сменных заданий для конкретных операторов.
name = models.CharField('Цех', max_length=120, unique=True)
location = models.ForeignKey('warehouse.Location', on_delete=models.PROTECT, null=True, blank=True, verbose_name='Склад цеха')
class Meta:
verbose_name = 'Цех'
verbose_name_plural = 'Цеха'
def __str__(self):
return self.name
class Machine(models.Model):
"""Список производственных участков (станков).
Источник склада для операций выработки/списаний:
- предпочитаем склад цеха (Machine.workshop.location)
- поле Machine.location оставлено для совместимости (если цех не задан)
"""
MACHINE_TYPE_CHOICES = [
@@ -28,6 +49,8 @@ class Machine(models.Model):
name = models.CharField("Название станка", max_length=100)
machine_type = models.CharField("Тип станка", max_length=10, choices=MACHINE_TYPE_CHOICES, default='linear')
workshop = models.ForeignKey('shiftflow.Workshop', on_delete=models.PROTECT, null=True, blank=True, verbose_name='Цех')
location = models.ForeignKey('warehouse.Location', on_delete=models.PROTECT, null=True, blank=True, verbose_name="Склад участка (устаревает)")
def __str__(self):
return self.name
@@ -59,11 +82,15 @@ class Deal(models.Model):
verbose_name = "Сделка"; verbose_name_plural = "Сделки"
class ProductionTask(models.Model):
"""План производства детали по сделке.
Переходный этап:
- сейчас в задаче ещё есть legacy-поля (drawing_name, файлы, material), чтобы не сломать UI;
- целевая модель: task.entity -> manufacturing.ProductEntity, а файлы/превью живут на entity.
"""
Основание для производства. Определяет ЧТО делать.
Создается технологом или мастером на основе заказа.
"""
deal = models.ForeignKey(Deal, on_delete=models.CASCADE, verbose_name="Сделка")
entity = models.ForeignKey('manufacturing.ProductEntity', on_delete=models.PROTECT, null=True, blank=True, verbose_name="КД (изделие/деталь)")
drawing_name = models.CharField("Название детали", max_length=255, blank=True, default="Б/ч")
size_value = models.FloatField("Размер детали", help_text="Длина (мм) или Толщина (мм)")
@@ -133,6 +160,171 @@ class DxfPreviewSettings(models.Model):
return "Настройки превью DXF"
class DealItem(models.Model):
"""Состав сделки: что заказал клиент (точка входа для BOM Explosion)."""
deal = models.ForeignKey(Deal, related_name='items', on_delete=models.CASCADE, verbose_name='Сделка')
entity = models.ForeignKey('manufacturing.ProductEntity', on_delete=models.PROTECT, verbose_name='Изделие/деталь')
quantity = models.PositiveIntegerField('Заказано, шт')
class Meta:
verbose_name = 'Позиция сделки'
verbose_name_plural = 'Позиции сделки'
unique_together = ('deal', 'entity')
def __str__(self):
return f"{self.deal.number}: {self.entity} x{self.quantity}"
class MaterialRequirement(models.Model):
"""Потребность в закупке сырья для сделки.
required_qty хранит величину в unit:
- для листа: m2
- для профиля/трубы: mm
Статус отражает этап обеспечения.
"""
STATUS_CHOICES = [
('needed', 'К закупке'),
('ordered', 'В пути'),
('fulfilled', 'Обеспечено'),
]
UNIT_CHOICES = [
('m2', 'м²'),
('mm', 'мм'),
('pcs', 'шт'),
]
deal = models.ForeignKey(Deal, on_delete=models.CASCADE, verbose_name='Сделка')
material = models.ForeignKey(WarehouseMaterial, on_delete=models.PROTECT, verbose_name='Материал')
required_qty = models.FloatField('Нужно докупить')
unit = models.CharField('Ед. изм.', max_length=8, choices=UNIT_CHOICES, default='pcs')
status = models.CharField('Статус', max_length=20, choices=STATUS_CHOICES, default='needed')
class Meta:
verbose_name = 'Потребность'
verbose_name_plural = 'Потребности'
def __str__(self):
return f"{self.deal.number}: {self.material} -> {self.required_qty} {self.unit}"
class CuttingSession(models.Model):
"""Производственный отчет (основа для списания/начисления).
Основная идея документа:
- оператор фиксирует выработку по нескольким плановым заданиям за смену;
- списание сырья на участке может включать несколько позиций (листы/хлысты/куски);
- по итогам могут появляться несколько деловых остатков.
used_stock_item — legacy-поле для упрощённого случая «списали 1 единицу сырья».
Для реального списания используем ProductionReportConsumption.
"""
operator = models.ForeignKey(User, on_delete=models.PROTECT, verbose_name='Оператор')
machine = models.ForeignKey(Machine, on_delete=models.PROTECT, verbose_name='Станок')
used_stock_item = models.ForeignKey(
'warehouse.StockItem',
on_delete=models.PROTECT,
null=True,
blank=True,
verbose_name='Взятый материал (legacy)',
)
date = models.DateField('Дата', default=timezone.localdate)
created_at = models.DateTimeField(auto_now_add=True)
is_closed = models.BooleanField('Отчет закрыт', default=False)
class Meta:
verbose_name = 'Производственный отчет'
verbose_name_plural = 'Производственные отчеты'
def __str__(self):
return f"{self.date} {self.machine} ({self.operator})"
class ProductionReportConsumption(models.Model):
"""Строка списания сырья в рамках производственного отчёта.
Переходная схема:
- целевой ввод делается по номенклатуре (material);
- legacy-поле stock_item оставлено временно, чтобы мигрировать существующие записи.
После переноса данных stock_item будет удалён, а material станет обязательным.
"""
report = models.ForeignKey(CuttingSession, related_name='consumptions', on_delete=models.CASCADE, verbose_name='Производственный отчет')
material = models.ForeignKey(WarehouseMaterial, on_delete=models.PROTECT, null=True, blank=True, verbose_name='Материал')
stock_item = models.ForeignKey('warehouse.StockItem', on_delete=models.PROTECT, null=True, blank=True, verbose_name='Сырье (позиция склада, legacy)')
quantity = models.FloatField('Списано (ед.)')
class Meta:
verbose_name = 'Списание сырья'
verbose_name_plural = 'Списание сырья'
unique_together = ('report', 'material')
def __str__(self):
return f"{self.report_id}: {self.material} - {self.quantity}"
class ProductionReportRemnant(models.Model):
"""Деловой остаток, который нужно начислить по итогам производственного отчёта."""
report = models.ForeignKey(CuttingSession, related_name='remnants', on_delete=models.CASCADE, verbose_name='Производственный отчет')
material = models.ForeignKey(WarehouseMaterial, on_delete=models.PROTECT, verbose_name='Материал')
quantity = models.FloatField('Количество (ед.)', default=1.0)
current_length = models.FloatField('Текущая длина, мм', null=True, blank=True)
current_width = models.FloatField('Текущая ширина, мм', null=True, blank=True)
unique_id = models.CharField('ID/Маркировка', max_length=50, null=True, blank=True)
class Meta:
verbose_name = 'Деловой остаток'
verbose_name_plural = 'Деловые остатки'
def __str__(self):
return f"{self.report_id}: {self.material}"
class ProductionReportStockResult(models.Model):
"""След созданных складских позиций по отчету (готовые детали и деловые остатки)."""
KIND_CHOICES = [
('finished', 'Готовая деталь'),
('remnant', 'Деловой остаток'),
]
report = models.ForeignKey(CuttingSession, related_name='results', on_delete=models.CASCADE, verbose_name='Производственный отчет')
stock_item = models.ForeignKey('warehouse.StockItem', on_delete=models.PROTECT, verbose_name='Созданная позиция склада')
kind = models.CharField('Тип', max_length=16, choices=KIND_CHOICES)
class Meta:
verbose_name = 'Результат отчета'
verbose_name_plural = 'Результаты отчета'
unique_together = ('report', 'stock_item')
def __str__(self):
return f"{self.report_id}: {self.stock_item_id}"
class ShiftItem(models.Model):
"""Фиксация выработки в рамках производственного отчёта."""
session = models.ForeignKey(CuttingSession, related_name='tasks', on_delete=models.CASCADE)
task = models.ForeignKey(ProductionTask, on_delete=models.PROTECT, verbose_name='Плановое задание')
quantity_fact = models.PositiveIntegerField('Изготовлено (факт), шт', default=0)
material_substitution = models.BooleanField('Замена материала по факту', default=False)
class Meta:
verbose_name = 'Фиксация выработки'
verbose_name_plural = 'Фиксации выработки'
def __str__(self):
return f"{self.session} -> {self.task}"
class DxfPreviewJob(models.Model):
"""Фоновая задача пакетной регенерации превью DXF.
@@ -239,6 +431,7 @@ class EmployeeProfile(models.Model):
('master', 'Мастер'),
('operator', 'Оператор'),
('clerk', 'Учетчик'),
('observer', 'Наблюдатель'),
]
# Связь 1 к 1 со стандартным юзером Django