Огромная замена логики
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

@@ -1,8 +1,20 @@
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from django.core.exceptions import ValidationError
import uuid
class MaterialCategory(models.Model):
"""Категория материала (например, Труба, Лист, Круг)"""
FORM_FACTOR_CHOICES = [
('sheet', 'Лист'),
('bar', 'Прокат/хлыст'),
('other', 'Прочее'),
]
name = models.CharField("Название категории", max_length=100, unique=True)
form_factor = models.CharField('Форма', max_length=16, choices=FORM_FACTOR_CHOICES, default='other')
gost_standard = models.CharField("ГОСТ на тип проката", max_length=255, blank=True, help_text="Напр: ГОСТ 8639-82")
class Meta:
@@ -26,7 +38,7 @@ class SteelGrade(models.Model):
return f"{self.name} ({self.gost_standard})" if self.gost_standard else self.name
class Material(models.Model):
"""Конкретная номенклатурная единица (например, Труба 100х100х4)"""
"""Конкретная номенклатурная единица (например, Труба 100х100х4)."""
category = models.ForeignKey(MaterialCategory, on_delete=models.PROTECT, verbose_name="Категория")
steel_grade = models.ForeignKey(SteelGrade, on_delete=models.PROTECT, verbose_name="Марка стали", null=True, blank=True)
name = models.CharField("Наименование (размер/характеристики)", max_length=255)
@@ -46,3 +58,134 @@ class Material(models.Model):
def __str__(self):
return self.full_name or ' '.join([p for p in [(self.category.name if self.category_id else ''), self.name, (self.steel_grade.name if self.steel_grade_id else '')] if p]).strip()
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):
"""Физический остаток на складе.
Правила заполнения:
- если это сырьё: заполнен material, entity пустой
- если это готовая деталь: заполнен entity, material пустой
Количество (quantity) интерпретируется как "единица учёта" для конкретного объекта:
- для листа может быть 1 лист
- для профиля/трубы может быть 1 хлыст
- для готовых деталей обычно шт.
"""
material = models.ForeignKey('warehouse.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="Произведённая сущность")
deal = models.ForeignKey('shiftflow.Deal', on_delete=models.PROTECT, null=True, blank=True, verbose_name='Сделка')
location = models.ForeignKey(Location, on_delete=models.PROTECT, verbose_name="Где находится")
quantity = models.FloatField("Количество (шт/м/кг/лист)")
created_at = models.DateTimeField("Поступление", default=timezone.now, editable=False)
created_at = models.DateTimeField("Поступление", default=timezone.now, editable=False)
is_remnant = models.BooleanField("Деловой остаток", default=False)
is_customer_supplied = 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 clean(self):
if self.material_id and not self.entity_id:
category = getattr(self.material, 'category', None)
form_factor = getattr(category, 'form_factor', 'other') if category else 'other'
if form_factor == 'sheet':
if self.current_length in (None, '') or self.current_width in (None, ''):
raise ValidationError('Для листового материала нужно заполнить длину и ширину (мм).')
if form_factor == 'bar':
if self.current_length in (None, ''):
raise ValidationError('Для проката нужно заполнить длину (мм).')
super().clean()
def save(self, *args, **kwargs):
if self.is_remnant and not self.unique_id:
while True:
candidate = f"DO-{uuid.uuid4().hex[:12].upper()}"
if not StockItem.objects.filter(unique_id=candidate).exists():
self.unique_id = candidate
break
super().save(*args, **kwargs)
def __str__(self):
obj = self.entity if self.entity_id else self.material
return f"{obj} | {self.quantity} | {self.location}"
class TransferRecord(models.Model):
"""Документ перемещения между складами.
Состав перемещения хранится в строках TransferLine, где указывается:
- какая складская позиция списывается со склада-источника
- сколько списывается
Статусы «в пути/принято» можно расширить позже. Сейчас ключевое — корректное списание/начисление.
"""
STATUS_CHOICES = [
('sent', 'В пути'),
('received', 'Принято'),
('discrepancy', 'Расхождение'),
]
from_location = models.ForeignKey(Location, related_name='outgoing', on_delete=models.PROTECT, verbose_name='Откуда')
to_location = models.ForeignKey(Location, related_name='incoming', on_delete=models.PROTECT, verbose_name='Куда')
sender = models.ForeignKey(User, related_name='sent_transfers', on_delete=models.PROTECT, null=True, blank=True, verbose_name='Кто')
receiver = models.ForeignKey(User, related_name='received_transfers', on_delete=models.PROTECT, null=True, blank=True, verbose_name='Кому')
occurred_at = models.DateTimeField('Дата/время', default=timezone.now)
status = models.CharField("Статус", max_length=20, choices=STATUS_CHOICES, default='received')
created_at = models.DateTimeField(auto_now_add=True)
received_at = models.DateTimeField(null=True, blank=True, default=timezone.now)
is_applied = models.BooleanField('Применено', default=False)
class Meta:
verbose_name = "Перемещение"
verbose_name_plural = "Перемещения"
def __str__(self):
return f"{self.from_location} -> {self.to_location}"
class TransferLine(models.Model):
"""Строка перемещения: сколько списать из конкретной складской позиции."""
transfer = models.ForeignKey(TransferRecord, related_name='lines', on_delete=models.CASCADE, verbose_name='Перемещение')
stock_item = models.ForeignKey(StockItem, on_delete=models.PROTECT, verbose_name='Единица на складе')
quantity = models.FloatField('Количество')
class Meta:
verbose_name = 'Строка перемещения'
verbose_name_plural = 'Строки перемещения'
unique_together = ('transfer', 'stock_item')
def __str__(self):
return f"{self.transfer_id}: {self.stock_item_id} x {self.quantity}"