Конкретно пересмотрел логику работы. Легаси вынесена в архив
All checks were successful
Deploy MES Core / deploy (push) Successful in 13s
All checks were successful
Deploy MES Core / deploy (push) Successful in 13s
This commit is contained in:
@@ -35,7 +35,12 @@ class Workshop(models.Model):
|
||||
|
||||
|
||||
class Machine(models.Model):
|
||||
"""Список производственных участков (станков).
|
||||
"""Справочник производственных постов (ресурсов).
|
||||
|
||||
Терминология UI:
|
||||
- в интерфейсе используется слово «Пост», чтобы одинаково обозначать станок, линию,
|
||||
камеру, рабочее место или бригаду (как единицу планирования у мастера).
|
||||
- в базе и коде модель остаётся Machine, чтобы не ломать существующие связи.
|
||||
|
||||
Источник склада для операций выработки/списаний:
|
||||
- предпочитаем склад цеха (Machine.workshop.location)
|
||||
@@ -45,6 +50,7 @@ class Machine(models.Model):
|
||||
MACHINE_TYPE_CHOICES = [
|
||||
('linear', 'Линейный'),
|
||||
('sheet', 'Листовой'),
|
||||
('post', 'Пост'),
|
||||
]
|
||||
|
||||
name = models.CharField("Название станка", max_length=100)
|
||||
@@ -74,6 +80,7 @@ class Deal(models.Model):
|
||||
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="Общая информация по заказу")
|
||||
due_date = models.DateField("Срок отгрузки", null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Сделка №{self.number} ({self.company})"
|
||||
@@ -100,7 +107,7 @@ class ProductionTask(models.Model):
|
||||
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="Материал")
|
||||
material = models.ForeignKey(WarehouseMaterial, on_delete=models.PROTECT, verbose_name="Материал", null=True, blank=True)
|
||||
quantity_ordered = models.PositiveIntegerField("Заказано всего, шт")
|
||||
is_bend = models.BooleanField("Гибка", default=False)
|
||||
|
||||
@@ -161,7 +168,10 @@ class DxfPreviewSettings(models.Model):
|
||||
|
||||
|
||||
class DealItem(models.Model):
|
||||
"""Состав сделки: что заказал клиент (точка входа для BOM Explosion)."""
|
||||
"""Состав сделки: что заказал клиент.
|
||||
|
||||
Примечание: при поставках частями используем DealDeliveryBatch/DealBatchItem.
|
||||
"""
|
||||
|
||||
deal = models.ForeignKey(Deal, related_name='items', on_delete=models.CASCADE, verbose_name='Сделка')
|
||||
entity = models.ForeignKey('manufacturing.ProductEntity', on_delete=models.PROTECT, verbose_name='Изделие/деталь')
|
||||
@@ -176,6 +186,66 @@ class DealItem(models.Model):
|
||||
return f"{self.deal.number}: {self.entity} x{self.quantity}"
|
||||
|
||||
|
||||
class DealDeliveryBatch(models.Model):
|
||||
"""Партия поставки по сделке (поставка частями)."""
|
||||
|
||||
deal = models.ForeignKey(Deal, on_delete=models.CASCADE, related_name='delivery_batches', verbose_name='Сделка')
|
||||
name = models.CharField('Название', max_length=120, blank=True, default='')
|
||||
due_date = models.DateField('Плановая отгрузка')
|
||||
is_default = models.BooleanField('Дефолтная партия (остаток)', default=False)
|
||||
created_at = models.DateTimeField('Создано', auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Партия поставки'
|
||||
verbose_name_plural = 'Партии поставки'
|
||||
ordering = ('deal', 'due_date', 'id')
|
||||
|
||||
def __str__(self):
|
||||
label = self.name.strip() or f"Партия {self.id}"
|
||||
return f"{self.deal.number}: {label} ({self.due_date:%d.%m.%Y})"
|
||||
|
||||
|
||||
class DealBatchItem(models.Model):
|
||||
"""Строка партии поставки: что и сколько отгружаем в эту дату.
|
||||
|
||||
started_qty — сколько уже запущено в производство по этой партии.
|
||||
"""
|
||||
|
||||
batch = models.ForeignKey(DealDeliveryBatch, on_delete=models.CASCADE, related_name='items', verbose_name='Партия')
|
||||
entity = models.ForeignKey('manufacturing.ProductEntity', on_delete=models.PROTECT, verbose_name='Изделие/деталь')
|
||||
quantity = models.PositiveIntegerField('Количество, шт')
|
||||
started_qty = models.PositiveIntegerField('Запущено в производство, шт', default=0)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Строка партии'
|
||||
verbose_name_plural = 'Строки партий'
|
||||
unique_together = ('batch', 'entity')
|
||||
ordering = ('batch', 'entity__entity_type', 'entity__drawing_number', 'entity__name', 'id')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.batch}: {self.entity} x{self.quantity}"
|
||||
|
||||
|
||||
class DealEntityProgress(models.Model):
|
||||
"""Текущая операция техпроцесса для пары (сделка, сущность).
|
||||
|
||||
Комментарий: current_seq=1 означает «выполняем 1-ю операцию в EntityOperation».
|
||||
Когда current_seq больше числа операций — сущность для сделки считается прошедшей техпроцесс.
|
||||
"""
|
||||
|
||||
deal = models.ForeignKey(Deal, on_delete=models.CASCADE, verbose_name='Сделка')
|
||||
entity = models.ForeignKey('manufacturing.ProductEntity', on_delete=models.PROTECT, verbose_name='Сущность')
|
||||
current_seq = models.PositiveSmallIntegerField('Текущая операция (порядок)', default=1)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Прогресс по операции'
|
||||
verbose_name_plural = 'Прогресс по операциям'
|
||||
unique_together = ('deal', 'entity')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.deal.number}: {self.entity} -> {self.current_seq}"
|
||||
|
||||
|
||||
class MaterialRequirement(models.Model):
|
||||
"""Потребность в закупке сырья для сделки.
|
||||
|
||||
@@ -212,6 +282,65 @@ class MaterialRequirement(models.Model):
|
||||
return f"{self.deal.number}: {self.material} -> {self.required_qty} {self.unit}"
|
||||
|
||||
|
||||
class ProcurementRequirement(models.Model):
|
||||
"""
|
||||
Потребность в закупке покупных комплектующих, литья и кооперации для сделки.
|
||||
Рассчитывается при взрыве BOM (с учетом свободных остатков на складах).
|
||||
"""
|
||||
STATUS_CHOICES = [
|
||||
('to_order', 'К заказу'),
|
||||
('ordered', 'Заказано'),
|
||||
('closed', 'Закрыто'),
|
||||
]
|
||||
|
||||
deal = models.ForeignKey(Deal, on_delete=models.CASCADE, verbose_name='Сделка')
|
||||
component = models.ForeignKey('manufacturing.ProductEntity', on_delete=models.PROTECT, verbose_name='Компонент (покупное/литье)')
|
||||
required_qty = models.PositiveIntegerField('Потребность (к закупке), шт')
|
||||
status = models.CharField('Статус', max_length=20, choices=STATUS_CHOICES, default='to_order')
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Потребность снабжения'
|
||||
verbose_name_plural = 'Потребности снабжения'
|
||||
unique_together = ('deal', 'component')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.deal.number}: {self.component} -> {self.required_qty}"
|
||||
|
||||
|
||||
class WorkItem(models.Model):
|
||||
deal = models.ForeignKey(Deal, on_delete=models.CASCADE, verbose_name='Сделка')
|
||||
entity = models.ForeignKey('manufacturing.ProductEntity', on_delete=models.PROTECT, verbose_name='Сущность')
|
||||
|
||||
# Комментарий: operation — основной признак операции (расширяемый справочник).
|
||||
operation = models.ForeignKey('manufacturing.Operation', on_delete=models.PROTECT, null=True, blank=True, verbose_name='Операция')
|
||||
|
||||
# Комментарий: stage оставляем строкой для совместимости с текущими фильтрами/экраном, но без choices.
|
||||
stage = models.CharField('Стадия', max_length=32, blank=True, default='')
|
||||
|
||||
machine = models.ForeignKey('shiftflow.Machine', on_delete=models.PROTECT, null=True, blank=True, verbose_name='Станок/участок')
|
||||
workshop = models.ForeignKey('shiftflow.Workshop', on_delete=models.PROTECT, null=True, blank=True, verbose_name='Цех')
|
||||
|
||||
quantity_plan = models.PositiveIntegerField('В план, шт', default=0)
|
||||
quantity_done = models.PositiveIntegerField('Сделано, шт', default=0)
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('planned', 'В работе'),
|
||||
('leftover', 'Недодел'),
|
||||
('done', 'Закрыта'),
|
||||
]
|
||||
|
||||
status = models.CharField('Статус', max_length=16, choices=STATUS_CHOICES, default='planned')
|
||||
date = models.DateField('Дата', default=timezone.localdate)
|
||||
comment = models.TextField('Комментарий', blank=True, default='')
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'План работ'
|
||||
verbose_name_plural = 'План работ'
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.deal.number}: {self.entity} [{self.stage}] {self.quantity_plan}"
|
||||
|
||||
|
||||
class CuttingSession(models.Model):
|
||||
"""Производственный отчет (основа для списания/начисления).
|
||||
|
||||
@@ -238,6 +367,17 @@ class CuttingSession(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
is_closed = models.BooleanField('Отчет закрыт', default=False)
|
||||
|
||||
is_synced_1c = models.BooleanField('Выгружено в 1С', default=False)
|
||||
synced_1c_at = models.DateTimeField('Выгружено в 1С (время)', null=True, blank=True)
|
||||
synced_1c_by = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='synced_cutting_sessions',
|
||||
verbose_name='Выгрузил в 1С',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Производственный отчет'
|
||||
verbose_name_plural = 'Производственные отчеты'
|
||||
@@ -392,7 +532,6 @@ class Item(models.Model):
|
||||
('done', 'Выполнено'),
|
||||
('partial', 'Частично'),
|
||||
('leftover', 'Недодел'),
|
||||
('imported', 'Импортировано'),
|
||||
]
|
||||
|
||||
# --- Ссылка на основу (временно null=True для миграции старых данных) ---
|
||||
@@ -432,14 +571,23 @@ class EmployeeProfile(models.Model):
|
||||
('operator', 'Оператор'),
|
||||
('clerk', 'Учетчик'),
|
||||
('observer', 'Наблюдатель'),
|
||||
('manager', 'Руководитель'),
|
||||
]
|
||||
|
||||
|
||||
# Связь 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='Должность')
|
||||
|
||||
# Комментарий: режим для руководителя/наблюдателя — видит всё, но любые изменения запрещены.
|
||||
is_readonly = models.BooleanField('Только просмотр', default=False)
|
||||
|
||||
# Привязка станков (можно выбрать несколько для одного оператора)
|
||||
machines = models.ManyToManyField('Machine', blank=True, verbose_name='Закрепленные станки')
|
||||
|
||||
# Комментарий: ограничение видимости/действий по цехам.
|
||||
# Если список пустой — считаем, что доступ не ограничен (админ/технолог/руководитель).
|
||||
allowed_workshops = models.ManyToManyField('Workshop', blank=True, verbose_name='Доступные цеха')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} - {self.get_role_display()}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user