All checks were successful
Deploy MES Core / deploy (push) Successful in 13s
596 lines
29 KiB
Python
596 lines
29 KiB
Python
from django.db import models
|
||
from django.utils import timezone
|
||
from django.contrib.auth.models import User
|
||
from warehouse.models import Material as WarehouseMaterial
|
||
|
||
class Company(models.Model):
|
||
"""
|
||
Справочник контрагентов/заказчиков.
|
||
Позволяет группировать сделки по компаниям и избегать дублей в названиях.
|
||
"""
|
||
name = models.CharField("Название компании", max_length=255, unique=True)
|
||
description = models.TextField("Краткое описание / Примечание", blank=True)
|
||
|
||
def __str__(self): return self.name
|
||
class Meta:
|
||
verbose_name = "Компания"; verbose_name_plural = "Компании"
|
||
|
||
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):
|
||
"""Справочник производственных постов (ресурсов).
|
||
|
||
Терминология UI:
|
||
- в интерфейсе используется слово «Пост», чтобы одинаково обозначать станок, линию,
|
||
камеру, рабочее место или бригаду (как единицу планирования у мастера).
|
||
- в базе и коде модель остаётся Machine, чтобы не ломать существующие связи.
|
||
|
||
Источник склада для операций выработки/списаний:
|
||
- предпочитаем склад цеха (Machine.workshop.location)
|
||
- поле Machine.location оставлено для совместимости (если цех не задан)
|
||
"""
|
||
|
||
MACHINE_TYPE_CHOICES = [
|
||
('linear', 'Линейный'),
|
||
('sheet', 'Листовой'),
|
||
('post', 'Пост'),
|
||
]
|
||
|
||
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
|
||
|
||
class Meta:
|
||
verbose_name = "Станок"; verbose_name_plural = "Станки"
|
||
|
||
class Deal(models.Model):
|
||
"""
|
||
Заказ или проект. Номер парсится из пути к файлам.
|
||
Служит контейнером для группы деталей (позиций).
|
||
"""
|
||
|
||
STATUS_CHOICES = [
|
||
('lead', 'Зашла'),
|
||
('work', 'В работе'),
|
||
('done', 'Завершена'),
|
||
]
|
||
|
||
number = models.CharField("№ Сделки", max_length=100, unique=True)
|
||
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})"
|
||
|
||
class Meta:
|
||
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="Длина (мм) или Толщина (мм)")
|
||
|
||
drawing_file = models.FileField("Исходник (DXF/IGES)", upload_to="drawings/%Y/%m/", blank=True, null=True)
|
||
extra_drawing = models.FileField("Доп. чертеж (PDF)", upload_to="extra_drawings/%Y/%m/", blank=True, null=True)
|
||
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="Материал", null=True, blank=True)
|
||
quantity_ordered = models.PositiveIntegerField("Заказано всего, шт")
|
||
is_bend = models.BooleanField("Гибка", default=False)
|
||
|
||
created_at = models.DateTimeField("Дата создания", auto_now_add=True)
|
||
|
||
class Meta:
|
||
verbose_name = "Задание на деталь"; verbose_name_plural = "Задания на детали"
|
||
ordering = ['-created_at']
|
||
|
||
def __str__(self):
|
||
return f"{self.drawing_name} (Заказ {self.deal.number})"
|
||
|
||
class DxfPreviewSettings(models.Model):
|
||
"""Настройки генерации превью для DXF.
|
||
|
||
Храним в БД, чтобы админ мог менять параметры через страницу «Обслуживание сервера»
|
||
без правок кода.
|
||
|
||
Сделано как singleton: ожидается одна строка (обычно pk=1).
|
||
"""
|
||
|
||
line_color = models.CharField(
|
||
"Цвет линий превью (HEX)",
|
||
max_length=16,
|
||
default="#006400",
|
||
help_text="Напр: #006400 (тёмно-зелёный)",
|
||
)
|
||
lineweight_scaling = models.FloatField(
|
||
"Коэффициент толщины линий",
|
||
default=1.0,
|
||
help_text="1.0 = как в DXF, 2.0 = толще, 0.5 = тоньше",
|
||
)
|
||
min_lineweight = models.FloatField(
|
||
"Минимальная толщина (мм)",
|
||
default=0.1,
|
||
help_text="Если в DXF нет lineweight — используем минимум, чтобы линии были видимы",
|
||
)
|
||
keep_original_colors = models.BooleanField(
|
||
"Оставить цвета оригинальные",
|
||
default=False,
|
||
help_text="Если включено — не перекрашиваем линии, берём цвета из DXF",
|
||
)
|
||
|
||
per_task_timeout_sec = models.PositiveIntegerField(
|
||
"Таймаут на 1 DXF (сек)",
|
||
default=45,
|
||
help_text="Если конкретный DXF завис — убиваем обработку этой детали и идём дальше",
|
||
)
|
||
|
||
updated_at = models.DateTimeField("Обновлено", auto_now=True)
|
||
|
||
class Meta:
|
||
verbose_name = "Настройки превью DXF"
|
||
verbose_name_plural = "Настройки превью DXF"
|
||
|
||
def __str__(self):
|
||
return "Настройки превью DXF"
|
||
|
||
|
||
class DealItem(models.Model):
|
||
"""Состав сделки: что заказал клиент.
|
||
|
||
Примечание: при поставках частями используем 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='Изделие/деталь')
|
||
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 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):
|
||
"""Потребность в закупке сырья для сделки.
|
||
|
||
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 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):
|
||
"""Производственный отчет (основа для списания/начисления).
|
||
|
||
Основная идея документа:
|
||
- оператор фиксирует выработку по нескольким плановым заданиям за смену;
|
||
- списание сырья на участке может включать несколько позиций (листы/хлысты/куски);
|
||
- по итогам могут появляться несколько деловых остатков.
|
||
|
||
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)
|
||
|
||
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 = 'Производственные отчеты'
|
||
|
||
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.
|
||
|
||
Зачем нужна:
|
||
- генерация превью и bbox может быть тяжёлой и в синхронном POST «вешает» ответ;
|
||
- поэтому мы запускаем задачу в фоне и пишем прогресс в БД;
|
||
- UI может показывать статус/счётчики без ожидания завершения.
|
||
|
||
Важно:
|
||
- это не Celery и не очередь, а простой «фон» для текущего процесса Django;
|
||
- для продакшена лучше вынести в полноценный воркер, но этот вариант уже убирает зависания UI.
|
||
"""
|
||
|
||
STATUS_CHOICES = [
|
||
('queued', 'В очереди'),
|
||
('running', 'Выполняется'),
|
||
('done', 'Готово'),
|
||
('failed', 'Ошибка'),
|
||
('cancelled', 'Остановлено'),
|
||
]
|
||
|
||
status = models.CharField("Статус", max_length=16, choices=STATUS_CHOICES, default='queued')
|
||
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Запустил")
|
||
|
||
cancel_requested = models.BooleanField(
|
||
"Запрошена остановка",
|
||
default=False,
|
||
help_text="Если включено — воркер завершит задачу после текущей детали",
|
||
)
|
||
pid = models.PositiveIntegerField(
|
||
"PID процесса",
|
||
null=True,
|
||
blank=True,
|
||
help_text="Номер процесса, который выполняет задачу (для диагностики)",
|
||
)
|
||
|
||
started_at = models.DateTimeField("Начато", null=True, blank=True)
|
||
finished_at = models.DateTimeField("Завершено", null=True, blank=True)
|
||
|
||
total = models.PositiveIntegerField("Всего задач", default=0)
|
||
processed = models.PositiveIntegerField("Обработано", default=0)
|
||
updated = models.PositiveIntegerField("Обновлено", default=0)
|
||
skipped = models.PositiveIntegerField("Пропущено", default=0)
|
||
errors = models.PositiveIntegerField("Ошибок", default=0)
|
||
|
||
last_message = models.CharField("Сообщение", max_length=255, blank=True, default='')
|
||
|
||
created_at = models.DateTimeField("Создано", auto_now_add=True)
|
||
|
||
class Meta:
|
||
verbose_name = "Задача превью DXF"
|
||
verbose_name_plural = "Задачи превью DXF"
|
||
ordering = ['-id']
|
||
|
||
def __str__(self):
|
||
return f"DXF превью: {self.get_status_display()}"
|
||
|
||
|
||
class Item(models.Model):
|
||
"""
|
||
Единица сменного задания. Определяет КТО, КОГДА и СКОЛЬКО сделал.
|
||
"""
|
||
STATUS_CHOICES = [
|
||
('work', 'В работе'),
|
||
('done', 'Выполнено'),
|
||
('partial', 'Частично'),
|
||
('leftover', 'Недодел'),
|
||
]
|
||
|
||
# --- Ссылка на основу (временно null=True для миграции старых данных) ---
|
||
task = models.ForeignKey(ProductionTask, on_delete=models.CASCADE, related_name='items', verbose_name="Задание", null=True, blank=True)
|
||
|
||
# --- Смена (заполняет мастер) ---
|
||
date = models.DateField("Дата смены", default=timezone.localdate)
|
||
machine = models.ForeignKey(Machine, on_delete=models.PROTECT, verbose_name="Станок")
|
||
quantity_plan = models.PositiveIntegerField("План на смену, шт")
|
||
|
||
# --- Исполнение (заполняет оператор) ---
|
||
quantity_fact = models.PositiveIntegerField("Факт, шт", default=0)
|
||
|
||
material_taken = models.TextField("Взятый материал", blank=True, help_text="Напр: 3 трубы по 12м")
|
||
usable_waste = models.TextField("Деловой отход", blank=True, help_text="Напр: кусок 1500мм")
|
||
scrap_weight = models.FloatField("Лом (кг)", default=0.0)
|
||
|
||
# --- Статусы и учет ---
|
||
status = models.CharField("Статус", max_length=10, choices=STATUS_CHOICES, default='work')
|
||
is_synced_1c = models.BooleanField("Учтено в 1С", default=False)
|
||
|
||
class Meta:
|
||
verbose_name = "Пункт сменки"; verbose_name_plural = "Реестр сменных заданий"
|
||
ordering = ['-date', 'task__deal']
|
||
|
||
def __str__(self):
|
||
if self.task:
|
||
return f"{self.task.drawing_name} - {self.date}"
|
||
return f"Без задания - {self.date}"
|
||
|
||
|
||
class EmployeeProfile(models.Model):
|
||
ROLE_CHOICES = [
|
||
('admin', 'Администратор'),
|
||
('technologist', 'Технолог'),
|
||
('master', 'Мастер'),
|
||
('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()}"
|
||
|
||
class Meta:
|
||
verbose_name = 'Профиль сотрудника'
|
||
verbose_name_plural = 'Профили сотрудников' |