#51 feat: API de categorias de productos

Issue #51: discriminacion y administracion de categorias de productos.

- ProductCategoryView en /api/product_categories (list/retrieve IsNotPublico,
  mutaciones admin): filtro ?active, PATCH de description/alternative_name/active
- POST deactivate/activate con body {ids} -> {deactivated|activated, failed}
  (ignora ids inexistentes)
- POST import_from_external admin via CategoryTrytonService
- create/destroy deshabilitados (la fuente de categorias es Tryton)
This commit is contained in:
2026-10-10 16:43:37 -05:00
parent 74cd9738b4
commit 3b53703c4d
4 changed files with 319 additions and 0 deletions

View File

@@ -1,5 +1,6 @@
from .catalogue_images import CatalogueImageViewSet
from .products import ProductView, ProductsFromTrytonView
from .categories import ProductCategoryView
from .customers import CustomerView, CustomersFromTrytonView
from .sales import (
SaleView,
@@ -35,6 +36,8 @@ __all__ = [
# Products
"ProductView",
"ProductsFromTrytonView",
# Product Categories
"ProductCategoryView",
# Customers
"CustomerView",
"CustomersFromTrytonView",

View File

@@ -0,0 +1,85 @@
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from ..models.products import ProductCategory
from ..serializers import ProductCategorySerializer
from ..permissions import IsAdministrator, IsNotPublico
from ..services.tryton.categories import CategoryTrytonService
from ..services.tryton.client import get_tryton_client
class ProductCategoryView(viewsets.ModelViewSet):
queryset = ProductCategory.objects.all()
serializer_class = ProductCategorySerializer
def create(self, request):
return Response(status=405)
def destroy(self, request, pk=None):
return Response(status=405)
def get_permissions(self):
if self.action in ("list", "retrieve"):
return [IsNotPublico()]
return [IsAuthenticated(), IsAdministrator()]
def get_queryset(self):
"""
Filters categories by active status for list operations.
Query params for list:
- active=true (default): Only active categories
- active=false: Only inactive categories
- active=all: All categories regardless of status
"""
queryset = ProductCategory.objects.all()
if self.action != "list":
return queryset
active_param = self.request.query_params.get("active", "true")
if active_param.lower() == "all":
return queryset
elif active_param.lower() in ["true", "1", "yes"]:
return queryset.filter(active=True)
elif active_param.lower() in ["false", "0", "no"]:
return queryset.filter(active=False)
else:
return queryset.filter(active=True)
@action(detail=False, methods=["post"])
def import_from_external(self, request):
tryton_client = get_tryton_client()
service = CategoryTrytonService(tryton_client)
return Response(service.import_from_external(), status=200)
@action(detail=False, methods=["post"])
def deactivate(self, request):
return self._change_active(request, active=False)
@action(detail=False, methods=["post"])
def activate(self, request):
return self._change_active(request, active=True)
def _change_active(self, request, active):
"""Activa o desactiva categorías por ids. Ignora ids inexistentes."""
key = "activated" if active else "deactivated"
affected = []
failed = []
for category_id in request.data.get("ids", []):
try:
category = ProductCategory.objects.get(pk=category_id)
except ProductCategory.DoesNotExist:
continue
except Exception as e:
failed.append({"id": category_id, "error": str(e)})
continue
try:
category.active = active
category.save()
affected.append({"id": category.id, "name": category.name})
except Exception as e:
failed.append({"id": category_id, "error": str(e)})
return Response({key: affected, "failed": failed})