From ec15d8ef2e9424a13f4c66043049c841ab51d572 Mon Sep 17 00:00:00 2001 From: Pablo Moreno Date: Thu, 23 Jul 2026 18:36:07 +0200 Subject: [PATCH] fix: responsive --- backoffice/forms.py | 42 +++++++++- backoffice/templates/backoffice/base.html | 77 +++++++++++-------- .../templates/backoffice/dashboard.html | 2 +- .../templates/backoffice/generic/list.html | 2 +- .../backoffice/orders/order_detail.html | 2 +- .../products/_product_detail_fragment.html | 14 ++-- .../backoffice/products/product_create.html | 6 +- backoffice/tests/test_products.py | 15 +++- backoffice/views/products.py | 8 +- 9 files changed, 112 insertions(+), 56 deletions(-) diff --git a/backoffice/forms.py b/backoffice/forms.py index 0308575..bd7431d 100644 --- a/backoffice/forms.py +++ b/backoffice/forms.py @@ -1,7 +1,18 @@ +from decimal import Decimal + from django import forms from django.core.exceptions import ValidationError -from shop.models import ProductVariant, Tax +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): @@ -12,7 +23,10 @@ class ProductInitialPriceForm(forms.Form): max_digits=11, decimal_places=2, required=False, - widget=forms.NumberInput(attrs={'class': 'input input-bordered w-full', 'step': '0.01', 'placeholder': 'Precio'}), + 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(), @@ -31,6 +45,30 @@ class ProductInitialPriceForm(forms.Form): 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: diff --git a/backoffice/templates/backoffice/base.html b/backoffice/templates/backoffice/base.html index 3944685..d36f290 100644 --- a/backoffice/templates/backoffice/base.html +++ b/backoffice/templates/backoffice/base.html @@ -1,4 +1,3 @@ -{% load static %} @@ -9,42 +8,54 @@ - {% block extra_js %} {% endblock %} -
- -
- {% block main %} - {% endblock %} -
+
+ + +
+ + +
+ {% block main %} + {% endblock %} +
+
+ +
+ + +
diff --git a/backoffice/templates/backoffice/dashboard.html b/backoffice/templates/backoffice/dashboard.html index 7bd6015..0e6777f 100644 --- a/backoffice/templates/backoffice/dashboard.html +++ b/backoffice/templates/backoffice/dashboard.html @@ -5,7 +5,7 @@ {% block main %}

Backoffice

-
+
{% for section in sections %}
diff --git a/backoffice/templates/backoffice/generic/list.html b/backoffice/templates/backoffice/generic/list.html index f783512..5639add 100644 --- a/backoffice/templates/backoffice/generic/list.html +++ b/backoffice/templates/backoffice/generic/list.html @@ -3,7 +3,7 @@ {% block title %}{{ title }}{% endblock %} {% block main %} -
+

{{ title }}

{% if create_url %} {% if create_is_page %} diff --git a/backoffice/templates/backoffice/orders/order_detail.html b/backoffice/templates/backoffice/orders/order_detail.html index bf0344f..5f0f110 100644 --- a/backoffice/templates/backoffice/orders/order_detail.html +++ b/backoffice/templates/backoffice/orders/order_detail.html @@ -5,7 +5,7 @@ {% block main %}

Pedido {{ object.code }}

-
+
Cliente
{{ object.email }}
diff --git a/backoffice/templates/backoffice/products/_product_detail_fragment.html b/backoffice/templates/backoffice/products/_product_detail_fragment.html index 5c5b79c..aef59be 100644 --- a/backoffice/templates/backoffice/products/_product_detail_fragment.html +++ b/backoffice/templates/backoffice/products/_product_detail_fragment.html @@ -16,20 +16,20 @@ {% url 'backoffice:product_inline_view' product.pk 'tags' as tags_view_url %} {% url 'backoffice:product_inline_edit' product.pk 'tags' as tags_edit_url %} -
+
{% include 'backoffice/products/_inline_view.html' with field_name='name' view_url=name_view_url edit_url=name_edit_url %}
-
+
{% with images.first as main_image %} -
+
{% if main_image %} - {{ product.name }} + {{ product.name }} {% else %} -
+
Sin imagen
{% endif %} @@ -37,7 +37,7 @@ {% endwith %}
-
+
SKU
{{ product.sku }}
@@ -58,7 +58,7 @@ {% include 'backoffice/products/_inline_view.html' with field_name='description' view_url=description_view_url edit_url=description_edit_url %}
-
+

Marca

{% include 'backoffice/products/_inline_view.html' with field_name='brand' view_url=brand_view_url edit_url=brand_edit_url %} diff --git a/backoffice/templates/backoffice/products/product_create.html b/backoffice/templates/backoffice/products/product_create.html index dac7966..319b41c 100644 --- a/backoffice/templates/backoffice/products/product_create.html +++ b/backoffice/templates/backoffice/products/product_create.html @@ -18,7 +18,7 @@ {{ form.description.errors }}
-
+

