feat: Add signal handler for synchronizing Incoming edits with StockBatch
## Changes ### 1. Fixed missing signal handler for Incoming edit (inventory/signals.py) - Added new signal handler `update_stock_batch_on_incoming_edit()` that: - Triggers when Incoming is edited (created=False) - Synchronizes StockBatch with new quantity and cost_price values - Automatically triggers cost price recalculation for the product - Updates Stock (inventory balance) for the warehouse - Includes proper logging and error handling ### 2. Created IncomingModelForm for editing individual incoming items (inventory/forms.py) - New ModelForm: `IncomingModelForm` that: - Inherits from forms.ModelForm (accepts 'instance' parameter required by UpdateView) - Allows editing: product, quantity, cost_price, notes - Includes validation for positive quantity and non-negative cost_price - Filters only active products ### 3. Updated IncomingUpdateView (inventory/views/incoming.py) - Changed form_class from IncomingForm to IncomingModelForm - Updated imports to include IncomingModelForm - Removed obsolete comments from form_valid method ## Architecture When editing an Incoming item: 1. User submits form with new quantity/cost_price 2. form.save() triggers post_save signal (created=False) 3. update_stock_batch_on_incoming_edit() synchronizes StockBatch 4. StockBatch.save() triggers update_product_cost_on_batch_change() 5. Product.cost_price is recalculated with weighted average ## Problem Solved Previously, editing an Incoming item would NOT: - Update the related StockBatch - Recalculate product cost_price - Update warehouse inventory balance - Maintain data consistency between Incoming and StockBatch Now all these operations happen automatically through the signal chain. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -13,12 +13,31 @@ from products.models import Product
|
||||
class WarehouseForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Warehouse
|
||||
fields = ['name', 'description', 'is_active', 'is_default']
|
||||
fields = [
|
||||
'name',
|
||||
'description',
|
||||
'street',
|
||||
'building_number',
|
||||
'phone',
|
||||
'email',
|
||||
'is_active',
|
||||
'is_default',
|
||||
'is_pickup_point'
|
||||
]
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}),
|
||||
'name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Название склада'}),
|
||||
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Описание'}),
|
||||
'street': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Улица'}),
|
||||
'building_number': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Номер дома'}),
|
||||
'phone': forms.TextInput(attrs={'class': 'form-control', 'placeholder': '+375 (29) 123-45-67'}),
|
||||
'email': forms.EmailInput(attrs={'class': 'form-control', 'placeholder': 'email@example.com'}),
|
||||
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'is_default': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'is_pickup_point': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
}
|
||||
help_texts = {
|
||||
'is_default': 'Автоматически выбирается при создании новых документов',
|
||||
'is_pickup_point': 'Можно ли выбрать этот склад как точку самовывоза заказа',
|
||||
}
|
||||
|
||||
|
||||
@@ -308,6 +327,41 @@ class IncomingForm(forms.Form):
|
||||
return document_number
|
||||
|
||||
|
||||
class IncomingModelForm(forms.ModelForm):
|
||||
"""
|
||||
ModelForm для редактирования отдельного товара в поступлении (Incoming).
|
||||
Используется в IncomingUpdateView для редактирования существующих товаров.
|
||||
"""
|
||||
class Meta:
|
||||
model = Incoming
|
||||
fields = ['product', 'quantity', 'cost_price', 'notes']
|
||||
widgets = {
|
||||
'product': forms.Select(attrs={'class': 'form-control'}),
|
||||
'quantity': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}),
|
||||
'cost_price': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
|
||||
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Фильтруем только активные товары
|
||||
self.fields['product'].queryset = Product.objects.filter(
|
||||
is_active=True
|
||||
).order_by('name')
|
||||
|
||||
def clean_quantity(self):
|
||||
quantity = self.cleaned_data.get('quantity')
|
||||
if quantity and quantity <= 0:
|
||||
raise ValidationError('Количество должно быть больше нуля')
|
||||
return quantity
|
||||
|
||||
def clean_cost_price(self):
|
||||
cost_price = self.cleaned_data.get('cost_price')
|
||||
if cost_price and cost_price < 0:
|
||||
raise ValidationError('Цена не может быть отрицательной')
|
||||
return cost_price
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TRANSFER FORMS - Перемещение товаров между складами
|
||||
# ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user