categorias-productos #51 #52

Open
mono wants to merge 13 commits from feat/51-categorias-productos into main
4 changed files with 319 additions and 0 deletions
Showing only changes of commit 3b53703c4d - Show all commits

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})

View File

@@ -0,0 +1,224 @@
import json
from unittest.mock import patch
from django.contrib.auth.models import User
from django.test import TestCase
from rest_framework.test import APIClient
from rest_framework_simplejwt.tokens import RefreshToken
from ..models.products import ProductCategory
class ProductCategoryApiTest(TestCase):
def setUp(self):
self.user = User.objects.create_superuser(
username="admin",
email="admin@example.com",
password="adminpass",
)
refresh = RefreshToken.for_user(self.user)
self.client = APIClient()
self.client.credentials(
HTTP_AUTHORIZATION=f"Bearer {refresh.access_token}"
)
def login_as_regular_user(self):
user = User.objects.create_user(
username="vendedor", password="vendedorpass"
)
refresh = RefreshToken.for_user(user)
self.client.credentials(
HTTP_AUTHORIZATION=f"Bearer {refresh.access_token}"
)
def test_list_categories_filters_by_active(self):
ProductCategory.objects.create(name="Granos", external_id="6")
ProductCategory.objects.create(
name="Verduras", external_id="7", active=False
)
response = self.client.get(
"/don_confiao/api/product_categories/?active=all"
)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 2)
response = self.client.get(
"/don_confiao/api/product_categories/?active=true"
)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]["name"], "Granos")
response = self.client.get(
"/don_confiao/api/product_categories/?active=false"
)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]["name"], "Verduras")
def test_patch_updates_don_confiao_fields_only(self):
category = ProductCategory.objects.create(
name="Granos", external_id="6"
)
response = self.client.patch(
f"/don_confiao/api/product_categories/{category.pk}/",
{
"description": "Granos enteros",
"alternative_name": "Cereales",
"active": False,
"name": "Nombre intentado",
"external_id": "999",
},
format="json",
)
self.assertEqual(response.status_code, 200)
refreshed = ProductCategory.objects.get(pk=category.pk)
self.assertEqual(refreshed.description, "Granos enteros")
self.assertEqual(refreshed.alternative_name, "Cereales")
self.assertFalse(refreshed.active)
self.assertEqual(refreshed.name, "Granos")
self.assertEqual(refreshed.external_id, "6")
def test_deactivate_categories(self):
category = ProductCategory.objects.create(
name="Granos", external_id="6"
)
response = self.client.post(
"/don_confiao/api/product_categories/deactivate/",
{"ids": [category.pk]},
format="json",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.data,
{
"deactivated": [{"id": category.pk, "name": "Granos"}],
"failed": [],
},
)
self.assertFalse(
ProductCategory.objects.get(pk=category.pk).active
)
def test_activate_categories(self):
category = ProductCategory.objects.create(
name="Granos", external_id="6", active=False
)
response = self.client.post(
"/don_confiao/api/product_categories/activate/",
{"ids": [category.pk]},
format="json",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.data,
{
"activated": [{"id": category.pk, "name": "Granos"}],
"failed": [],
},
)
self.assertTrue(ProductCategory.objects.get(pk=category.pk).active)
def test_deactivate_ignores_nonexistent_ids(self):
category = ProductCategory.objects.create(
name="Granos", external_id="6"
)
response = self.client.post(
"/don_confiao/api/product_categories/deactivate/",
{"ids": [category.pk, 99999]},
format="json",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.data["deactivated"],
[{"id": category.pk, "name": "Granos"}],
)
self.assertEqual(response.data["failed"], [])
self.assertFalse(
ProductCategory.objects.get(pk=category.pk).active
)
@patch("sabatron_tryton_rpc_client.client.Client.call")
@patch("sabatron_tryton_rpc_client.client.Client.connect")
def test_import_from_external(self, mock_connect, mock_call):
mock_connect.return_value = None
def fake_call(*args, **kwargs):
if args[0] == "model.product.product.search":
return [1]
if args[0] == "model.product.product.read":
return [
{
"id": 1,
"name": "Producto 1",
"template.": {"id": 999, "categories": [6]},
},
]
if args[0] == "model.product.category.read":
return [{"id": 6, "name": "Alimentos"}]
raise Exception(f"args no esperados: {args}")
mock_call.side_effect = fake_call
response = self.client.post(
"/don_confiao/api/product_categories/import_from_external/"
)
self.assertEqual(response.status_code, 200)
content = json.loads(response.content.decode("utf-8"))
self.assertTrue(
set(
[
"checked_tryton_categories",
"created_categories",
"updated_categories",
"untouched_categories",
"failed_categories",
]
).issubset(content.keys())
)
self.assertEqual(
content["created_categories"],
[
{
"id": ProductCategory.objects.get(external_id="6").pk,
"name": "Alimentos",
"external_id": "6",
}
],
)
@patch("sabatron_tryton_rpc_client.client.Client.call")
@patch("sabatron_tryton_rpc_client.client.Client.connect")
def test_mutations_require_administrator(
self, mock_connect, mock_call
):
mock_connect.return_value = None
self.login_as_regular_user()
category = ProductCategory.objects.create(
name="Granos", external_id="6"
)
response = self.client.post(
"/don_confiao/api/product_categories/deactivate/",
{"ids": [category.pk]},
format="json",
)
self.assertEqual(response.status_code, 403)
response = self.client.post(
"/don_confiao/api/product_categories/import_from_external/"
)
self.assertEqual(response.status_code, 403)
response = self.client.patch(
f"/don_confiao/api/product_categories/{category.pk}/",
{"description": "x"},
format="json",
)
self.assertEqual(response.status_code, 403)

View File

@@ -8,6 +8,8 @@ from .api import (
# Products
ProductView,
ProductsFromTrytonView,
# Product Categories
ProductCategoryView,
# Customers
CustomerView,
CustomersFromTrytonView,
@@ -45,6 +47,11 @@ router.register(r"sales", SaleView, basename="sale")
router.register(r"catalog_sales", CatalogSaleView, basename="catalog_sale")
router.register(r"customers", CustomerView, basename="customer")
router.register(r"products", ProductView, basename="product")
router.register(
r"product_categories",
ProductCategoryView,
basename="product_category",
)
router.register(
r"catalogue_images",
CatalogueImageViewSet,