from decimal import Decimal from django import forms from django.core.exceptions import ValidationError from shop.models import ProductPrice, ProductVariant, Tax def price_without_tax(price_with_tax, tax): """El staff introduce el precio con impuestos (lo que paga el cliente); ProductPrice.price se guarda sin impuestos, que es lo que usa el resto de la aplicación (carritos, pedidos, shop/utils.py) para calcular bases imponibles/impuestos. ProductPrice.save() recalcula price_with_tax a partir de este valor, así que no hace falta guardarlo aquí.""" return round(price_with_tax / (1 + Decimal(tax.value) / 100), 2) class ProductInitialPriceForm(forms.Form): """Precio opcional que se puede fijar al crear un producto, junto con el resto de datos, en el mismo envío (ver backoffice/views/products.py:ProductCreateView).""" price = forms.DecimalField( max_digits=11, decimal_places=2, required=False, label='Precio con impuestos', widget=forms.NumberInput( attrs={'class': 'input input-bordered w-full', 'step': '0.01', 'placeholder': 'Precio con impuestos'} ), ) tax = forms.ModelChoiceField( queryset=Tax.objects.all(), required=False, widget=forms.Select(attrs={'class': 'select select-bordered w-full'}), ) def clean(self): cleaned_data = super().clean() if cleaned_data.get('price') is not None and not cleaned_data.get('tax'): raise ValidationError('Selecciona un impuesto para el precio.') if cleaned_data.get('tax') and cleaned_data.get('price') is None: raise ValidationError('Indica un precio para el impuesto seleccionado.') return cleaned_data def get_price_without_tax(self): return price_without_tax(self.cleaned_data['price'], self.cleaned_data['tax']) class ProductPriceForm(forms.ModelForm): """El campo `price` del formulario representa el precio CON impuestos (lo que introduce el staff); al guardar se convierte al precio sin impuestos, que es lo que almacena ProductPrice.price.""" price = forms.DecimalField( max_digits=11, decimal_places=2, label='Precio con impuestos', widget=forms.NumberInput(attrs={'step': '0.01'}), ) class Meta: model = ProductPrice fields = ('price', 'tax', 'current') def save(self, commit=True): self.instance.price = price_without_tax(self.cleaned_data['price'], self.cleaned_data['tax']) return super().save(commit=commit) class ProductVariantForm(forms.ModelForm): class Meta: model = ProductVariant fields = ('sku', 'stock', 'attribute_values') widgets = {'attribute_values': forms.CheckboxSelectMultiple} def __init__(self, *args, product=None, **kwargs): super().__init__(*args, **kwargs) self.product = product or self.instance.product def clean(self): cleaned_data = super().clean() attribute_values = cleaned_data.get('attribute_values') if attribute_values is not None: combination = set(attribute_values.values_list('pk', flat=True)) siblings = ProductVariant.objects.filter(product=self.product).exclude(pk=self.instance.pk) for sibling in siblings: if set(sibling.attribute_values.values_list('pk', flat=True)) == combination: raise ValidationError( 'Ya existe una variante de este producto con la misma combinación de atributos.' ) return cleaned_data def save(self, commit=True): self.instance.product = self.product return super().save(commit=commit)