# -*- coding: utf-8 -*- from django import forms from django.core.exceptions import ValidationError from decimal import Decimal from .models import ( Warehouse, Incoming, Sale, WriteOff, Transfer, Reservation, Inventory, InventoryLine, StockBatch, TransferBatch, TransferItem, Showcase, WriteOffDocument, WriteOffDocumentItem, Stock, IncomingDocument, IncomingDocumentItem, Transformation, TransformationInput, TransformationOutput ) from products.models import Product class WarehouseForm(forms.ModelForm): class Meta: model = Warehouse fields = [ 'name', 'description', 'street', 'building_number', 'phone', 'email', 'is_active', 'is_default', 'is_pickup_point' ] widgets = { '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': 'Можно ли выбрать этот склад как точку самовывоза заказа', } class SaleForm(forms.ModelForm): class Meta: model = Sale fields = ['product', 'warehouse', 'quantity', 'sale_price', 'order', 'document_number'] widgets = { 'product': forms.Select(attrs={'class': 'form-control'}), 'warehouse': forms.Select(attrs={'class': 'form-control'}), 'quantity': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}), 'sale_price': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}), 'order': forms.Select(attrs={'class': 'form-control'}), 'document_number': forms.TextInput(attrs={'class': 'form-control'}), } def clean_quantity(self): quantity = self.cleaned_data.get('quantity') if quantity and quantity <= 0: raise ValidationError('Количество должно быть больше нуля') return quantity def clean_sale_price(self): sale_price = self.cleaned_data.get('sale_price') if sale_price and sale_price < 0: raise ValidationError('Цена не может быть отрицательной') return sale_price class WriteOffForm(forms.ModelForm): class Meta: model = WriteOff fields = ['batch', 'quantity', 'reason', 'document_number', 'notes'] widgets = { 'batch': forms.Select(attrs={'class': 'form-control'}), 'quantity': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}), 'reason': forms.Select(attrs={'class': 'form-control'}), 'document_number': forms.TextInput(attrs={'class': 'form-control'}), 'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Фильтруем партии - показываем только активные self.fields['batch'].queryset = StockBatch.objects.filter( is_active=True ).select_related('product', 'warehouse').order_by('-created_at') def clean(self): cleaned_data = super().clean() batch = cleaned_data.get('batch') quantity = cleaned_data.get('quantity') if batch and quantity: if quantity > batch.quantity: raise ValidationError( f'Невозможно списать {quantity} шт из партии, ' f'где только {batch.quantity} шт. ' f'Недостаток: {quantity - batch.quantity} шт.' ) if quantity <= 0: raise ValidationError('Количество должно быть больше нуля') return cleaned_data class InventoryForm(forms.ModelForm): class Meta: model = Inventory fields = ['warehouse', 'notes'] widgets = { 'warehouse': forms.Select(attrs={'class': 'form-control'}), 'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Фильтруем только активные склады (исключаем скрытые) self.fields['warehouse'].queryset = Warehouse.objects.filter(is_active=True) # Если есть склад по умолчанию и значение не установлено явно - предвыбираем его if not self.initial.get('warehouse'): default_warehouse = Warehouse.objects.filter( is_active=True, is_default=True ).first() if default_warehouse: self.initial['warehouse'] = default_warehouse.id class InventoryLineForm(forms.ModelForm): class Meta: model = InventoryLine fields = ['product', 'quantity_system', 'quantity_fact'] widgets = { 'product': forms.Select(attrs={'class': 'form-control'}), 'quantity_system': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001', 'readonly': True}), 'quantity_fact': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}), } def clean_quantity_fact(self): quantity_fact = self.cleaned_data.get('quantity_fact') if quantity_fact and quantity_fact < 0: raise ValidationError('Количество не может быть отрицательным') return quantity_fact # ============================================================================ # INCOMING FORMS - Ввод товаров (один или много) от одного поставщика # ============================================================================ class IncomingHeaderForm(forms.Form): """ Форма для общей информации при приходе товаров. Используется для ввода информации об источнике поступления (склад, номер документа, поставщик). """ warehouse = forms.ModelChoiceField( queryset=Warehouse.objects.filter(is_active=True), widget=forms.Select(attrs={'class': 'form-control'}), label="Склад", required=True ) document_number = forms.CharField( max_length=100, widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'PO-2024-001 (опционально)'}), label="Номер документа / ПО", required=False ) supplier_name = forms.CharField( max_length=200, widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'ООО Поставщик'}), label="Наименование поставщика", required=False ) notes = forms.CharField( widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Дополнительная информация'}), label="Примечания", required=False ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Если есть склад по умолчанию и значение не установлено явно - предвыбираем его if not self.initial.get('warehouse'): default_warehouse = Warehouse.objects.filter( is_active=True, is_default=True ).first() if default_warehouse: self.initial['warehouse'] = default_warehouse.id def clean_document_number(self): document_number = self.cleaned_data.get('document_number', '') if document_number: document_number = document_number.strip() # Запретить номера, начинающиеся с "IN-" (зарезервировано для системы) if document_number.upper().startswith('IN-'): raise ValidationError( 'Номера, начинающиеся с "IN-", зарезервированы для системы автогенерации. ' 'Оставьте поле пустым для автогенерации или используйте другой формат.' ) return document_number class IncomingLineForm(forms.Form): """ Форма для одной строки товара при массовом приходе. Используется в formset'е для динамического ввода нескольких товаров. """ product = forms.ModelChoiceField( queryset=None, # Будет установлено в __init__ widget=forms.Select(attrs={'class': 'form-control'}), label="Товар", required=True ) quantity = forms.DecimalField( max_digits=10, decimal_places=3, widget=forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}), label="Количество", required=True ) cost_price = forms.DecimalField( max_digits=10, decimal_places=2, widget=forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}), label="Цена закупки за ед.", required=True ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Устанавливаем queryset товаров для поля product 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 class IncomingForm(forms.Form): """ Комбинированная форма для ввода товаров (один или много). Содержит header информацию (склад, документ, поставщик) + динамический набор товаров. """ warehouse = forms.ModelChoiceField( queryset=Warehouse.objects.filter(is_active=True), widget=forms.Select(attrs={'class': 'form-control'}), label="Склад", required=True ) document_number = forms.CharField( max_length=100, widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'PO-2024-001 (опционально)'}), label="Номер документа / ПО", required=False ) receipt_type = forms.CharField( max_length=20, widget=forms.HiddenInput(), initial='supplier', required=False ) supplier_name = forms.CharField( max_length=200, widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'ООО Поставщик'}), label="Наименование поставщика (опционально)", required=False ) notes = forms.CharField( widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Дополнительная информация'}), label="Примечания", required=False ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Если есть склад по умолчанию и значение не установлено явно - предвыбираем его if not self.initial.get('warehouse'): default_warehouse = Warehouse.objects.filter( is_active=True, is_default=True ).first() if default_warehouse: self.initial['warehouse'] = default_warehouse.id def clean_document_number(self): document_number = self.cleaned_data.get('document_number', '') if document_number: document_number = document_number.strip() # Запретить номера, начинающиеся с "IN-" (зарезервировано для системы) if document_number.upper().startswith('IN-'): raise ValidationError( 'Номера, начинающиеся с "IN-", зарезервированы для системы автогенерации. ' 'Оставьте поле пустым для автогенерации или используйте другой формат.' ) 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( status='active' ).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 - Перемещение товаров между складами # ============================================================================ class TransferHeaderForm(forms.ModelForm): """ Форма заголовка документа перемещения товара между складами. Содержит информацию о складах-источнике и складе-назначении, примечания. """ class Meta: model = TransferBatch fields = ['from_warehouse', 'to_warehouse', 'notes'] widgets = { 'from_warehouse': forms.Select(attrs={'class': 'form-control'}), 'to_warehouse': forms.Select(attrs={'class': 'form-control'}), 'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Примечания к перемещению'}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Фильтруем только активные склады self.fields['from_warehouse'].queryset = Warehouse.objects.filter(is_active=True) self.fields['to_warehouse'].queryset = Warehouse.objects.filter(is_active=True) def clean(self): cleaned_data = super().clean() from_warehouse = cleaned_data.get('from_warehouse') to_warehouse = cleaned_data.get('to_warehouse') if from_warehouse and to_warehouse: if from_warehouse.id == to_warehouse.id: raise ValidationError('Склад-источник и склад-назначение должны быть разными') return cleaned_data class TransferLineForm(forms.Form): """ Форма для одной строки товара при массовом перемещении. Используется в динамической таблице для ввода нескольких товаров. """ product = forms.ModelChoiceField( queryset=Product.objects.filter(status='active').order_by('name'), widget=forms.Select(attrs={'class': 'form-control'}), label="Товар", required=True ) quantity = forms.DecimalField( max_digits=10, decimal_places=3, widget=forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}), label="Количество", required=True ) def clean_quantity(self): quantity = self.cleaned_data.get('quantity') if quantity and quantity <= 0: raise ValidationError('Количество должно быть больше нуля') return quantity class TransferBulkForm(forms.Form): """ Комбинированная форма для ввода перемещения товаров. Содержит header информацию (склад-источник, склад-назначение, примечания) + динамический набор товаров. """ from_warehouse = forms.ModelChoiceField( queryset=Warehouse.objects.filter(is_active=True), widget=forms.Select(attrs={'class': 'form-control'}), label="Склад-отгрузки", required=True ) to_warehouse = forms.ModelChoiceField( queryset=Warehouse.objects.filter(is_active=True), widget=forms.Select(attrs={'class': 'form-control'}), label="Склад-приемки", required=True ) notes = forms.CharField( widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Примечания к перемещению'}), label="Примечания", required=False ) def clean(self): cleaned_data = super().clean() from_warehouse = cleaned_data.get('from_warehouse') to_warehouse = cleaned_data.get('to_warehouse') if from_warehouse and to_warehouse: if from_warehouse.id == to_warehouse.id: raise ValidationError('Склад-источник и склад-назначение должны быть разными') return cleaned_data # ============================================================================ # WRITEOFF DOCUMENT FORMS - Документы списания # ============================================================================ class WriteOffDocumentForm(forms.ModelForm): """ Форма создания/редактирования документа списания. """ class Meta: model = WriteOffDocument fields = ['warehouse', 'date', 'notes'] widgets = { 'warehouse': forms.Select(attrs={'class': 'form-control'}), 'date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}), 'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Примечания к документу'}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['warehouse'].queryset = Warehouse.objects.filter(is_active=True) # Устанавливаем дату по умолчанию - сегодня if not self.initial.get('date'): from django.utils import timezone self.initial['date'] = timezone.now().date() # Если есть склад по умолчанию - предвыбираем его if not self.initial.get('warehouse'): default_warehouse = Warehouse.objects.filter( is_active=True, is_default=True ).first() if default_warehouse: self.initial['warehouse'] = default_warehouse.id class WriteOffDocumentItemForm(forms.ModelForm): """ Форма добавления/редактирования позиции в документ списания. """ class Meta: model = WriteOffDocumentItem fields = ['product', 'quantity', 'reason', 'notes'] widgets = { 'product': forms.Select(attrs={'class': 'form-control'}), 'quantity': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001', 'min': '0.001'}), 'reason': forms.Select(attrs={'class': 'form-control'}), 'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 2, 'placeholder': 'Примечания'}), } def __init__(self, *args, document=None, **kwargs): super().__init__(*args, **kwargs) self.document = document if document: # Фильтруем товары - только те, что есть на складе products_with_stock = Stock.objects.filter( warehouse=document.warehouse, quantity_available__gt=0 ).values_list('product_id', flat=True) self.fields['product'].queryset = Product.objects.filter( id__in=products_with_stock, status='active' ).order_by('name') else: self.fields['product'].queryset = Product.objects.filter( status='active' ).order_by('name') def clean_quantity(self): quantity = self.cleaned_data.get('quantity') if quantity and quantity <= 0: raise ValidationError('Количество должно быть больше нуля') return quantity def clean(self): cleaned_data = super().clean() product = cleaned_data.get('product') quantity = cleaned_data.get('quantity') if product and quantity and self.document: stock = Stock.objects.filter( product=product, warehouse=self.document.warehouse ).first() if not stock: raise ValidationError({ 'product': f'Товар "{product.name}" отсутствует на складе' }) available = stock.quantity_available - stock.quantity_reserved # Учитываем текущий резерв при редактировании if self.instance.pk and self.instance.reservation: available += self.instance.reservation.quantity if quantity > available: raise ValidationError({ 'quantity': f'Недостаточно свободного товара. ' f'Доступно: {available}, запрашивается: {quantity}' }) return cleaned_data class IncomingDocumentForm(forms.ModelForm): """ Форма создания/редактирования документа поступления. """ class Meta: model = IncomingDocument fields = ['warehouse', 'date', 'receipt_type', 'supplier_name', 'notes'] widgets = { 'warehouse': forms.Select(attrs={'class': 'form-control'}), 'date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}), 'receipt_type': forms.Select(attrs={'class': 'form-control'}), 'supplier_name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Наименование поставщика'}), 'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Примечания к документу'}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['warehouse'].queryset = Warehouse.objects.filter(is_active=True) # Устанавливаем дату по умолчанию - сегодня if not self.initial.get('date'): from django.utils import timezone self.initial['date'] = timezone.now().date() # Если есть склад по умолчанию - предвыбираем его if not self.initial.get('warehouse'): default_warehouse = Warehouse.objects.filter( is_active=True, is_default=True ).first() if default_warehouse: self.initial['warehouse'] = default_warehouse.id # Устанавливаем тип поступления по умолчанию if not self.initial.get('receipt_type'): self.initial['receipt_type'] = 'supplier' def clean(self): cleaned_data = super().clean() receipt_type = cleaned_data.get('receipt_type') supplier_name = cleaned_data.get('supplier_name') # Для типа 'supplier' supplier_name обязателен if receipt_type == 'supplier' and not supplier_name: raise ValidationError({ 'supplier_name': 'Для типа "Поступление от поставщика" необходимо указать наименование поставщика' }) # Для других типов supplier_name не нужен if receipt_type != 'supplier' and supplier_name: cleaned_data['supplier_name'] = None return cleaned_data class IncomingDocumentItemForm(forms.ModelForm): """ Форма добавления/редактирования позиции в документ поступления. """ class Meta: model = IncomingDocumentItem fields = ['product', 'quantity', 'cost_price', 'notes'] widgets = { 'product': forms.Select(attrs={'class': 'form-control'}), 'quantity': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001', 'min': '0.001'}), 'cost_price': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}), 'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 2, 'placeholder': 'Примечания'}), } def __init__(self, *args, document=None, **kwargs): super().__init__(*args, **kwargs) self.document = document # Для поступлений можно выбрать любой активный товар (не нужно проверять наличие на складе) self.fields['product'].queryset = Product.objects.filter( status='active' ).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 is not None and cost_price < 0: 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