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