This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user