# -*- 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 from products.models import Product class WarehouseForm(forms.ModelForm): class Meta: model = Warehouse fields = ['name', 'description', 'is_active'] widgets = { 'name': forms.TextInput(attrs={'class': 'form-control'}), 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}), 'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}), } 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 TransferForm(forms.ModelForm): class Meta: model = Transfer fields = ['batch', 'from_warehouse', 'to_warehouse', 'quantity', 'document_number'] widgets = { 'batch': forms.Select(attrs={'class': 'form-control'}), 'from_warehouse': forms.Select(attrs={'class': 'form-control'}), 'to_warehouse': forms.Select(attrs={'class': 'form-control'}), 'quantity': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}), 'document_number': forms.TextInput(attrs={'class': 'form-control'}), } def clean(self): cleaned_data = super().clean() batch = cleaned_data.get('batch') quantity = cleaned_data.get('quantity') from_warehouse = cleaned_data.get('from_warehouse') if batch and quantity: if quantity > batch.quantity: raise ValidationError( f'Невозможно перенести {quantity} шт, доступно {batch.quantity} шт' ) if quantity <= 0: raise ValidationError('Количество должно быть больше нуля') # Проверяем что складской источник совпадает с складом партии if from_warehouse and batch.warehouse_id != from_warehouse.id: raise ValidationError( f'Партия находится на складе "{batch.warehouse.name}", ' f'а вы выбрали "{from_warehouse.name}"' ) return cleaned_data class ReservationForm(forms.ModelForm): class Meta: model = Reservation fields = ['product', 'warehouse', 'quantity', 'order_item'] 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'}), 'order_item': forms.Select(attrs={'class': 'form-control'}), } def clean_quantity(self): quantity = self.cleaned_data.get('quantity') if quantity and quantity <= 0: raise ValidationError('Количество должно быть больше нуля') return quantity class InventoryForm(forms.ModelForm): class Meta: model = Inventory fields = ['warehouse', 'conducted_by', 'notes'] widgets = { 'warehouse': forms.Select(attrs={'class': 'form-control'}), 'conducted_by': forms.TextInput(attrs={'class': 'form-control'}), 'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), } 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 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 ) 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 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