828 lines
30 KiB
Python
828 lines
30 KiB
Python
from django import forms
|
|
from django.core.exceptions import ValidationError
|
|
from django.forms import modelform_factory
|
|
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
|
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.mixins import (
|
|
BackofficeCRUDMixin,
|
|
BackofficeHtmxMixin,
|
|
BackofficeModalDeleteMixin,
|
|
BackofficeModalFormMixin,
|
|
BackofficeStyledFormMixin,
|
|
)
|
|
from shop.models import (
|
|
Brand,
|
|
Product,
|
|
ProductAttribute,
|
|
ProductAttributeValue,
|
|
ProductBatch,
|
|
ProductCategory,
|
|
ProductImage,
|
|
ProductPrice,
|
|
ProductVariant,
|
|
Tag,
|
|
)
|
|
from shop.utils import delete_product_batch
|
|
|
|
SECTION = 'products'
|
|
|
|
LIST_FRAGMENT = 'backoffice/generic/_list_fragment.html'
|
|
FORM_FRAGMENT = 'backoffice/generic/_form_fragment.html'
|
|
DELETE_FRAGMENT = 'backoffice/generic/_confirm_delete_fragment.html'
|
|
|
|
|
|
# --- Product ---
|
|
|
|
|
|
class ProductListView(BackofficeCRUDMixin, BackofficeHtmxMixin, ListView):
|
|
model = Product
|
|
permission_required = 'shop.view_product'
|
|
section = SECTION
|
|
paginate_by = 20
|
|
template_name = 'backoffice/generic/list.html'
|
|
fragment_template_name = LIST_FRAGMENT
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context.update(
|
|
{
|
|
'title': 'Productos',
|
|
'columns': [('SKU', 'sku'), ('Nombre', 'name'), ('Stock', 'stock'), ('Oculto', 'hidden')],
|
|
'create_url': reverse_lazy('backoffice:product_create'),
|
|
'create_is_page': True,
|
|
'detail_url_name': 'backoffice:product_detail',
|
|
'delete_url_name': 'backoffice:product_delete',
|
|
}
|
|
)
|
|
return context
|
|
|
|
|
|
class ProductCreateView(BackofficeCRUDMixin, BackofficeStyledFormMixin, CreateView):
|
|
"""Página completa (no modal), con el mismo aspecto/widgets que la página de
|
|
detalle (incluido el combobox de marca/categorías/etiquetas), pero todo en
|
|
un único formulario: nada se envía al servidor hasta pulsar "Crear
|
|
producto". Las imágenes y el precio inicial se suben/crean en el mismo
|
|
envío (variantes y remesas sí requieren que el producto ya exista, así que
|
|
esas se añaden después desde el detalle)."""
|
|
|
|
model = Product
|
|
permission_required = 'shop.add_product'
|
|
section = SECTION
|
|
fields = ('sku', 'name', 'description', 'stock', 'brand', 'categories', 'tags', 'hidden', 'is_shipping_method')
|
|
template_name = 'backoffice/products/product_create.html'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context.setdefault('price_form', ProductInitialPriceForm())
|
|
context.setdefault('image_errors', [])
|
|
return context
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy('backoffice:product_detail', args=[self.object.pk])
|
|
|
|
def form_valid(self, form):
|
|
price_form = ProductInitialPriceForm(self.request.POST)
|
|
image_files = self.request.FILES.getlist('images')
|
|
image_field = forms.ImageField()
|
|
image_errors = []
|
|
|
|
for file in image_files:
|
|
try:
|
|
image_field.clean(file)
|
|
except ValidationError as exc:
|
|
image_errors.append(f'{file.name}: {", ".join(exc.messages)}')
|
|
|
|
if not price_form.is_valid() or image_errors:
|
|
return self.render_to_response(
|
|
self.get_context_data(form=form, price_form=price_form, image_errors=image_errors)
|
|
)
|
|
|
|
self.object = form.save()
|
|
|
|
for file in image_files:
|
|
ProductImage.objects.create(product=self.object, original=file)
|
|
|
|
if price_form.cleaned_data.get('price') is not None:
|
|
ProductPrice.objects.create(
|
|
product=self.object,
|
|
price=price_form.cleaned_data['price'],
|
|
tax=price_form.cleaned_data['tax'],
|
|
current=True,
|
|
)
|
|
|
|
return HttpResponseRedirect(self.get_success_url())
|
|
|
|
|
|
class ProductDeleteView(BackofficeCRUDMixin, BackofficeModalDeleteMixin, DeleteView):
|
|
model = Product
|
|
permission_required = 'shop.delete_product'
|
|
section = SECTION
|
|
template_name = 'backoffice/generic/confirm_delete.html'
|
|
fragment_template_name = DELETE_FRAGMENT
|
|
success_url = reverse_lazy('backoffice:product_list')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['cancel_url'] = reverse_lazy('backoffice:product_list')
|
|
return context
|
|
|
|
def form_valid(self, form):
|
|
# Borrar el producto invalida la página en la que estuvieras (lista o su
|
|
# propio detalle), así que aquí sí navegamos de verdad en vez de solo
|
|
# cerrar el modal y refrescar en sitio.
|
|
success_url = self.get_success_url()
|
|
self.perform_delete()
|
|
|
|
if self.is_htmx():
|
|
response = HttpResponse(status=200)
|
|
response['HX-Redirect'] = success_url
|
|
return response
|
|
|
|
return HttpResponseRedirect(success_url)
|
|
|
|
|
|
class ProductDetailView(BackofficeCRUDMixin, DetailView):
|
|
model = Product
|
|
permission_required = 'shop.view_product'
|
|
section = SECTION
|
|
template_name = 'backoffice/products/product_detail.html'
|
|
fragment_template_name = 'backoffice/products/_product_detail_fragment.html'
|
|
|
|
def get_template_names(self):
|
|
if self.request.headers.get('HX-Request') == 'true':
|
|
return [self.fragment_template_name]
|
|
return super().get_template_names()
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['variants'] = self.object.variants.prefetch_related('attribute_values__attribute')
|
|
context['batches'] = ProductBatch.objects.filter(product=self.object)
|
|
context['images'] = self.object.images.all()
|
|
context['prices'] = self.object.prices.select_related('tax')
|
|
return context
|
|
|
|
|
|
# --- Edición inline de Product.name / Product.description / Product.brand /
|
|
# Product.categories / Product.tags ---
|
|
|
|
INLINE_EDITABLE_PRODUCT_FIELDS = ('name', 'description', 'brand', 'categories', 'tags')
|
|
MULTI_VALUE_INLINE_FIELDS = ('categories', 'tags')
|
|
COMBOBOX_INLINE_FIELDS = {
|
|
'brand': Brand,
|
|
'categories': ProductCategory,
|
|
'tags': Tag,
|
|
}
|
|
|
|
|
|
class ProductInlineFieldMixin:
|
|
def get_field_name(self):
|
|
field_name = self.kwargs['field_name']
|
|
|
|
if field_name not in INLINE_EDITABLE_PRODUCT_FIELDS:
|
|
raise Http404
|
|
|
|
return field_name
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context.update(
|
|
{
|
|
'field_name': self.get_field_name(),
|
|
'view_url': reverse('backoffice:product_inline_view', args=[self.object.pk, self.get_field_name()]),
|
|
'edit_url': reverse('backoffice:product_inline_edit', args=[self.object.pk, self.get_field_name()]),
|
|
}
|
|
)
|
|
return context
|
|
|
|
|
|
class ProductInlineViewFragment(BackofficeCRUDMixin, ProductInlineFieldMixin, DetailView):
|
|
model = Product
|
|
permission_required = 'shop.view_product'
|
|
section = SECTION
|
|
template_name = 'backoffice/products/_inline_view.html'
|
|
|
|
|
|
class ProductInlineEditView(BackofficeCRUDMixin, BackofficeStyledFormMixin, ProductInlineFieldMixin, UpdateView):
|
|
model = Product
|
|
permission_required = 'shop.change_product'
|
|
section = SECTION
|
|
template_name = 'backoffice/products/_inline_edit.html'
|
|
|
|
def get_form_class(self):
|
|
return modelform_factory(Product, fields=[self.get_field_name()])
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
field_name = self.get_field_name()
|
|
|
|
if field_name in COMBOBOX_INLINE_FIELDS:
|
|
multiple = field_name in MULTI_VALUE_INLINE_FIELDS
|
|
value = getattr(self.object, field_name)
|
|
context.update(
|
|
{'multiple': multiple, 'selected': list(value.all()) if multiple else list(filter(None, [value]))}
|
|
)
|
|
|
|
return context
|
|
|
|
def form_valid(self, form):
|
|
self.object = form.save()
|
|
return render(self.request, 'backoffice/products/_inline_view.html', self.get_context_data())
|
|
|
|
|
|
class ProductFieldSearchView(BackofficeCRUDMixin, View):
|
|
permission_required = 'shop.change_product'
|
|
section = SECTION
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
field_name = kwargs['field_name']
|
|
model = COMBOBOX_INLINE_FIELDS.get(field_name)
|
|
|
|
if model is None:
|
|
raise Http404
|
|
|
|
query = request.GET.get('q', '').strip()
|
|
queryset = model.objects.all()
|
|
|
|
if query:
|
|
queryset = queryset.filter(name__icontains=query)
|
|
|
|
return render(
|
|
request,
|
|
'backoffice/products/_combobox_results.html',
|
|
{'results': queryset[:10], 'field_name': field_name},
|
|
)
|
|
|
|
|
|
# --- ProductImage (nested under a product) ---
|
|
|
|
|
|
class ProductImageCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView):
|
|
model = ProductImage
|
|
permission_required = 'shop.add_productimage'
|
|
section = SECTION
|
|
fields = ('original',)
|
|
template_name = 'backoffice/generic/form.html'
|
|
fragment_template_name = FORM_FRAGMENT
|
|
|
|
def get_product(self):
|
|
return get_object_or_404(Product, pk=self.kwargs['product_pk'])
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
form.instance.product = self.get_product()
|
|
return form
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
product = self.get_product()
|
|
context.update(
|
|
{'title': f'Añadir imagen a {product.name}', 'cancel_url': reverse_lazy('backoffice:product_detail', args=[product.pk])}
|
|
)
|
|
return context
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
|
|
|
|
|
|
class ProductImageDeleteView(BackofficeCRUDMixin, BackofficeModalDeleteMixin, DeleteView):
|
|
model = ProductImage
|
|
permission_required = 'shop.delete_productimage'
|
|
section = SECTION
|
|
template_name = 'backoffice/generic/confirm_delete.html'
|
|
fragment_template_name = DELETE_FRAGMENT
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['cancel_url'] = reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
|
|
return context
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
|
|
|
|
|
|
# --- ProductVariant (nested under a product) ---
|
|
|
|
|
|
class ProductVariantCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView):
|
|
model = ProductVariant
|
|
form_class = ProductVariantForm
|
|
permission_required = 'shop.add_productvariant'
|
|
section = SECTION
|
|
template_name = 'backoffice/generic/form.html'
|
|
fragment_template_name = FORM_FRAGMENT
|
|
|
|
def get_product(self):
|
|
return get_object_or_404(Product, pk=self.kwargs['product_pk'])
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['product'] = self.get_product()
|
|
return kwargs
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
product = self.get_product()
|
|
context.update(
|
|
{'title': f'Añadir variante a {product.name}', 'cancel_url': reverse_lazy('backoffice:product_detail', args=[product.pk])}
|
|
)
|
|
return context
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
|
|
|
|
|
|
class ProductVariantUpdateView(BackofficeCRUDMixin, BackofficeModalFormMixin, UpdateView):
|
|
model = ProductVariant
|
|
form_class = ProductVariantForm
|
|
permission_required = 'shop.change_productvariant'
|
|
section = SECTION
|
|
template_name = 'backoffice/generic/form.html'
|
|
fragment_template_name = FORM_FRAGMENT
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context.update(
|
|
{
|
|
'title': f'Editar variante {self.object.sku}',
|
|
'cancel_url': reverse_lazy('backoffice:product_detail', args=[self.object.product_id]),
|
|
}
|
|
)
|
|
return context
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
|
|
|
|
|
|
class ProductVariantDeleteView(BackofficeCRUDMixin, BackofficeModalDeleteMixin, DeleteView):
|
|
model = ProductVariant
|
|
permission_required = 'shop.delete_productvariant'
|
|
section = SECTION
|
|
template_name = 'backoffice/generic/confirm_delete.html'
|
|
fragment_template_name = DELETE_FRAGMENT
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['cancel_url'] = reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
|
|
return context
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
|
|
|
|
|
|
# --- ProductPrice (create only — la historia es de solo alta, ver shop/models.py:ProductPrice) ---
|
|
|
|
|
|
class ProductPriceCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView):
|
|
model = ProductPrice
|
|
permission_required = 'shop.add_productprice'
|
|
section = SECTION
|
|
fields = ('price', 'tax', 'current')
|
|
template_name = 'backoffice/generic/form.html'
|
|
fragment_template_name = FORM_FRAGMENT
|
|
|
|
def get_product(self):
|
|
return get_object_or_404(Product, pk=self.kwargs['product_pk'])
|
|
|
|
def get_form(self, form_class=None):
|
|
# El FK debe fijarse antes de is_valid(), porque ProductPrice.clean()
|
|
# exige exactamente uno de product/variant en el momento de validar.
|
|
form = super().get_form(form_class)
|
|
form.instance.product = self.get_product()
|
|
return form
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
product = self.get_product()
|
|
context.update(
|
|
{'title': f'Añadir precio a {product.name}', 'cancel_url': reverse_lazy('backoffice:product_detail', args=[product.pk])}
|
|
)
|
|
return context
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
|
|
|
|
|
|
class ProductVariantPriceCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView):
|
|
model = ProductPrice
|
|
permission_required = 'shop.add_productprice'
|
|
section = SECTION
|
|
fields = ('price', 'tax', 'current')
|
|
template_name = 'backoffice/generic/form.html'
|
|
fragment_template_name = FORM_FRAGMENT
|
|
|
|
def get_variant(self):
|
|
return get_object_or_404(ProductVariant, pk=self.kwargs['variant_pk'])
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
form.instance.variant = self.get_variant()
|
|
return form
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
variant = self.get_variant()
|
|
context.update(
|
|
{
|
|
'title': f'Añadir precio a la variante {variant.sku}',
|
|
'cancel_url': reverse_lazy('backoffice:product_detail', args=[variant.product_id]),
|
|
}
|
|
)
|
|
return context
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy('backoffice:product_detail', args=[self.object.variant.product_id])
|
|
|
|
|
|
# --- ProductBatch (nested under a product) ---
|
|
|
|
|
|
class ProductBatchCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView):
|
|
model = ProductBatch
|
|
permission_required = 'shop.add_productbatch'
|
|
section = SECTION
|
|
fields = ('code', 'quantity', 'expiration_date', 'provider')
|
|
template_name = 'backoffice/generic/form.html'
|
|
fragment_template_name = FORM_FRAGMENT
|
|
|
|
def get_product(self):
|
|
return get_object_or_404(Product, pk=self.kwargs['product_pk'])
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
form.instance.product = self.get_product()
|
|
return form
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
product = self.get_product()
|
|
context.update(
|
|
{'title': f'Añadir remesa a {product.name}', 'cancel_url': reverse_lazy('backoffice:product_detail', args=[product.pk])}
|
|
)
|
|
return context
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
|
|
|
|
|
|
class ProductBatchDeleteView(BackofficeCRUDMixin, BackofficeModalDeleteMixin, DeleteView):
|
|
model = ProductBatch
|
|
permission_required = 'shop.delete_productbatch'
|
|
section = SECTION
|
|
template_name = 'backoffice/generic/confirm_delete.html'
|
|
fragment_template_name = DELETE_FRAGMENT
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['cancel_url'] = reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
|
|
return context
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy('backoffice:product_detail', args=[self.object.product_id])
|
|
|
|
def perform_delete(self):
|
|
# Reutiliza la lógica ya existente de descuento de stock al borrar una remesa.
|
|
delete_product_batch(self.object)
|
|
|
|
|
|
# --- ProductCategory ---
|
|
|
|
|
|
class ProductCategoryListView(BackofficeCRUDMixin, BackofficeHtmxMixin, ListView):
|
|
model = ProductCategory
|
|
permission_required = 'shop.view_productcategory'
|
|
section = SECTION
|
|
template_name = 'backoffice/generic/list.html'
|
|
fragment_template_name = LIST_FRAGMENT
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context.update(
|
|
{
|
|
'title': 'Categorías de producto',
|
|
'columns': [('Nombre', 'name'), ('Padre', 'parent'), ('Oculta', 'hidden')],
|
|
'create_url': reverse_lazy('backoffice:product_category_create'),
|
|
'update_url_name': 'backoffice:product_category_update',
|
|
'delete_url_name': 'backoffice:product_category_delete',
|
|
}
|
|
)
|
|
return context
|
|
|
|
|
|
class ProductCategoryCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView):
|
|
model = ProductCategory
|
|
permission_required = 'shop.add_productcategory'
|
|
section = SECTION
|
|
fields = ('name', 'parent', 'promoted', 'hidden', 'show_in_navbar')
|
|
template_name = 'backoffice/generic/form.html'
|
|
fragment_template_name = FORM_FRAGMENT
|
|
success_url = reverse_lazy('backoffice:product_category_list')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context.update({'title': 'Añadir categoría', 'cancel_url': reverse_lazy('backoffice:product_category_list')})
|
|
return context
|
|
|
|
|
|
class ProductCategoryUpdateView(BackofficeCRUDMixin, BackofficeModalFormMixin, UpdateView):
|
|
model = ProductCategory
|
|
permission_required = 'shop.change_productcategory'
|
|
section = SECTION
|
|
fields = ('name', 'parent', 'promoted', 'hidden', 'show_in_navbar')
|
|
template_name = 'backoffice/generic/form.html'
|
|
fragment_template_name = FORM_FRAGMENT
|
|
success_url = reverse_lazy('backoffice:product_category_list')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context.update({'title': f'Editar {self.object.name}', 'cancel_url': reverse_lazy('backoffice:product_category_list')})
|
|
return context
|
|
|
|
|
|
class ProductCategoryDeleteView(BackofficeCRUDMixin, BackofficeModalDeleteMixin, DeleteView):
|
|
model = ProductCategory
|
|
permission_required = 'shop.delete_productcategory'
|
|
section = SECTION
|
|
template_name = 'backoffice/generic/confirm_delete.html'
|
|
fragment_template_name = DELETE_FRAGMENT
|
|
success_url = reverse_lazy('backoffice:product_category_list')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['cancel_url'] = reverse_lazy('backoffice:product_category_list')
|
|
return context
|
|
|
|
|
|
# --- ProductAttribute / ProductAttributeValue ---
|
|
|
|
|
|
class ProductAttributeListView(BackofficeCRUDMixin, BackofficeHtmxMixin, ListView):
|
|
model = ProductAttribute
|
|
permission_required = 'shop.view_productattribute'
|
|
section = SECTION
|
|
template_name = 'backoffice/generic/list.html'
|
|
fragment_template_name = LIST_FRAGMENT
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context.update(
|
|
{
|
|
'title': 'Atributos de producto',
|
|
'columns': [('Nombre', 'name')],
|
|
'create_url': reverse_lazy('backoffice:product_attribute_create'),
|
|
'detail_url_name': 'backoffice:product_attribute_detail',
|
|
'update_url_name': 'backoffice:product_attribute_update',
|
|
'delete_url_name': 'backoffice:product_attribute_delete',
|
|
}
|
|
)
|
|
return context
|
|
|
|
|
|
class ProductAttributeCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView):
|
|
model = ProductAttribute
|
|
permission_required = 'shop.add_productattribute'
|
|
section = SECTION
|
|
fields = ('name',)
|
|
template_name = 'backoffice/generic/form.html'
|
|
fragment_template_name = FORM_FRAGMENT
|
|
success_url = reverse_lazy('backoffice:product_attribute_list')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context.update({'title': 'Añadir atributo', 'cancel_url': reverse_lazy('backoffice:product_attribute_list')})
|
|
return context
|
|
|
|
|
|
class ProductAttributeUpdateView(BackofficeCRUDMixin, BackofficeModalFormMixin, UpdateView):
|
|
"""Solo edita el nombre del atributo. Los valores se gestionan en ProductAttributeDetailView."""
|
|
|
|
model = ProductAttribute
|
|
permission_required = 'shop.change_productattribute'
|
|
section = SECTION
|
|
fields = ('name',)
|
|
template_name = 'backoffice/generic/form.html'
|
|
fragment_template_name = FORM_FRAGMENT
|
|
success_url = reverse_lazy('backoffice:product_attribute_list')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context.update({'title': f'Editar {self.object.name}', 'cancel_url': reverse_lazy('backoffice:product_attribute_list')})
|
|
return context
|
|
|
|
|
|
class ProductAttributeDeleteView(BackofficeCRUDMixin, BackofficeModalDeleteMixin, DeleteView):
|
|
model = ProductAttribute
|
|
permission_required = 'shop.delete_productattribute'
|
|
section = SECTION
|
|
template_name = 'backoffice/generic/confirm_delete.html'
|
|
fragment_template_name = DELETE_FRAGMENT
|
|
success_url = reverse_lazy('backoffice:product_attribute_list')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['cancel_url'] = reverse_lazy('backoffice:product_attribute_list')
|
|
return context
|
|
|
|
|
|
class ProductAttributeDetailView(BackofficeCRUDMixin, DetailView):
|
|
"""Gestión de los valores (ProductAttributeValue) de un atributo."""
|
|
|
|
model = ProductAttribute
|
|
permission_required = 'shop.view_productattribute'
|
|
section = SECTION
|
|
template_name = 'backoffice/products/attribute_detail.html'
|
|
fragment_template_name = 'backoffice/products/_attribute_detail_fragment.html'
|
|
|
|
def get_template_names(self):
|
|
if self.request.headers.get('HX-Request') == 'true':
|
|
return [self.fragment_template_name]
|
|
return super().get_template_names()
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['values'] = self.object.values.all()
|
|
return context
|
|
|
|
|
|
class ProductAttributeValueCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView):
|
|
model = ProductAttributeValue
|
|
permission_required = 'shop.add_productattributevalue'
|
|
section = SECTION
|
|
fields = ('value',)
|
|
template_name = 'backoffice/generic/form.html'
|
|
fragment_template_name = FORM_FRAGMENT
|
|
|
|
def get_attribute(self):
|
|
return get_object_or_404(ProductAttribute, pk=self.kwargs['attribute_pk'])
|
|
|
|
def get_form(self, form_class=None):
|
|
form = super().get_form(form_class)
|
|
form.instance.attribute = self.get_attribute()
|
|
return form
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
attribute = self.get_attribute()
|
|
context.update(
|
|
{
|
|
'title': f'Añadir valor a {attribute.name}',
|
|
'cancel_url': reverse_lazy('backoffice:product_attribute_detail', args=[attribute.pk]),
|
|
}
|
|
)
|
|
return context
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy('backoffice:product_attribute_detail', args=[self.object.attribute_id])
|
|
|
|
|
|
class ProductAttributeValueDeleteView(BackofficeCRUDMixin, BackofficeModalDeleteMixin, DeleteView):
|
|
model = ProductAttributeValue
|
|
permission_required = 'shop.delete_productattributevalue'
|
|
section = SECTION
|
|
template_name = 'backoffice/generic/confirm_delete.html'
|
|
fragment_template_name = DELETE_FRAGMENT
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['cancel_url'] = reverse_lazy('backoffice:product_attribute_detail', args=[self.object.attribute_id])
|
|
return context
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy('backoffice:product_attribute_detail', args=[self.object.attribute_id])
|
|
|
|
|
|
# --- Brand / Tag ---
|
|
|
|
|
|
class BrandListView(BackofficeCRUDMixin, BackofficeHtmxMixin, ListView):
|
|
model = Brand
|
|
permission_required = 'shop.view_brand'
|
|
section = SECTION
|
|
template_name = 'backoffice/generic/list.html'
|
|
fragment_template_name = LIST_FRAGMENT
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context.update(
|
|
{
|
|
'title': 'Marcas',
|
|
'columns': [('Nombre', 'name')],
|
|
'create_url': reverse_lazy('backoffice:brand_create'),
|
|
'update_url_name': 'backoffice:brand_update',
|
|
'delete_url_name': 'backoffice:brand_delete',
|
|
}
|
|
)
|
|
return context
|
|
|
|
|
|
class BrandCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView):
|
|
model = Brand
|
|
permission_required = 'shop.add_brand'
|
|
section = SECTION
|
|
fields = ('name',)
|
|
template_name = 'backoffice/generic/form.html'
|
|
fragment_template_name = FORM_FRAGMENT
|
|
success_url = reverse_lazy('backoffice:brand_list')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context.update({'title': 'Añadir marca', 'cancel_url': reverse_lazy('backoffice:brand_list')})
|
|
return context
|
|
|
|
|
|
class BrandUpdateView(BackofficeCRUDMixin, BackofficeModalFormMixin, UpdateView):
|
|
model = Brand
|
|
permission_required = 'shop.change_brand'
|
|
section = SECTION
|
|
fields = ('name',)
|
|
template_name = 'backoffice/generic/form.html'
|
|
fragment_template_name = FORM_FRAGMENT
|
|
success_url = reverse_lazy('backoffice:brand_list')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context.update({'title': f'Editar {self.object.name}', 'cancel_url': reverse_lazy('backoffice:brand_list')})
|
|
return context
|
|
|
|
|
|
class BrandDeleteView(BackofficeCRUDMixin, BackofficeModalDeleteMixin, DeleteView):
|
|
model = Brand
|
|
permission_required = 'shop.delete_brand'
|
|
section = SECTION
|
|
template_name = 'backoffice/generic/confirm_delete.html'
|
|
fragment_template_name = DELETE_FRAGMENT
|
|
success_url = reverse_lazy('backoffice:brand_list')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['cancel_url'] = reverse_lazy('backoffice:brand_list')
|
|
return context
|
|
|
|
|
|
class TagListView(BackofficeCRUDMixin, BackofficeHtmxMixin, ListView):
|
|
model = Tag
|
|
permission_required = 'shop.view_tag'
|
|
section = SECTION
|
|
template_name = 'backoffice/generic/list.html'
|
|
fragment_template_name = LIST_FRAGMENT
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context.update(
|
|
{
|
|
'title': 'Etiquetas',
|
|
'columns': [('Nombre', 'name')],
|
|
'create_url': reverse_lazy('backoffice:tag_create'),
|
|
'update_url_name': 'backoffice:tag_update',
|
|
'delete_url_name': 'backoffice:tag_delete',
|
|
}
|
|
)
|
|
return context
|
|
|
|
|
|
class TagCreateView(BackofficeCRUDMixin, BackofficeModalFormMixin, CreateView):
|
|
model = Tag
|
|
permission_required = 'shop.add_tag'
|
|
section = SECTION
|
|
fields = ('name',)
|
|
template_name = 'backoffice/generic/form.html'
|
|
fragment_template_name = FORM_FRAGMENT
|
|
success_url = reverse_lazy('backoffice:tag_list')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context.update({'title': 'Añadir etiqueta', 'cancel_url': reverse_lazy('backoffice:tag_list')})
|
|
return context
|
|
|
|
|
|
class TagUpdateView(BackofficeCRUDMixin, BackofficeModalFormMixin, UpdateView):
|
|
model = Tag
|
|
permission_required = 'shop.change_tag'
|
|
section = SECTION
|
|
fields = ('name',)
|
|
template_name = 'backoffice/generic/form.html'
|
|
fragment_template_name = FORM_FRAGMENT
|
|
success_url = reverse_lazy('backoffice:tag_list')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context.update({'title': f'Editar {self.object.name}', 'cancel_url': reverse_lazy('backoffice:tag_list')})
|
|
return context
|
|
|
|
|
|
class TagDeleteView(BackofficeCRUDMixin, BackofficeModalDeleteMixin, DeleteView):
|
|
model = Tag
|
|
permission_required = 'shop.delete_tag'
|
|
section = SECTION
|
|
template_name = 'backoffice/generic/confirm_delete.html'
|
|
fragment_template_name = DELETE_FRAGMENT
|
|
success_url = reverse_lazy('backoffice:tag_list')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['cancel_url'] = reverse_lazy('backoffice:tag_list')
|
|
return context
|