Files
MES_Core/warehouse/models.py
2026-04-13 07:36:57 +03:00

195 lines
9.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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:
verbose_name = "Категория материала"
verbose_name_plural = "Категории материалов"
def __str__(self):
return self.name
class SteelGrade(models.Model):
"""Марка стали (например, Ст3сп, 09Г2С) и связанные с ней ГОСТы"""
name = models.CharField("Марка стали", max_length=100, unique=True)
gost_standard = models.CharField("ГОСТ/ТУ", max_length=255, blank=True, help_text="Основной стандарт для этой марки")
certificate_pdf = models.FileField("Сертификат/ГОСТ (PDF)", upload_to='certificates/', blank=True, null=True)
class Meta:
verbose_name = "Марка стали"
verbose_name_plural = "Марки стали"
def __str__(self):
return f"{self.name} ({self.gost_standard})" if self.gost_standard else self.name
class Material(models.Model):
"""Конкретная номенклатурная единица (например, Труба 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)
full_name = models.CharField("Полное наименование", max_length=500, blank=True, help_text="Генерируется автоматически")
mass_per_unit = models.FloatField("Масса на ед. учёта", null=True, blank=True)
class Meta:
verbose_name = "Материал (номенклатура)"
verbose_name_plural = "Материалы"
def save(self, *args, **kwargs):
category_part = (self.category.name or '').strip() if self.category_id else ''
name_part = (self.name or '').strip()
grade_part = (self.steel_grade.name or '').strip() if self.steel_grade_id else ''
self.full_name = ' '.join([p for p in [category_part, name_part, grade_part] if p])
super().save(*args, **kwargs)
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)
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)
is_archived = models.BooleanField('В архиве', default=False)
archived_at = models.DateTimeField('Дата архивации', 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}"