diff --git a/myproject/inventory/forms.py b/myproject/inventory/forms.py index a6ac3a8..948fbef 100644 --- a/myproject/inventory/forms.py +++ b/myproject/inventory/forms.py @@ -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 - Перемещение товаров между складами # ============================================================================ diff --git a/myproject/inventory/signals.py b/myproject/inventory/signals.py index 24e4ff4..46da24e 100644 --- a/myproject/inventory/signals.py +++ b/myproject/inventory/signals.py @@ -232,6 +232,81 @@ def create_stock_batch_on_incoming(sender, instance, created, **kwargs): stock.refresh_from_batches() +@receiver(post_save, sender=Incoming) +def update_stock_batch_on_incoming_edit(sender, instance, created, **kwargs): + """ + Сигнал: При редактировании товара в приходе (Incoming) автоматически + обновляется связанная партия товара на складе (StockBatch). + + Это обеспечивает синхронизацию данных между Incoming и StockBatch. + + Архитектура: + - Если Incoming редактируется - обновляем StockBatch с новыми значениями + - Обновление StockBatch автоматически пересчитывает себестоимость товара (Product.cost_price) + через сигнал update_product_cost_on_batch_change() + + Процесс: + 1. Проверяем, это редактирование (created=False), а не создание + 2. Получаем связанный StockBatch + 3. Проверяем, изменились ли quantity или cost_price + 4. Если да - обновляем StockBatch + 5. Сохраняем StockBatch (запускает цепь пересчета себестоимости) + 6. Обновляем остатки на складе (Stock) + """ + if created: + return # Только для редактирования (не для создания) + + # Получаем связанный StockBatch + if not instance.stock_batch: + return # Если нет связи со StockBatch - нечего обновлять + + stock_batch = instance.stock_batch + + import logging + logger = logging.getLogger(__name__) + + try: + # Проверяем, отличаются ли значения в StockBatch от Incoming + # Это говорит нам о том, что произошло редактирование + needs_update = ( + stock_batch.quantity != instance.quantity or + stock_batch.cost_price != instance.cost_price + ) + + if not needs_update: + return # Никаких изменений + + # Обновляем StockBatch с новыми значениями из Incoming + stock_batch.quantity = instance.quantity + stock_batch.cost_price = instance.cost_price + stock_batch.save() + + logger.info( + f"✓ StockBatch #{stock_batch.id} обновлён при редактировании Incoming: " + f"quantity={instance.quantity}, cost_price={instance.cost_price} " + f"(товар: {instance.product.sku})" + ) + + # Обновляем Stock (остатки на складе) + warehouse = stock_batch.warehouse + stock, _ = Stock.objects.get_or_create( + product=instance.product, + warehouse=warehouse + ) + stock.refresh_from_batches() + + logger.info( + f"✓ Stock обновлён для товара {instance.product.sku} " + f"на складе {warehouse.name}" + ) + + except Exception as e: + logger.error( + f"Ошибка при обновлении StockBatch при редактировании Incoming #{instance.id}: {e}", + exc_info=True + ) + + @receiver(post_save, sender=Sale) def process_sale_fifo(sender, instance, created, **kwargs): """ diff --git a/myproject/inventory/templates/inventory/warehouse/warehouse_form.html b/myproject/inventory/templates/inventory/warehouse/warehouse_form.html index 992c492..da9e6fd 100644 --- a/myproject/inventory/templates/inventory/warehouse/warehouse_form.html +++ b/myproject/inventory/templates/inventory/warehouse/warehouse_form.html @@ -47,7 +47,7 @@ + rows="3">{{ form.description.value|default:'' }} {% if form.description.errors %}
{% for error in form.description.errors %} @@ -57,6 +57,72 @@ {% endif %}
+
Адрес
+ +
+
+ + {{ form.street }} + {% if form.street.errors %} +
+ {% for error in form.street.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+ +
+ + {{ form.building_number }} + {% if form.building_number.errors %} +
+ {% for error in form.building_number.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+
+ +
Контакты
+ +
+
+ + {{ form.phone }} + {% if form.phone.errors %} +
+ {% for error in form.phone.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+ +
+ + {{ form.email }} + {% if form.email.errors %} +
+ {% for error in form.email.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} +
+
+ +
Настройки
+
{{ form.is_default.label }} - Отмечьте, чтобы использовать этот склад по умолчанию при создании новых документов + {{ form.is_default.help_text }} + + +
+
+ +
+
+ +
diff --git a/myproject/inventory/views/incoming.py b/myproject/inventory/views/incoming.py index 992b257..a05bebb 100644 --- a/myproject/inventory/views/incoming.py +++ b/myproject/inventory/views/incoming.py @@ -10,7 +10,7 @@ from django.views.decorators.http import require_http_methods from django.utils.decorators import method_decorator from django.db import IntegrityError, transaction from ..models import Incoming, IncomingBatch, Warehouse -from ..forms import IncomingForm, IncomingLineForm +from ..forms import IncomingForm, IncomingLineForm, IncomingModelForm from inventory.utils import generate_incoming_document_number from products.models import Product @@ -47,13 +47,11 @@ class IncomingUpdateView(LoginRequiredMixin, UpdateView): Обработанные приходы редактировать нельзя. """ model = Incoming - form_class = IncomingForm + form_class = IncomingModelForm template_name = 'inventory/incoming/incoming_form.html' success_url = reverse_lazy('inventory:incoming-list') def form_valid(self, form): - # При редактировании можем оставить номер пустым - модель генерирует при сохранении - # Но это только если объект ещё не имеет номера (новый) messages.success(self.request, f'Приход товара обновлён.') return super().form_valid(form)