Добавлена система трансформации товаров

Реализована полная система трансформации товаров (превращение одного товара в другой).
Пример: белая гипсофила → крашеная гипсофила.

Особенности реализации:
- Резервирование входных товаров в статусе 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:
2025-12-25 18:27:31 +03:00
parent 56850e790e
commit 30ee077963
12 changed files with 1682 additions and 4 deletions

View File

@@ -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}"