diff --git a/doc/requests.org b/doc/requests.org index 4e590b1..f4de0bc 100644 --- a/doc/requests.org +++ b/doc/requests.org @@ -14,8 +14,8 @@ post /token/ **** respuesta #+begin_src json { - "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTc4MDEwMzY0NiwiaWF0IjoxNzgwMDE3MjQ2LCJqdGkiOiI1NDk2NmQ0YTFmMGE0OWNjOGU5MGY5MmZmMTE0ZTMwZCIsInVzZXJfaWQiOiIxIn0.uWIe0Xm9i9eI4fFaM3Ha3FrIaQLfwvpHwbJue3OvhTo", - "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzgwMDE5MDQ2LCJpYXQiOjE3ODAwMTcyNDYsImp0aSI6IjQzYmYzOGM0ZWY3MTQ1YTk5ZjliMTQzODMyYjEwZmVkIiwidXNlcl9pZCI6IjEifQ.LMxWs0bHejpgcZvCpMCqfe5ue3YxAaWUweWHHoHhoH0" + "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTc4MDExNTc0NywiaWF0IjoxNzgwMDI5MzQ3LCJqdGkiOiIxNmVjZGMxZmY4Y2Y0MzA4ODM3ZjM5Y2ZiNjQwNmZiMCIsInVzZXJfaWQiOiIxIn0.wmN-wp3Izv0NrfL_ap_i8eyg29w-foHNrQCCL6HoZWg", + "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzgwMDMxMTQ3LCJpYXQiOjE3ODAwMjkzNDcsImp0aSI6ImQ4ZjE1YTRmODc5NzRjZGViZDEzYzc1ZTU4ZDk3ZjEwIiwidXNlcl9pZCI6IjEifQ.kkQVT2pcYeS_TxlJ6QPU3rNOlZhOv96pyqVEGJI85KA" } #+end_src *** Perfil de usuario @@ -47,7 +47,7 @@ post /token/refresh/ ** Don confiao :verb: template http://localhost:7000/don_confiao/api/ Content-Type: application/json; -Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzgwMDE5MDQ2LCJpYXQiOjE3ODAwMTcyNDYsImp0aSI6IjQzYmYzOGM0ZWY3MTQ1YTk5ZjliMTQzODMyYjEwZmVkIiwidXNlcl9pZCI6IjEifQ.LMxWs0bHejpgcZvCpMCqfe5ue3YxAaWUweWHHoHhoH0 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzgwMDMxMTQ3LCJpYXQiOjE3ODAwMjkzNDcsImp0aSI6ImQ4ZjE1YTRmODc5NzRjZGViZDEzYzc1ZTU4ZDk3ZjEwIiwidXNlcl9pZCI6IjEifQ.kkQVT2pcYeS_TxlJ6QPU3rNOlZhOv96pyqVEGJI85KA *** todas las rutas get **** response @@ -79,6 +79,22 @@ get customers/ *** products get products/ +*** Productos Inactivos +get products/?active=false + +*** Productos Activos +get products/?active=true + +*** Traer todos los productos +get products/?active=all + +*** Inactiva productos +patch products/1 + +{ + "active": false +} + *** Obtener Ventas por catalogo get catalog_sales/ diff --git a/tienda_ilusion/don_confiao/api_views.py b/tienda_ilusion/don_confiao/api_views.py index 02ad601..fa28919 100644 --- a/tienda_ilusion/don_confiao/api_views.py +++ b/tienda_ilusion/don_confiao/api_views.py @@ -95,6 +95,34 @@ class ProductView(viewsets.ModelViewSet): queryset = Product.objects.all() serializer_class = ProductSerializer + def get_queryset(self): + """ + Filters products by active status for list operations. + Detail operations (retrieve, update, destroy) return all products. + + Query params for list: + - active=true (default): Only active products + - active=false: Only inactive products + - active=all: All products regardless of status + """ + queryset = Product.objects.all() + + # Only filter for list action, not for detail operations + 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: + # Default behavior: return only active products + return queryset.filter(active=True) + class CustomerView(viewsets.ModelViewSet): queryset = Customer.objects.all() @@ -120,21 +148,15 @@ class ReconciliateJarView(APIView): status=HTTP_400_BAD_REQUEST, ) reconciliation = serializer.save() - other_purchases = self._get_other_purchases( - data.get("other_totals") - ) + other_purchases = self._get_other_purchases(data.get("other_totals")) - self._link_purchases( - reconciliation, cash_purchases, other_purchases - ) + self._link_purchases(reconciliation, cash_purchases, other_purchases) return Response({"id": reconciliation.id}) return Response(serializer.errors, status=HTTP_400_BAD_REQUEST) def get(self, request): reconciliations = ReconciliationJar.objects.all() - serializer = ReconciliationJarSerializer( - reconciliations, many=True - ) + serializer = ReconciliationJarSerializer(reconciliations, many=True) return Response(serializer.data) def _is_valid_total(self, purchases, total): @@ -153,9 +175,7 @@ class ReconciliateJarView(APIView): return Sale.objects.filter(pk__in=purchases) return [] - def _link_purchases( - self, reconciliation, cash_purchases, other_purchases - ): + def _link_purchases(self, reconciliation, cash_purchases, other_purchases): for purchase in cash_purchases: purchase.reconciliation = reconciliation purchase.clean() @@ -169,9 +189,7 @@ class ReconciliateJarView(APIView): class PaymentMethodView(APIView): def get(self, request): - serializer = PaymentMethodSerializer( - PaymentMethods.choices, many=True - ) + serializer = PaymentMethodSerializer(PaymentMethods.choices, many=True) return Response(serializer.data) @@ -255,23 +273,17 @@ class SalesToTrytonView(APIView): for sale in sales: try: lines = SaleLine.objects.filter(sale=sale.id) - tryton_params = self.__to_tryton_params( - sale, lines, tryton_context - ) + tryton_params = self.__to_tryton_params(sale, lines, tryton_context) external_ids = tryton_client.call(method, tryton_params) sale.external_id = external_ids[0] sale.save() successful.append(sale.id) except Exception as e: - print( - f"Error al enviar la venta: {e}" f"venta_id: {sale.id}" - ) + print(f"Error al enviar la venta: {e}venta_id: {sale.id}") failed.append(sale.id) continue - return Response( - {"successful": successful, "failed": failed}, status=200 - ) + return Response({"successful": successful, "failed": failed}, status=200) def __to_tryton_params(self, sale, lines, tryton_context): sale_tryton = TrytonSale(sale, lines) @@ -279,7 +291,6 @@ class SalesToTrytonView(APIView): class TrytonSale: - def __init__(self, sale, lines): self.sale = sale self.lines = lines @@ -299,18 +310,14 @@ class TrytonSale: "invoice_address": self.sale.customer.address_external_id, "currency": TRYTON_COP_CURRENCY, "comment": self.sale.description or "", - "description": "Metodo pago: " - + str(self.sale.payment_method or ""), + "description": "Metodo pago: " + str(self.sale.payment_method or ""), "party": self.sale.customer.external_id, "reference": "don_confiao " + str(self.sale.id), "sale_date": self._format_date(self.sale.date), "lines": [ [ "create", - [ - TrytonLineSale(line).to_tryton() - for line in self.lines - ], + [TrytonLineSale(line).to_tryton() for line in self.lines], ] ], "self_pick_up": True, @@ -366,9 +373,7 @@ class ProductsFromTrytonView(APIView): for tryton_product in tryton_products: try: - product = Product.objects.get( - external_id=tryton_product.get("id") - ) + product = Product.objects.get(external_id=tryton_product.get("id")) except Product.DoesNotExist: try: product = self.__create_product(tryton_product) @@ -376,8 +381,7 @@ class ProductsFromTrytonView(APIView): continue except Exception as e: print( - f"Error al importar productos: {e}" - f"El producto: {tryton_product}" + f"Error al importar productos: {e}El producto: {tryton_product}" ) failed_products.append(tryton_product.get("id")) continue @@ -399,9 +403,7 @@ class ProductsFromTrytonView(APIView): status=200, ) - def __get_product_datails_from_tryton( - self, product_ids, tryton_client, context - ): + def __get_product_datails_from_tryton(self, product_ids, tryton_client, context): tryton_fields = [ "id", "name", @@ -459,9 +461,7 @@ class CustomersFromTrytonView(APIView): context = {"company": 1} params = [[], 0, 1000, [["name", "ASC"], ["id", None]], context] party_ids = tryton_client.call(method, params) - tryton_parties = self.__get_party_datails( - party_ids, tryton_client, context - ) + tryton_parties = self.__get_party_datails(party_ids, tryton_client, context) checked_tryton_parties = party_ids failed_parties = [] updated_customers = [] @@ -470,9 +470,7 @@ class CustomersFromTrytonView(APIView): for tryton_party in tryton_parties: try: - customer = Customer.objects.get( - external_id=tryton_party.get("id") - ) + customer = Customer.objects.get(external_id=tryton_party.get("id")) except Customer.DoesNotExist: customer = self.__create_customer(tryton_party) created_customers.append(customer.id) @@ -504,10 +502,7 @@ class CustomersFromTrytonView(APIView): def __need_update(self, customer, tryton_party): if not customer.name == tryton_party.get("name"): return True - if ( - tryton_party.get("addresses") - and tryton_party.get("addresses")[0] - ): + if tryton_party.get("addresses") and tryton_party.get("addresses")[0]: if not customer.address_external_id == str( tryton_party.get("addresses")[0] ): @@ -517,10 +512,7 @@ class CustomersFromTrytonView(APIView): customer = Customer() customer.name = tryton_party.get("name") customer.external_id = tryton_party.get("id") - if ( - tryton_party.get("addresses") - and tryton_party.get("addresses")[0] - ): + if tryton_party.get("addresses") and tryton_party.get("addresses")[0]: customer.address_external_id = tryton_party.get("addresses")[0] customer.save() return customer @@ -528,9 +520,6 @@ class CustomersFromTrytonView(APIView): def __update_customer(self, customer, tryton_party): customer.name = tryton_party.get("name") customer.external_id = tryton_party.get("id") - if ( - tryton_party.get("addresses") - and tryton_party.get("addresses")[0] - ): + if tryton_party.get("addresses") and tryton_party.get("addresses")[0]: customer.address_external_id = tryton_party.get("addresses")[0] customer.save() diff --git a/tienda_ilusion/don_confiao/migrations/0046_product_active.py b/tienda_ilusion/don_confiao/migrations/0046_product_active.py new file mode 100644 index 0000000..b134111 --- /dev/null +++ b/tienda_ilusion/don_confiao/migrations/0046_product_active.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.6 on 2026-05-29 04:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('don_confiao', '0045_catalogsale_catalogsaleline'), + ] + + operations = [ + migrations.AddField( + model_name='product', + name='active', + field=models.BooleanField(default=True), + ), + ] diff --git a/tienda_ilusion/don_confiao/models/products.py b/tienda_ilusion/don_confiao/models/products.py index bc82226..9cc63f4 100644 --- a/tienda_ilusion/don_confiao/models/products.py +++ b/tienda_ilusion/don_confiao/models/products.py @@ -14,6 +14,7 @@ class ProductCategory(models.Model): class Product(models.Model): + active = models.BooleanField(default=True) name = models.CharField(max_length=100, unique=True) price = models.DecimalField(max_digits=9, decimal_places=2) measuring_unit = models.CharField( diff --git a/tienda_ilusion/don_confiao/serializers.py b/tienda_ilusion/don_confiao/serializers.py index f28b15b..423a5de 100644 --- a/tienda_ilusion/don_confiao/serializers.py +++ b/tienda_ilusion/don_confiao/serializers.py @@ -82,6 +82,7 @@ class ProductSerializer(serializers.ModelSerializer): fields = [ "id", "name", + "active", "price", "measuring_unit", "categories", diff --git a/tienda_ilusion/don_confiao/tests/test_products.py b/tienda_ilusion/don_confiao/tests/test_products.py index 1f41fb6..bb190fb 100644 --- a/tienda_ilusion/don_confiao/tests/test_products.py +++ b/tienda_ilusion/don_confiao/tests/test_products.py @@ -1,6 +1,9 @@ from django.test import Client, TestCase from django.conf import settings +from rest_framework.test import APITestCase +from rest_framework import status from ..models.products import ProductCategory, Product +from .Mixins import LoginMixin import os import json @@ -36,6 +39,211 @@ class TestProducts(TestCase): app_dir = os.path.join(settings.BASE_DIR, app_name) example_csv = os.path.join(app_dir, csv_file) with open(example_csv, "rb") as csv: - self.client.post( - "/don_confiao/importar_productos", {"csv_file": csv} - ) + self.client.post("/don_confiao/importar_productos", {"csv_file": csv}) + + +class TestProductsAPIFiltering(APITestCase, LoginMixin): + """Tests for filtering products by active status via API""" + + def setUp(self): + self.login() + + # Create active products + self.active_product_1 = Product.objects.create( + name="Active Product 1", price=100.00, active=True + ) + self.active_product_2 = Product.objects.create( + name="Active Product 2", price=200.00, active=True + ) + + # Create inactive products + self.inactive_product_1 = Product.objects.create( + name="Inactive Product 1", price=150.00, active=False + ) + self.inactive_product_2 = Product.objects.create( + name="Inactive Product 2", price=250.00, active=False + ) + + def test_get_products_default_returns_only_active(self): + """By default, API should return only active products""" + response = self.client.get("/don_confiao/api/products/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = response.json() + self.assertEqual(len(data), 2) + + product_names = [p["name"] for p in data] + self.assertIn("Active Product 1", product_names) + self.assertIn("Active Product 2", product_names) + self.assertNotIn("Inactive Product 1", product_names) + self.assertNotIn("Inactive Product 2", product_names) + + def test_get_products_active_true(self): + """Filter products with active=true should return only active products""" + response = self.client.get("/don_confiao/api/products/?active=true") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = response.json() + self.assertEqual(len(data), 2) + + for product in data: + self.assertTrue(product["active"]) + + def test_get_products_active_false(self): + """Filter products with active=false should return only inactive products""" + response = self.client.get("/don_confiao/api/products/?active=false") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = response.json() + self.assertEqual(len(data), 2) + + for product in data: + self.assertFalse(product["active"]) + + product_names = [p["name"] for p in data] + self.assertIn("Inactive Product 1", product_names) + self.assertIn("Inactive Product 2", product_names) + + def test_get_products_active_all(self): + """Filter products with active=all should return all products""" + response = self.client.get("/don_confiao/api/products/?active=all") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = response.json() + self.assertEqual(len(data), 4) + + def test_get_products_active_variations(self): + """Test different variations of true/false values""" + # Test '1' for true + response = self.client.get("/don_confiao/api/products/?active=1") + self.assertEqual(len(response.json()), 2) + + # Test 'yes' for true + response = self.client.get("/don_confiao/api/products/?active=yes") + self.assertEqual(len(response.json()), 2) + + # Test '0' for false + response = self.client.get("/don_confiao/api/products/?active=0") + self.assertEqual(len(response.json()), 2) + + # Test 'no' for false + response = self.client.get("/don_confiao/api/products/?active=no") + self.assertEqual(len(response.json()), 2) + + def test_get_product_detail_regardless_of_status(self): + """Getting a specific product by ID should work regardless of active status""" + # Get active product + response = self.client.get( + f"/don_confiao/api/products/{self.active_product_1.id}/" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()["name"], "Active Product 1") + + # Get inactive product + response = self.client.get( + f"/don_confiao/api/products/{self.inactive_product_1.id}/" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()["name"], "Inactive Product 1") + + +class TestProductsAPIActivation(APITestCase, LoginMixin): + """Tests for activating/deactivating products via API""" + + def setUp(self): + self.login() + + self.product = Product.objects.create( + name="Test Product", price=100.00, active=True + ) + + def test_deactivate_product_via_patch(self): + """PATCH request should be able to deactivate a product""" + response = self.client.patch( + f"/don_confiao/api/products/{self.product.id}/", + {"active": False}, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Verify product was deactivated + self.product.refresh_from_db() + self.assertFalse(self.product.active) + + # Verify response contains updated data + self.assertFalse(response.json()["active"]) + + def test_activate_product_via_patch(self): + """PATCH request should be able to activate a product""" + # First deactivate the product + self.product.active = False + self.product.save() + + response = self.client.patch( + f"/don_confiao/api/products/{self.product.id}/", + {"active": True}, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Verify product was activated + self.product.refresh_from_db() + self.assertTrue(self.product.active) + + # Verify response contains updated data + self.assertTrue(response.json()["active"]) + + def test_update_other_fields_preserves_active_status(self): + """Updating other fields should not affect active status""" + response = self.client.patch( + f"/don_confiao/api/products/{self.product.id}/", + {"price": "250.00"}, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Verify active status was preserved + self.product.refresh_from_db() + self.assertTrue(self.product.active) + self.assertEqual(self.product.price, 250.00) + + def test_deactivated_product_not_in_default_list(self): + """After deactivating a product, it should not appear in default list""" + # Deactivate product + self.client.patch( + f"/don_confiao/api/products/{self.product.id}/", + {"active": False}, + format="json", + ) + + # Get default product list + response = self.client.get("/don_confiao/api/products/") + data = response.json() + + # Product should not be in list + product_ids = [p["id"] for p in data] + self.assertNotIn(self.product.id, product_ids) + + def test_activated_product_appears_in_default_list(self): + """After activating a product, it should appear in default list""" + # Deactivate product first + self.product.active = False + self.product.save() + + # Activate product + self.client.patch( + f"/don_confiao/api/products/{self.product.id}/", + {"active": True}, + format="json", + ) + + # Get default product list + response = self.client.get("/don_confiao/api/products/") + data = response.json() + + # Product should be in list + product_ids = [p["id"] for p in data] + self.assertIn(self.product.id, product_ids)