Добавлена система трансформации товаров
Реализована полная система трансформации товаров (превращение одного товара в другой). Пример: белая гипсофила → крашеная гипсофила. Особенности реализации: - Резервирование входных товаров в статусе 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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user