# -*- coding: utf-8 -*- from django import forms from django.forms import inlineformset_factory from .models import Order, OrderItem, Address from customers.models import Customer from shops.models import Shop from products.models import Product, ProductKit class OrderForm(forms.ModelForm): """Форма для создания и редактирования заказа""" # Поля для ввода адреса address_mode = forms.ChoiceField( choices=[ ('history', 'Выбрать из истории'), ('new', 'Ввести новый адрес'), ('empty', 'Без адреса (заполнить позже)'), ], initial='empty', widget=forms.RadioSelect(attrs={'class': 'form-check-input'}), required=False, label='Способ указания адреса' ) # Выбор адреса из истории address_from_history = forms.ModelChoiceField( queryset=Address.objects.none(), required=False, widget=forms.Select(attrs={'class': 'form-select'}), label='Адрес из истории' ) # Поля для ввода нового адреса address_street = forms.CharField( max_length=255, required=False, widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Улица'}), label='Улица' ) address_building_number = forms.CharField( max_length=20, required=False, widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Номер дома'}), label='Номер дома' ) address_apartment_number = forms.CharField( max_length=20, required=False, widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Квартира/офис'}), label='Квартира/офис' ) address_entrance = forms.CharField( max_length=20, required=False, widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Подъезд'}), label='Подъезд' ) address_floor = forms.CharField( max_length=20, required=False, widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Этаж'}), label='Этаж' ) address_intercom_code = forms.CharField( max_length=100, required=False, widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Код домофона'}), label='Код домофона' ) address_delivery_instructions = forms.CharField( required=False, widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Инструкции для курьера'}), label='Инструкции для доставки' ) address_confirm_with_recipient = forms.BooleanField( required=False, widget=forms.CheckboxInput(attrs={'class': 'form-check-input'}), label='Уточнить адрес у получателя' ) class Meta: model = Order fields = [ 'customer', 'is_delivery', 'delivery_address', 'pickup_shop', 'delivery_date', 'delivery_time_start', 'delivery_time_end', 'delivery_cost', 'customer_is_recipient', 'recipient_name', 'recipient_phone', 'status', 'payment_method', 'discount_amount', 'is_anonymous', 'special_instructions', ] widgets = { 'delivery_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'), 'delivery_time_start': forms.TimeInput(attrs={'type': 'time'}, format='%H:%M'), 'delivery_time_end': forms.TimeInput(attrs={'type': 'time'}, format='%H:%M'), 'special_instructions': forms.Textarea(attrs={'rows': 3}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Добавляем Bootstrap классы ко всем полям for field_name, field in self.fields.items(): if isinstance(field.widget, forms.CheckboxInput): field.widget.attrs.update({'class': 'form-check-input'}) elif isinstance(field.widget, forms.Textarea): field.widget.attrs.update({'class': 'form-control', 'rows': 3}) elif isinstance(field.widget, forms.RadioSelect): # RadioSelect не нуждается в доп классах (уже есть form-check-input) pass elif isinstance(field.widget, forms.Select): # Select поля получают form-select field.widget.attrs.update({'class': 'form-select'}) else: # Остальные поля (TextInput, NumberInput, etc) field.widget.attrs.update({'class': 'form-control'}) # Select2 для поля customer с AJAX поиском (инициализируется отдельно в JS) # Django автоматически генерирует ID как id_customer self.fields['customer'].widget.attrs.update({ 'class': 'form-select', 'data-placeholder': 'Начните вводить имя, телефон или email' }) self.fields['delivery_address'].widget.attrs.update({ 'class': 'form-select select2', 'data-placeholder': 'Выберите адрес доставки' }) self.fields['delivery_address'].required = False self.fields['pickup_shop'].widget.attrs.update({ 'class': 'form-select select2', 'data-placeholder': 'Выберите точку самовывоза' }) self.fields['pickup_shop'].required = False # Опциональные поля даты/времени self.fields['delivery_date'].required = False self.fields['delivery_time_start'].required = False self.fields['delivery_time_end'].required = False # Подсказки self.fields['is_delivery'].label = 'С доставкой' self.fields['customer_is_recipient'].label = 'Покупатель = получатель' # Поля получателя опциональны self.fields['recipient_name'].required = False self.fields['recipient_phone'].required = False # Поле ручной стоимости доставки опционально self.fields['delivery_cost'].required = False self.fields['delivery_cost'].label = 'Ручная стоимость доставки' self.fields['delivery_cost'].help_text = 'Оставьте пустым для автоматического расчета' # Инициализируем queryset для address_from_history # Это будет переопределено в представлении после выбора клиента if self.instance.pk and self.instance.customer: # При редактировании заказа загружаем историю адресов этого клиента customer_orders = Order.objects.filter( customer=self.instance.customer, delivery_address__isnull=False ).order_by('-created_at') self.fields['address_from_history'].queryset = Address.objects.filter( order__in=customer_orders ).distinct().order_by('-created_at') def save(self, commit=True): """ Сохраняет форму с учетом автоматического/ручного расчета стоимости доставки. Логика: - Если delivery_cost заполнено → используется ручное значение (is_custom_delivery_cost = True) - Если delivery_cost пустое → автоматический расчет (is_custom_delivery_cost = False) """ instance = super().save(commit=False) # Получаем значение ручной стоимости доставки delivery_cost = self.cleaned_data.get('delivery_cost') if delivery_cost is not None and delivery_cost > 0: # Ручное значение указано instance.set_delivery_cost(delivery_cost, is_custom=True) else: # Пустое поле или 0 → автоматический расчет instance.reset_delivery_cost() if commit: instance.save() return instance class OrderItemForm(forms.ModelForm): """Форма для позиции заказа""" class Meta: model = OrderItem fields = ['product', 'product_kit', 'quantity', 'price', 'is_custom_price'] widgets = { 'quantity': forms.NumberInput(attrs={'min': 1, 'value': 1}), # Скрываем поля product и product_kit - они будут заполняться через JS 'product': forms.HiddenInput(), 'product_kit': forms.HiddenInput(), 'is_custom_price': forms.HiddenInput(), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Bootstrap классы for field_name, field in self.fields.items(): if not isinstance(field.widget, forms.HiddenInput): field.widget.attrs.update({'class': 'form-control'}) # Поля product и product_kit опциональны self.fields['product'].required = False self.fields['product_kit'].required = False # Поле цены заполняется автоматически, но можно редактировать вручную self.fields['price'].widget.attrs.update({ 'placeholder': 'Цена', 'step': '0.01' }) self.fields['price'].required = False # Поле is_custom_price устанавливается через JS self.fields['is_custom_price'].required = False def clean(self): """Валидация: должен быть выбран либо товар, либо комплект (не оба, не ни один)""" cleaned_data = super().clean() product = cleaned_data.get('product') product_kit = cleaned_data.get('product_kit') quantity = cleaned_data.get('quantity') # Пустая форма - это нормально (будет удалена) if not product and not product_kit: # Обнуляем количество для пустых форм cleaned_data['quantity'] = None return cleaned_data # Проверка: нельзя выбрать оба одновременно if product and product_kit: raise forms.ValidationError( 'Нельзя указывать одновременно товар и комплект. Выберите что-то одно.' ) # Проверка: если выбрано что-то, количество обязательно if (product or product_kit): if not quantity or quantity <= 0: raise forms.ValidationError('Необходимо указать количество больше 0') return cleaned_data # Formset для inline добавления товаров в заказ OrderItemFormSet = inlineformset_factory( Order, OrderItem, form=OrderItemForm, extra=0, # Без пустых форм (будем добавлять через JavaScript) can_delete=True, min_num=0, # Минимум 0 товаров (валидация на уровне бизнес-логики) validate_min=False, ) # === ВРЕМЕННЫЕ КОМПЛЕКТЫ === class TemporaryKitForm(forms.ModelForm): """ Упрощенная форма для создания временного комплекта. Используется при оформлении заказа для создания букета "на лету". """ class Meta: model = ProductKit fields = ['name', 'description'] widgets = { 'description': forms.Textarea(attrs={'rows': 2, 'placeholder': 'Краткое описание (опционально)'}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Bootstrap классы for field in self.fields.values(): if isinstance(field.widget, forms.Textarea): field.widget.attrs.update({'class': 'form-control'}) else: field.widget.attrs.update({'class': 'form-control'}) # Название обязательно self.fields['name'].required = True self.fields['name'].widget.attrs.update({ 'placeholder': 'Название временного букета (например: "Букет для Анны")' }) # Описание опционально self.fields['description'].required = False class TemporaryKitItemForm(forms.Form): """ Форма для компонента временного комплекта. Используется в формсете для добавления товаров в букет. """ product = forms.IntegerField(required=False, widget=forms.HiddenInput()) quantity = forms.DecimalField( required=False, min_value=0.001, max_digits=10, decimal_places=3, widget=forms.NumberInput(attrs={'class': 'form-control', 'min': '0.001', 'step': '1'}) ) def clean(self): cleaned_data = super().clean() product_id = cleaned_data.get('product') quantity = cleaned_data.get('quantity') # Пустая форма - это нормально (будет удалена) if not product_id: return cleaned_data # Если выбран товар, количество обязательно if product_id and (not quantity or quantity <= 0): raise forms.ValidationError('Необходимо указать количество больше 0') return cleaned_data # Formset для компонентов временного комплекта from django.forms import formset_factory TemporaryKitItemFormSet = formset_factory( TemporaryKitItemForm, extra=1, # Одна пустая форма для добавления can_delete=True, min_num=1, # Минимум 1 компонент в комплекте validate_min=True, )