Marca

{% include 'backoffice/products/_combobox.html' with field_name='brand' multiple=False %} @@ -33,7 +33,7 @@
-
+
{{ form.sku }} @@ -57,7 +57,7 @@

Precio inicial (opcional)

-
+
{{ price_form.price }} {{ price_form.price.errors }} diff --git a/backoffice/tests/test_products.py b/backoffice/tests/test_products.py index 6c44112..f9eeb47 100644 --- a/backoffice/tests/test_products.py +++ b/backoffice/tests/test_products.py @@ -75,7 +75,9 @@ class TestBackofficeProducts(TestCase, CreateProductsMixin): assert response.status_code == 302 product = Product.objects.get(sku='NEW-3') price = ProductPrice.objects.get(product=product) - assert price.price == Decimal('12.50') + # 12.50 es el precio CON impuestos que introduce el staff; se guarda sin impuestos. + assert price.price == Decimal('10.33') + assert price.price_with_tax == Decimal('12.50') assert price.current is True def test_create_product_with_price_missing_tax_shows_error(self): @@ -123,7 +125,7 @@ class TestBackofficeProducts(TestCase, CreateProductsMixin): old_price.refresh_from_db() assert old_price.current is False - new_price = self.product.prices.get(price=Decimal('25.00')) + new_price = self.product.prices.get(price=Decimal('20.66')) assert new_price.current is True def test_create_variant_for_product(self): @@ -150,6 +152,7 @@ class TestBackofficeProducts(TestCase, CreateProductsMixin): assert not ProductVariant.objects.filter(sku='V-M2').exists() def test_create_price_for_product(self): + # El staff introduce el precio CON impuestos; se guarda sin impuestos. tax, created = Tax.objects.get_or_create(code='IVA', value=21) response = self.client.post( @@ -157,7 +160,9 @@ class TestBackofficeProducts(TestCase, CreateProductsMixin): {'price': '9.99', 'tax': tax.pk, 'current': 'on'}, ) assert response.status_code == 302 - assert ProductPrice.objects.filter(product=self.product, price=Decimal('9.99')).exists() + price = ProductPrice.objects.get(product=self.product, current=True) + assert price.price == Decimal('8.26') + assert price.price_with_tax == Decimal('9.99') def test_create_price_for_variant(self): tax, created = Tax.objects.get_or_create(code='IVA', value=21) @@ -169,7 +174,9 @@ class TestBackofficeProducts(TestCase, CreateProductsMixin): {'price': '19.99', 'tax': tax.pk, 'current': 'on'}, ) assert response.status_code == 302 - assert ProductPrice.objects.filter(variant=variant, price=Decimal('19.99')).exists() + price = ProductPrice.objects.get(variant=variant, current=True) + assert price.price == Decimal('16.52') + assert price.price_with_tax == Decimal('19.99') def create_image_file(self, name='test.webp'): image = Image.new('RGB', (256, 256), '#ACACAC') diff --git a/backoffice/views/products.py b/backoffice/views/products.py index 550a8f1..d61305a 100644 --- a/backoffice/views/products.py +++ b/backoffice/views/products.py @@ -6,7 +6,7 @@ from django.shortcuts import get_object_or_404, render from django.urls import reverse, reverse_lazy from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView, View -from backoffice.forms import ProductInitialPriceForm, ProductVariantForm +from backoffice.forms import ProductInitialPriceForm, ProductPriceForm, ProductVariantForm from backoffice.mixins import ( BackofficeCRUDMixin, BackofficeHtmxMixin, @@ -109,7 +109,7 @@ class ProductCreateView(BackofficeCRUDMixin, BackofficeStyledFormMixin, CreateVi if price_form.cleaned_data.get('price') is not None: ProductPrice.objects.create( product=self.object, - price=price_form.cleaned_data['price'], + price=price_form.get_price_without_tax(), tax=price_form.cleaned_data['tax'], current=True, ) @@ -378,9 +378,9 @@ class ProductVariantDeleteView(BackofficeCRUDMixin, BackofficeModalDeleteMixin, class ProductPriceCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView): model = ProductPrice + form_class = ProductPriceForm permission_required = 'shop.add_productprice' section = SECTION - fields = ('price', 'tax', 'current') template_name = 'backoffice/generic/form.html' fragment_template_name = FORM_FRAGMENT @@ -408,9 +408,9 @@ class ProductPriceCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, Crea class ProductVariantPriceCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView): model = ProductPrice + form_class = ProductPriceForm permission_required = 'shop.add_productprice' section = SECTION - fields = ('price', 'tax', 'current') template_name = 'backoffice/generic/form.html' fragment_template_name = FORM_FRAGMENT