Добавлена система трансформации товаров
Реализована полная система трансформации товаров (превращение одного товара в другой). Пример: белая гипсофила → крашеная гипсофила. Особенности реализации: - Резервирование входных товаров в статусе draft - FIFO списание входных товаров при проведении - Автоматический расчёт себестоимости выходных товаров - Возможность отмены как черновиков, так и проведённых трансформаций Модели (inventory/models.py): - Transformation: документ трансформации (draft/completed/cancelled) - TransformationInput: входные товары (списание) - TransformationOutput: выходные товары (оприходование) - Добавлен статус 'converted_to_transformation' в Reservation - Добавлен тип 'transformation' в DocumentCounter Бизнес-логика (inventory/services/transformation_service.py): - TransformationService с методами CRUD - Валидация наличия товаров - Автоматическая генерация номеров документов Сигналы (inventory/signals.py): - Автоматическое резервирование входных товаров - FIFO списание при проведении - Создание партий выходных товаров - Откат операций при отмене Интерфейс без Django Admin: - Список трансформаций (list.html) - Форма создания (form.html) - Детальный просмотр с добавлением товаров (detail.html) - Интеграция с компонентом поиска товаров - 8 views для полного CRUD + проведение/отмена Миграция: - 0003_alter_documentcounter_counter_type_and_more.py 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -487,6 +487,7 @@ class Reservation(models.Model):
|
||||
('released', 'Освобожден'),
|
||||
('converted_to_sale', 'Преобразован в продажу'),
|
||||
('converted_to_writeoff', 'Преобразован в списание'),
|
||||
('converted_to_transformation', 'Преобразован в трансформацию'),
|
||||
]
|
||||
|
||||
order_item = models.ForeignKey('orders.OrderItem', on_delete=models.CASCADE,
|
||||
@@ -505,7 +506,7 @@ class Reservation(models.Model):
|
||||
warehouse = models.ForeignKey(Warehouse, on_delete=models.CASCADE,
|
||||
related_name='reservations', verbose_name="Склад")
|
||||
quantity = models.DecimalField(max_digits=10, decimal_places=3, verbose_name="Количество")
|
||||
status = models.CharField(max_length=25, choices=STATUS_CHOICES,
|
||||
status = models.CharField(max_length=30, choices=STATUS_CHOICES,
|
||||
default='reserved', verbose_name="Статус")
|
||||
reserved_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата резервирования")
|
||||
released_at = models.DateTimeField(null=True, blank=True, verbose_name="Дата освобождения")
|
||||
@@ -556,6 +557,17 @@ class Reservation(models.Model):
|
||||
help_text="Резерв для документа списания (черновик)"
|
||||
)
|
||||
|
||||
# Связь с входным товаром трансформации (для резервирования в черновике)
|
||||
transformation_input = models.ForeignKey(
|
||||
'TransformationInput',
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='reservations',
|
||||
verbose_name="Входной товар трансформации",
|
||||
help_text="Резерв для входного товара трансформации (черновик)"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Резервирование"
|
||||
verbose_name_plural = "Резервирования"
|
||||
@@ -831,6 +843,7 @@ class DocumentCounter(models.Model):
|
||||
('writeoff', 'Списание товара'),
|
||||
('incoming', 'Поступление товара'),
|
||||
('inventory', 'Инвентаризация'),
|
||||
('transformation', 'Трансформация товара'),
|
||||
]
|
||||
|
||||
counter_type = models.CharField(
|
||||
@@ -1392,3 +1405,150 @@ class IncomingDocumentItem(models.Model):
|
||||
def total_cost(self):
|
||||
"""Себестоимость позиции (quantity * cost_price)"""
|
||||
return self.quantity * self.cost_price
|
||||
|
||||
|
||||
class Transformation(models.Model):
|
||||
"""
|
||||
Документ трансформации товара (превращение одного товара в другой).
|
||||
|
||||
Пример: белая гипсофила → крашеная гипсофила
|
||||
"""
|
||||
STATUS_CHOICES = [
|
||||
('draft', 'Черновик'),
|
||||
('completed', 'Проведён'),
|
||||
('cancelled', 'Отменён'),
|
||||
]
|
||||
|
||||
document_number = models.CharField(
|
||||
max_length=100,
|
||||
unique=True,
|
||||
db_index=True,
|
||||
verbose_name="Номер документа"
|
||||
)
|
||||
|
||||
warehouse = models.ForeignKey(
|
||||
Warehouse,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='transformations',
|
||||
verbose_name="Склад"
|
||||
)
|
||||
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=STATUS_CHOICES,
|
||||
default='draft',
|
||||
db_index=True,
|
||||
verbose_name="Статус"
|
||||
)
|
||||
|
||||
date = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
verbose_name="Дата создания"
|
||||
)
|
||||
|
||||
employee = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='transformations',
|
||||
verbose_name="Сотрудник"
|
||||
)
|
||||
|
||||
comment = models.TextField(
|
||||
blank=True,
|
||||
verbose_name="Комментарий"
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Создан")
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="Обновлён")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Трансформация товара"
|
||||
verbose_name_plural = "Трансформации товаров"
|
||||
ordering = ['-date']
|
||||
indexes = [
|
||||
models.Index(fields=['document_number']),
|
||||
models.Index(fields=['warehouse', 'status']),
|
||||
models.Index(fields=['-date']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.document_number} ({self.get_status_display()})"
|
||||
|
||||
|
||||
class TransformationInput(models.Model):
|
||||
"""
|
||||
Входной товар трансформации (что списываем).
|
||||
"""
|
||||
transformation = models.ForeignKey(
|
||||
Transformation,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='inputs',
|
||||
verbose_name="Трансформация"
|
||||
)
|
||||
|
||||
product = models.ForeignKey(
|
||||
Product,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='transformation_inputs',
|
||||
verbose_name="Товар"
|
||||
)
|
||||
|
||||
quantity = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=3,
|
||||
verbose_name="Количество"
|
||||
)
|
||||
|
||||
# Резерв (создается автоматически при draft)
|
||||
# Связь через Reservation.transformation_input
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Входной товар трансформации"
|
||||
verbose_name_plural = "Входные товары трансформации"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.product.name}: {self.quantity}"
|
||||
|
||||
|
||||
class TransformationOutput(models.Model):
|
||||
"""
|
||||
Выходной товар трансформации (что получаем).
|
||||
"""
|
||||
transformation = models.ForeignKey(
|
||||
Transformation,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='outputs',
|
||||
verbose_name="Трансформация"
|
||||
)
|
||||
|
||||
product = models.ForeignKey(
|
||||
Product,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='transformation_outputs',
|
||||
verbose_name="Товар"
|
||||
)
|
||||
|
||||
quantity = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=3,
|
||||
verbose_name="Количество"
|
||||
)
|
||||
|
||||
# Ссылка на созданную партию (после проведения)
|
||||
stock_batch = models.ForeignKey(
|
||||
StockBatch,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='transformation_outputs',
|
||||
verbose_name="Созданная партия"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Выходной товар трансформации"
|
||||
verbose_name_plural = "Выходные товары трансформации"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.product.name}: {self.quantity}"
|
||||
|
||||
Reference in New Issue
Block a user