This commit is contained in:
50
exempl/manufacturing/models.py
Normal file
50
exempl/manufacturing/models.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from django.db import models
|
||||
|
||||
class RouteStub(models.Model):
|
||||
"""Заглушка для будущего модуля техпроцессов."""
|
||||
name = models.CharField("Маршрут (напр. Лазер-Гибка-Сварка)", max_length=200, unique=True)
|
||||
|
||||
def __str__(self): return self.name
|
||||
|
||||
class ProductEntity(models.Model):
|
||||
"""
|
||||
Универсальный паспорт Детали или Сборки.
|
||||
Логика Вьюх:
|
||||
Это "Чертеж". Он не привязан к конкретному заказу (Сделке).
|
||||
planned_material - это то, что задумал конструктор. Оператор по факту может взять другое сырье.
|
||||
"""
|
||||
ENTITY_TYPE = [('product', 'Готовое изделие'), ('assembly', 'Сборочная единица'), ('part', 'Деталь')]
|
||||
|
||||
name = models.CharField("Наименование", max_length=255)
|
||||
drawing_number = models.CharField("Обозначение/Чертеж", max_length=100, blank=True)
|
||||
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="Заложенный материал")
|
||||
route = models.ForeignKey(RouteStub, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Маршрут")
|
||||
|
||||
weight_unit = models.FloatField("Вес 1 шт, кг", default=0.0)
|
||||
surface_area = models.FloatField("Площадь поверхности, м2", default=0.0)
|
||||
|
||||
dxf_file = models.FileField("Исходник (DXF/IGES)", upload_to="drawings/dxf/%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)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "КД (Изделие/Деталь)"; verbose_name_plural = "Конструкторская документация"
|
||||
|
||||
def __str__(self): return f"{self.drawing_number} {self.name}".strip()
|
||||
|
||||
class BOM(models.Model):
|
||||
"""
|
||||
Спецификация (Bill of Materials). Состав изделия.
|
||||
Логика Вьюх:
|
||||
При создании заказа на 5 "Лавок", система рекурсивно ищет все 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)"
|
||||
90
exempl/shiftflow/models.py
Normal file
90
exempl/shiftflow/models.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
# --- СПРАВОЧНИКИ ---
|
||||
class Company(models.Model):
|
||||
name = models.CharField("Название компании", max_length=255, unique=True)
|
||||
def __str__(self): return self.name
|
||||
|
||||
class Machine(models.Model):
|
||||
name = models.CharField("Название станка", max_length=100)
|
||||
def __str__(self): return self.name
|
||||
|
||||
class EmployeeProfile(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
|
||||
role = models.CharField("Должность", max_length=20, default='operator')
|
||||
machines = models.ManyToManyField(Machine, blank=True)
|
||||
def __str__(self): return self.user.username
|
||||
|
||||
# --- КОНТУР ЗАКАЗОВ (СДЕЛКИ И ПОТРЕБНОСТИ) ---
|
||||
class Deal(models.Model):
|
||||
"""Сделка. Контейнер заказа клиента."""
|
||||
number = models.CharField("№ Сделки", max_length=100, unique=True)
|
||||
company = models.ForeignKey(Company, on_delete=models.PROTECT, null=True, blank=True)
|
||||
status = models.CharField("Статус", max_length=20, default='lead')
|
||||
def __str__(self): return f"Сделка №{self.number}"
|
||||
|
||||
class DealItem(models.Model):
|
||||
"""
|
||||
Что заказал клиент (точка входа MRP).
|
||||
Логика Вьюх:
|
||||
Менеджер вносит сюда 5 шт "Лавок". На основе этого генерируются MaterialRequirement и ProductionTask.
|
||||
"""
|
||||
deal = models.ForeignKey(Deal, related_name='items', on_delete=models.CASCADE)
|
||||
entity = models.ForeignKey('manufacturing.ProductEntity', on_delete=models.PROTECT, verbose_name="Изделие")
|
||||
quantity = models.PositiveIntegerField("Заказано, шт")
|
||||
|
||||
class MaterialRequirement(models.Model):
|
||||
"""
|
||||
Потребность в закупке сырья для Сделки.
|
||||
Логика Вьюх:
|
||||
Генерируется автоматически после "взрыва" спецификации (BOM).
|
||||
Снабженец видит это в своем АРМ и организует приход.
|
||||
"""
|
||||
STATUS = [('needed', 'К закупке'), ('ordered', 'В пути'), ('fulfilled', 'Обеспечено')]
|
||||
deal = models.ForeignKey(Deal, on_delete=models.CASCADE)
|
||||
material = models.ForeignKey('warehouse.Material', on_delete=models.PROTECT)
|
||||
required_qty = models.FloatField("Нужно докупить (шт/м/кг)")
|
||||
status = models.CharField(max_length=20, choices=STATUS, default='needed')
|
||||
|
||||
# --- ПРОИЗВОДСТВЕННЫЙ КОНТУР ---
|
||||
class ProductionTask(models.Model):
|
||||
"""
|
||||
Сменное задание (План на производство детали).
|
||||
Логика: Связывает Сделку и КД (ProductEntity).
|
||||
"""
|
||||
deal = models.ForeignKey(Deal, on_delete=models.CASCADE, verbose_name="Сделка")
|
||||
entity = models.ForeignKey('manufacturing.ProductEntity', on_delete=models.CASCADE, verbose_name="Что делать")
|
||||
quantity_ordered = models.PositiveIntegerField("План, шт")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self): return f"{self.entity.name} (Заказ {self.deal.number})"
|
||||
|
||||
class CuttingSession(models.Model):
|
||||
"""
|
||||
Сессия переработки (Основа для списания/начисления).
|
||||
Логика Вьюх:
|
||||
Оператор создает сессию. Указывает, какой StockItem (Лист/Хлыст) он взял со своего участка.
|
||||
В рамках сессии он закрывает пункты (ShiftItem).
|
||||
При закрытии сессии Вьюха: 1) списывает used_stock_item, 2) начисляет новые StockItem с готовыми деталями, 3) начисляет ДО.
|
||||
"""
|
||||
operator = models.ForeignKey(User, on_delete=models.PROTECT)
|
||||
machine = models.ForeignKey(Machine, on_delete=models.PROTECT)
|
||||
used_stock_item = models.ForeignKey('warehouse.StockItem', on_delete=models.PROTECT, verbose_name="Взятый со склада материал")
|
||||
|
||||
date = models.DateField("Дата", default=timezone.localdate)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
is_closed = models.BooleanField("Сессия закрыта", default=False)
|
||||
|
||||
class ShiftItem(models.Model):
|
||||
"""
|
||||
Конкретный пункт отчета в рамках сессии.
|
||||
(Замена старой модели Item).
|
||||
"""
|
||||
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)
|
||||
|
||||
# Флаг для контроля отклонений (если взяли Ст3 вместо Ст10)
|
||||
material_substitution = models.BooleanField("Замена материала по факту", default=False)
|
||||
104
exempl/warehouse/models.py
Normal file
104
exempl/warehouse/models.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
class MaterialCategory(models.Model):
|
||||
"""Категория сырья (Лист, Труба, Круг)."""
|
||||
name = models.CharField("Название категории", max_length=100, unique=True)
|
||||
gost_standard = models.CharField("ГОСТ", max_length=255, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Категория материала"
|
||||
verbose_name_plural = "Категории материалов"
|
||||
|
||||
def __str__(self): return self.name
|
||||
|
||||
class SteelGrade(models.Model):
|
||||
"""Марка стали."""
|
||||
name = models.CharField("Марка стали", max_length=100, unique=True)
|
||||
gost_standard = models.CharField("ГОСТ/ТУ", max_length=255, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Марка стали"; verbose_name_plural = "Марки стали"
|
||||
|
||||
def __str__(self): return self.name
|
||||
|
||||
class Material(models.Model):
|
||||
"""
|
||||
Справочник закупаемого сырья (Номенклатура).
|
||||
Логика: Это только "идея" материала, а не физический объект на полке.
|
||||
Для листа заполняем thickness, для трубы width (сечение), для всех length (стандартная длина).
|
||||
"""
|
||||
category = models.ForeignKey(MaterialCategory, on_delete=models.PROTECT, verbose_name="Категория")
|
||||
steel_grade = models.ForeignKey(SteelGrade, on_delete=models.PROTECT, null=True, blank=True)
|
||||
name = models.CharField("Наименование", max_length=255)
|
||||
|
||||
thickness = models.FloatField("Толщина (S), мм", null=True, blank=True)
|
||||
width = models.FloatField("Ширина/Сечение (B), мм", null=True, blank=True)
|
||||
length = models.FloatField("Длина (L), мм", null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Номенклатура (Сырье)"; verbose_name_plural = "Номенклатура (Сырье)"
|
||||
|
||||
def __str__(self): return f"{self.category.name} {self.name} {self.steel_grade.name if self.steel_grade else ''}"
|
||||
|
||||
class Location(models.Model):
|
||||
"""Склады и участки (Центральный, Лазер, Сварка, СГП)."""
|
||||
name = models.CharField("Место хранения", max_length=100, unique=True)
|
||||
is_production_area = models.BooleanField("Это производственный участок", default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Склад/Участок"; verbose_name_plural = "Склады и участки"
|
||||
|
||||
def __str__(self): return self.name
|
||||
|
||||
class StockItem(models.Model):
|
||||
"""
|
||||
Универсальная физическая единица на складе.
|
||||
Логика Вьюх:
|
||||
1. Если это сырье: заполнен material, пусто entity.
|
||||
2. Если это готовая деталь: заполнен entity, пусто material.
|
||||
3. Если is_remnant=True, то current_length/width показывают реальный размер куска.
|
||||
При списании в CuttingSession количество здесь уменьшается. Если 0 - можно удалять или скрывать.
|
||||
"""
|
||||
material = models.ForeignKey(Material, on_delete=models.PROTECT, null=True, blank=True, verbose_name="Сырье")
|
||||
entity = models.ForeignKey('manufacturing.ProductEntity', on_delete=models.PROTECT, null=True, blank=True, verbose_name="Произведенная сущность")
|
||||
|
||||
location = models.ForeignKey(Location, on_delete=models.PROTECT, verbose_name="Где находится")
|
||||
quantity = models.FloatField("Количество (шт/м/кг)")
|
||||
|
||||
# Для деловых остатков
|
||||
is_remnant = models.BooleanField("Деловой остаток", default=False)
|
||||
current_length = models.FloatField("Текущая длина, мм", null=True, blank=True)
|
||||
current_width = models.FloatField("Текущая ширина, мм", null=True, blank=True)
|
||||
unique_id = models.CharField("ID/Маркировка (для ДО)", max_length=50, unique=True, null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Единица на складе"; verbose_name_plural = "Остатки на складах"
|
||||
|
||||
def __str__(self):
|
||||
obj = self.entity if self.entity else self.material
|
||||
return f"{obj} | {self.quantity} ед. | {self.location}"
|
||||
|
||||
class TransferRecord(models.Model):
|
||||
"""
|
||||
Документ перемещения (Вариант Б: строгий учет).
|
||||
Логика Вьюх:
|
||||
Создается "Отправителем" (статус sent).
|
||||
"Получатель" видит его в своем интерфейсе и жмет "Принять" (статус received).
|
||||
В этот момент у связанных StockItem меняется location на to_location.
|
||||
"""
|
||||
STATUS_CHOICES = [('sent', 'В пути'), ('received', 'Принято'), ('discrepancy', 'Расхождение')]
|
||||
|
||||
items = models.ManyToManyField(StockItem, verbose_name="Перемещаемые объекты")
|
||||
from_location = models.ForeignKey(Location, related_name='outgoing', on_delete=models.PROTECT)
|
||||
to_location = models.ForeignKey(Location, related_name='incoming', on_delete=models.PROTECT)
|
||||
|
||||
sender = models.ForeignKey(User, related_name='sent_transfers', on_delete=models.PROTECT)
|
||||
receiver = models.ForeignKey(User, related_name='received_transfers', on_delete=models.PROTECT, null=True, blank=True)
|
||||
status = models.CharField("Статус", max_length=20, choices=STATUS_CHOICES, default='sent')
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
received_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Перемещение"; verbose_name_plural = "Перемещения"
|
||||
Reference in New Issue
Block a user