feat: improved product attribute views
This commit is contained in:
@@ -33,7 +33,20 @@
|
|||||||
<a href="{% url 'backoffice:dashboard' %}" class="text-xl font-semibold block p-4">Backoffice</a>
|
<a href="{% url 'backoffice:dashboard' %}" class="text-xl font-semibold block p-4">Backoffice</a>
|
||||||
<ul class="menu w-full">
|
<ul class="menu w-full">
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'backoffice:product_list' %}" class="{% if section == 'products' %}menu-active{% endif %}">Productos</a>
|
<details {% if section == 'products' %}open{% endif %}>
|
||||||
|
<summary>
|
||||||
|
<a
|
||||||
|
href="{% url 'backoffice:product_list' %}"
|
||||||
|
onclick="event.stopPropagation()"
|
||||||
|
class="grow {% if section == 'products' and '/attributes/' not in request.path %}menu-active{% endif %}"
|
||||||
|
>Productos</a>
|
||||||
|
</summary>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'backoffice:product_attribute_list' %}" class="{% if '/attributes/' in request.path %}menu-active{% endif %}">Atributos</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'backoffice:order_list' %}" class="{% if section == 'orders' %}menu-active{% endif %}">Pedidos</a>
|
<a href="{% url 'backoffice:order_list' %}" class="{% if section == 'orders' %}menu-active{% endif %}">Pedidos</a>
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<h3 class="text-lg font-bold mb-4">{{ title }}</h3>
|
||||||
|
|
||||||
|
<form method="post" hx-post="{{ request.path }}" hx-target="#backoffice-modal-box" hx-swap="innerHTML">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="text-sm font-medium opacity-70 mb-1 block" for="{{ form.sku.id_for_label }}">SKU</label>
|
||||||
|
{{ form.sku }}
|
||||||
|
{{ form.sku.errors }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="text-sm font-medium opacity-70 mb-1 block" for="{{ form.stock.id_for_label }}">Stock</label>
|
||||||
|
{{ form.stock }}
|
||||||
|
{{ form.stock.errors }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="text-sm font-medium opacity-70 mb-1">Atributos</h4>
|
||||||
|
<div class="overflow-x-auto bg-base-100 rounded-box border border-base-300">
|
||||||
|
<table class="table">
|
||||||
|
<thead><tr><th>Atributo</th><th>Valores</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for attribute in attributes %}
|
||||||
|
<tr>
|
||||||
|
<td class="whitespace-nowrap align-top">{{ attribute.name }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{% for value in attribute.values.all %}
|
||||||
|
<label class="label cursor-pointer gap-2 border border-base-300 rounded-box px-2 py-1">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="attribute_values"
|
||||||
|
value="{{ value.pk }}"
|
||||||
|
class="checkbox checkbox-sm"
|
||||||
|
{% if value.pk in selected_value_ids %}checked{% endif %}
|
||||||
|
>
|
||||||
|
<span class="label-text">{{ value.value }}</span>
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="2" class="text-center py-4">No hay atributos definidos.</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{{ form.attribute_values.errors }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
|
||||||
|
<div class="modal-action">
|
||||||
|
<button class="btn btn-primary">Guardar</button>
|
||||||
|
<button type="button" class="btn" onclick="document.getElementById('backoffice-modal').close()">Cancelar</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{% extends 'backoffice/base.html' %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include 'backoffice/products/_variant_form_fragment.html' %}
|
||||||
|
{% endblock %}
|
||||||
@@ -306,14 +306,38 @@ class ProductImageDeleteView(BackofficeCRUDMixin, BackofficeModalDeleteMixin, De
|
|||||||
|
|
||||||
# --- ProductVariant (nested under a product) ---
|
# --- ProductVariant (nested under a product) ---
|
||||||
|
|
||||||
|
VARIANT_FORM_FRAGMENT = 'backoffice/products/_variant_form_fragment.html'
|
||||||
|
|
||||||
class ProductVariantCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView):
|
|
||||||
|
class ProductVariantAttributesContextMixin:
|
||||||
|
"""Agrupa los valores de atributo por atributo para pintarlos como tabla
|
||||||
|
(ver _variant_form_fragment.html) en vez del <ul> de checkboxes que genera
|
||||||
|
por defecto CheckboxSelectMultiple. Si el form viene con datos (reenvío
|
||||||
|
tras un error de validación, p.ej. combinación duplicada) se respeta lo
|
||||||
|
que el usuario había marcado en vez de lo que tuviera guardado la instancia."""
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
form = context['form']
|
||||||
|
|
||||||
|
if form.is_bound:
|
||||||
|
selected_value_ids = {int(v) for v in form.data.getlist('attribute_values') if v.isdigit()}
|
||||||
|
else:
|
||||||
|
instance = getattr(self, 'object', None)
|
||||||
|
selected_value_ids = set(instance.attribute_values.values_list('pk', flat=True)) if instance and instance.pk else set()
|
||||||
|
|
||||||
|
context['attributes'] = ProductAttribute.objects.prefetch_related('values')
|
||||||
|
context['selected_value_ids'] = selected_value_ids
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class ProductVariantCreateView(ProductVariantAttributesContextMixin, BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView):
|
||||||
model = ProductVariant
|
model = ProductVariant
|
||||||
form_class = ProductVariantForm
|
form_class = ProductVariantForm
|
||||||
permission_required = 'shop.add_productvariant'
|
permission_required = 'shop.add_productvariant'
|
||||||
section = SECTION
|
section = SECTION
|
||||||
template_name = 'backoffice/generic/form.html'
|
template_name = 'backoffice/products/variant_form.html'
|
||||||
fragment_template_name = FORM_FRAGMENT
|
fragment_template_name = VARIANT_FORM_FRAGMENT
|
||||||
|
|
||||||
def get_product(self):
|
def get_product(self):
|
||||||
return get_object_or_404(Product, pk=self.kwargs['product_pk'])
|
return get_object_or_404(Product, pk=self.kwargs['product_pk'])
|
||||||
@@ -335,13 +359,13 @@ class ProductVariantCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, Cr
|
|||||||
return reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
|
return reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
|
||||||
|
|
||||||
|
|
||||||
class ProductVariantUpdateView(BackofficeCRUDMixin, BackofficeModalFormMixin, UpdateView):
|
class ProductVariantUpdateView(ProductVariantAttributesContextMixin, BackofficeCRUDMixin, BackofficeModalFormMixin, UpdateView):
|
||||||
model = ProductVariant
|
model = ProductVariant
|
||||||
form_class = ProductVariantForm
|
form_class = ProductVariantForm
|
||||||
permission_required = 'shop.change_productvariant'
|
permission_required = 'shop.change_productvariant'
|
||||||
section = SECTION
|
section = SECTION
|
||||||
template_name = 'backoffice/generic/form.html'
|
template_name = 'backoffice/products/variant_form.html'
|
||||||
fragment_template_name = FORM_FRAGMENT
|
fragment_template_name = VARIANT_FORM_FRAGMENT
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|||||||
Reference in New Issue
Block a user