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

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

Особенности реализации:
- Резервирование входных товаров в статусе 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

@@ -6,7 +6,7 @@ from decimal import Decimal
from .models import (
Warehouse, Incoming, Sale, WriteOff, Transfer, Reservation, Inventory, InventoryLine, StockBatch,
TransferBatch, TransferItem, Showcase, WriteOffDocument, WriteOffDocumentItem, Stock,
IncomingDocument, IncomingDocumentItem
IncomingDocument, IncomingDocumentItem, Transformation, TransformationInput, TransformationOutput
)
from products.models import Product
@@ -650,3 +650,103 @@ class IncomingDocumentItemForm(forms.ModelForm):
raise ValidationError('Закупочная цена не может быть отрицательной')
return cost_price
# ==================== TRANSFORMATION FORMS ====================
class TransformationForm(forms.ModelForm):
"""Форма для создания документа трансформации"""
class Meta:
model = Transformation
fields = ['warehouse', 'comment']
widgets = {
'warehouse': forms.Select(attrs={'class': 'form-select'}),
'comment': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Комментарий (необязательно)'
}),
}
class TransformationInputForm(forms.Form):
"""Форма для добавления входного товара в трансформацию"""
product = forms.ModelChoiceField(
queryset=Product.objects.filter(status='active').order_by('name'),
label='Товар (что списываем)',
widget=forms.Select(attrs={
'class': 'form-select',
'id': 'id_input_product'
})
)
quantity = forms.DecimalField(
label='Количество',
min_value=Decimal('0.001'),
max_digits=10,
decimal_places=3,
widget=forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.001',
'placeholder': '0.000',
'id': 'id_input_quantity'
})
)
def __init__(self, *args, **kwargs):
self.transformation = kwargs.pop('transformation', None)
super().__init__(*args, **kwargs)
def clean(self):
cleaned_data = super().clean()
product = cleaned_data.get('product')
quantity = cleaned_data.get('quantity')
if product and quantity:
# Проверяем что товар еще не добавлен
if self.transformation and self.transformation.inputs.filter(product=product).exists():
raise ValidationError(f'Товар "{product.name}" уже добавлен в качестве входного')
return cleaned_data
class TransformationOutputForm(forms.Form):
"""Форма для добавления выходного товара в трансформацию"""
product = forms.ModelChoiceField(
queryset=Product.objects.filter(status='active').order_by('name'),
label='Товар (что получаем)',
widget=forms.Select(attrs={
'class': 'form-select',
'id': 'id_output_product'
})
)
quantity = forms.DecimalField(
label='Количество',
min_value=Decimal('0.001'),
max_digits=10,
decimal_places=3,
widget=forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.001',
'placeholder': '0.000',
'id': 'id_output_quantity'
})
)
def __init__(self, *args, **kwargs):
self.transformation = kwargs.pop('transformation', None)
super().__init__(*args, **kwargs)
def clean(self):
cleaned_data = super().clean()
product = cleaned_data.get('product')
quantity = cleaned_data.get('quantity')
if product and quantity:
# Проверяем что товар еще не добавлен
if self.transformation and self.transformation.outputs.filter(product=product).exists():
raise ValidationError(f'Товар "{product.name}" уже добавлен в качестве выходного')
return cleaned_data