feat: add product activation/deactivation and filtering by active status
- Add 'active' boolean field to Product model with default=True - Implement ProductView.get_queryset() to filter products by active status - Default behavior: return only active products - Support query params: ?active=true|false|all - Support variations: 1/0, yes/no for true/false - Detail operations (GET/PATCH/DELETE by ID) work with all products - Update ProductSerializer to include 'active' field - Add comprehensive test suite (11 new tests): - Test filtering by active/inactive/all products - Test parameter variations (1, yes, 0, no) - Test PATCH to activate/deactivate products - Test default list behavior after status changes - Update API documentation in doc/requests.org with examples - All tests passing (13 product tests + 8 API tests)
This commit is contained in:
@@ -14,8 +14,8 @@ post /token/
|
|||||||
**** respuesta
|
**** respuesta
|
||||||
#+begin_src json
|
#+begin_src json
|
||||||
{
|
{
|
||||||
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTc4MDEwMzY0NiwiaWF0IjoxNzgwMDE3MjQ2LCJqdGkiOiI1NDk2NmQ0YTFmMGE0OWNjOGU5MGY5MmZmMTE0ZTMwZCIsInVzZXJfaWQiOiIxIn0.uWIe0Xm9i9eI4fFaM3Ha3FrIaQLfwvpHwbJue3OvhTo",
|
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTc4MDExNTc0NywiaWF0IjoxNzgwMDI5MzQ3LCJqdGkiOiIxNmVjZGMxZmY4Y2Y0MzA4ODM3ZjM5Y2ZiNjQwNmZiMCIsInVzZXJfaWQiOiIxIn0.wmN-wp3Izv0NrfL_ap_i8eyg29w-foHNrQCCL6HoZWg",
|
||||||
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzgwMDE5MDQ2LCJpYXQiOjE3ODAwMTcyNDYsImp0aSI6IjQzYmYzOGM0ZWY3MTQ1YTk5ZjliMTQzODMyYjEwZmVkIiwidXNlcl9pZCI6IjEifQ.LMxWs0bHejpgcZvCpMCqfe5ue3YxAaWUweWHHoHhoH0"
|
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzgwMDMxMTQ3LCJpYXQiOjE3ODAwMjkzNDcsImp0aSI6ImQ4ZjE1YTRmODc5NzRjZGViZDEzYzc1ZTU4ZDk3ZjEwIiwidXNlcl9pZCI6IjEifQ.kkQVT2pcYeS_TxlJ6QPU3rNOlZhOv96pyqVEGJI85KA"
|
||||||
}
|
}
|
||||||
#+end_src
|
#+end_src
|
||||||
*** Perfil de usuario
|
*** Perfil de usuario
|
||||||
@@ -47,7 +47,7 @@ post /token/refresh/
|
|||||||
** Don confiao :verb:
|
** Don confiao :verb:
|
||||||
template http://localhost:7000/don_confiao/api/
|
template http://localhost:7000/don_confiao/api/
|
||||||
Content-Type: application/json;
|
Content-Type: application/json;
|
||||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzgwMDE5MDQ2LCJpYXQiOjE3ODAwMTcyNDYsImp0aSI6IjQzYmYzOGM0ZWY3MTQ1YTk5ZjliMTQzODMyYjEwZmVkIiwidXNlcl9pZCI6IjEifQ.LMxWs0bHejpgcZvCpMCqfe5ue3YxAaWUweWHHoHhoH0
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzgwMDMxMTQ3LCJpYXQiOjE3ODAwMjkzNDcsImp0aSI6ImQ4ZjE1YTRmODc5NzRjZGViZDEzYzc1ZTU4ZDk3ZjEwIiwidXNlcl9pZCI6IjEifQ.kkQVT2pcYeS_TxlJ6QPU3rNOlZhOv96pyqVEGJI85KA
|
||||||
*** todas las rutas
|
*** todas las rutas
|
||||||
get
|
get
|
||||||
**** response
|
**** response
|
||||||
@@ -79,6 +79,22 @@ get customers/
|
|||||||
*** products
|
*** products
|
||||||
get 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
|
*** Obtener Ventas por catalogo
|
||||||
get catalog_sales/
|
get catalog_sales/
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,34 @@ class ProductView(viewsets.ModelViewSet):
|
|||||||
queryset = Product.objects.all()
|
queryset = Product.objects.all()
|
||||||
serializer_class = ProductSerializer
|
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):
|
class CustomerView(viewsets.ModelViewSet):
|
||||||
queryset = Customer.objects.all()
|
queryset = Customer.objects.all()
|
||||||
@@ -120,21 +148,15 @@ class ReconciliateJarView(APIView):
|
|||||||
status=HTTP_400_BAD_REQUEST,
|
status=HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
reconciliation = serializer.save()
|
reconciliation = serializer.save()
|
||||||
other_purchases = self._get_other_purchases(
|
other_purchases = self._get_other_purchases(data.get("other_totals"))
|
||||||
data.get("other_totals")
|
|
||||||
)
|
|
||||||
|
|
||||||
self._link_purchases(
|
self._link_purchases(reconciliation, cash_purchases, other_purchases)
|
||||||
reconciliation, cash_purchases, other_purchases
|
|
||||||
)
|
|
||||||
return Response({"id": reconciliation.id})
|
return Response({"id": reconciliation.id})
|
||||||
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
reconciliations = ReconciliationJar.objects.all()
|
reconciliations = ReconciliationJar.objects.all()
|
||||||
serializer = ReconciliationJarSerializer(
|
serializer = ReconciliationJarSerializer(reconciliations, many=True)
|
||||||
reconciliations, many=True
|
|
||||||
)
|
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
def _is_valid_total(self, purchases, total):
|
def _is_valid_total(self, purchases, total):
|
||||||
@@ -153,9 +175,7 @@ class ReconciliateJarView(APIView):
|
|||||||
return Sale.objects.filter(pk__in=purchases)
|
return Sale.objects.filter(pk__in=purchases)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _link_purchases(
|
def _link_purchases(self, reconciliation, cash_purchases, other_purchases):
|
||||||
self, reconciliation, cash_purchases, other_purchases
|
|
||||||
):
|
|
||||||
for purchase in cash_purchases:
|
for purchase in cash_purchases:
|
||||||
purchase.reconciliation = reconciliation
|
purchase.reconciliation = reconciliation
|
||||||
purchase.clean()
|
purchase.clean()
|
||||||
@@ -169,9 +189,7 @@ class ReconciliateJarView(APIView):
|
|||||||
|
|
||||||
class PaymentMethodView(APIView):
|
class PaymentMethodView(APIView):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
serializer = PaymentMethodSerializer(
|
serializer = PaymentMethodSerializer(PaymentMethods.choices, many=True)
|
||||||
PaymentMethods.choices, many=True
|
|
||||||
)
|
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
@@ -255,23 +273,17 @@ class SalesToTrytonView(APIView):
|
|||||||
for sale in sales:
|
for sale in sales:
|
||||||
try:
|
try:
|
||||||
lines = SaleLine.objects.filter(sale=sale.id)
|
lines = SaleLine.objects.filter(sale=sale.id)
|
||||||
tryton_params = self.__to_tryton_params(
|
tryton_params = self.__to_tryton_params(sale, lines, tryton_context)
|
||||||
sale, lines, tryton_context
|
|
||||||
)
|
|
||||||
external_ids = tryton_client.call(method, tryton_params)
|
external_ids = tryton_client.call(method, tryton_params)
|
||||||
sale.external_id = external_ids[0]
|
sale.external_id = external_ids[0]
|
||||||
sale.save()
|
sale.save()
|
||||||
successful.append(sale.id)
|
successful.append(sale.id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(
|
print(f"Error al enviar la venta: {e}venta_id: {sale.id}")
|
||||||
f"Error al enviar la venta: {e}" f"venta_id: {sale.id}"
|
|
||||||
)
|
|
||||||
failed.append(sale.id)
|
failed.append(sale.id)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return Response(
|
return Response({"successful": successful, "failed": failed}, status=200)
|
||||||
{"successful": successful, "failed": failed}, status=200
|
|
||||||
)
|
|
||||||
|
|
||||||
def __to_tryton_params(self, sale, lines, tryton_context):
|
def __to_tryton_params(self, sale, lines, tryton_context):
|
||||||
sale_tryton = TrytonSale(sale, lines)
|
sale_tryton = TrytonSale(sale, lines)
|
||||||
@@ -279,7 +291,6 @@ class SalesToTrytonView(APIView):
|
|||||||
|
|
||||||
|
|
||||||
class TrytonSale:
|
class TrytonSale:
|
||||||
|
|
||||||
def __init__(self, sale, lines):
|
def __init__(self, sale, lines):
|
||||||
self.sale = sale
|
self.sale = sale
|
||||||
self.lines = lines
|
self.lines = lines
|
||||||
@@ -299,18 +310,14 @@ class TrytonSale:
|
|||||||
"invoice_address": self.sale.customer.address_external_id,
|
"invoice_address": self.sale.customer.address_external_id,
|
||||||
"currency": TRYTON_COP_CURRENCY,
|
"currency": TRYTON_COP_CURRENCY,
|
||||||
"comment": self.sale.description or "",
|
"comment": self.sale.description or "",
|
||||||
"description": "Metodo pago: "
|
"description": "Metodo pago: " + str(self.sale.payment_method or ""),
|
||||||
+ str(self.sale.payment_method or ""),
|
|
||||||
"party": self.sale.customer.external_id,
|
"party": self.sale.customer.external_id,
|
||||||
"reference": "don_confiao " + str(self.sale.id),
|
"reference": "don_confiao " + str(self.sale.id),
|
||||||
"sale_date": self._format_date(self.sale.date),
|
"sale_date": self._format_date(self.sale.date),
|
||||||
"lines": [
|
"lines": [
|
||||||
[
|
[
|
||||||
"create",
|
"create",
|
||||||
[
|
[TrytonLineSale(line).to_tryton() for line in self.lines],
|
||||||
TrytonLineSale(line).to_tryton()
|
|
||||||
for line in self.lines
|
|
||||||
],
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"self_pick_up": True,
|
"self_pick_up": True,
|
||||||
@@ -366,9 +373,7 @@ class ProductsFromTrytonView(APIView):
|
|||||||
|
|
||||||
for tryton_product in tryton_products:
|
for tryton_product in tryton_products:
|
||||||
try:
|
try:
|
||||||
product = Product.objects.get(
|
product = Product.objects.get(external_id=tryton_product.get("id"))
|
||||||
external_id=tryton_product.get("id")
|
|
||||||
)
|
|
||||||
except Product.DoesNotExist:
|
except Product.DoesNotExist:
|
||||||
try:
|
try:
|
||||||
product = self.__create_product(tryton_product)
|
product = self.__create_product(tryton_product)
|
||||||
@@ -376,8 +381,7 @@ class ProductsFromTrytonView(APIView):
|
|||||||
continue
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(
|
print(
|
||||||
f"Error al importar productos: {e}"
|
f"Error al importar productos: {e}El producto: {tryton_product}"
|
||||||
f"El producto: {tryton_product}"
|
|
||||||
)
|
)
|
||||||
failed_products.append(tryton_product.get("id"))
|
failed_products.append(tryton_product.get("id"))
|
||||||
continue
|
continue
|
||||||
@@ -399,9 +403,7 @@ class ProductsFromTrytonView(APIView):
|
|||||||
status=200,
|
status=200,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __get_product_datails_from_tryton(
|
def __get_product_datails_from_tryton(self, product_ids, tryton_client, context):
|
||||||
self, product_ids, tryton_client, context
|
|
||||||
):
|
|
||||||
tryton_fields = [
|
tryton_fields = [
|
||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
@@ -459,9 +461,7 @@ class CustomersFromTrytonView(APIView):
|
|||||||
context = {"company": 1}
|
context = {"company": 1}
|
||||||
params = [[], 0, 1000, [["name", "ASC"], ["id", None]], context]
|
params = [[], 0, 1000, [["name", "ASC"], ["id", None]], context]
|
||||||
party_ids = tryton_client.call(method, params)
|
party_ids = tryton_client.call(method, params)
|
||||||
tryton_parties = self.__get_party_datails(
|
tryton_parties = self.__get_party_datails(party_ids, tryton_client, context)
|
||||||
party_ids, tryton_client, context
|
|
||||||
)
|
|
||||||
checked_tryton_parties = party_ids
|
checked_tryton_parties = party_ids
|
||||||
failed_parties = []
|
failed_parties = []
|
||||||
updated_customers = []
|
updated_customers = []
|
||||||
@@ -470,9 +470,7 @@ class CustomersFromTrytonView(APIView):
|
|||||||
|
|
||||||
for tryton_party in tryton_parties:
|
for tryton_party in tryton_parties:
|
||||||
try:
|
try:
|
||||||
customer = Customer.objects.get(
|
customer = Customer.objects.get(external_id=tryton_party.get("id"))
|
||||||
external_id=tryton_party.get("id")
|
|
||||||
)
|
|
||||||
except Customer.DoesNotExist:
|
except Customer.DoesNotExist:
|
||||||
customer = self.__create_customer(tryton_party)
|
customer = self.__create_customer(tryton_party)
|
||||||
created_customers.append(customer.id)
|
created_customers.append(customer.id)
|
||||||
@@ -504,10 +502,7 @@ class CustomersFromTrytonView(APIView):
|
|||||||
def __need_update(self, customer, tryton_party):
|
def __need_update(self, customer, tryton_party):
|
||||||
if not customer.name == tryton_party.get("name"):
|
if not customer.name == tryton_party.get("name"):
|
||||||
return True
|
return True
|
||||||
if (
|
if tryton_party.get("addresses") and tryton_party.get("addresses")[0]:
|
||||||
tryton_party.get("addresses")
|
|
||||||
and tryton_party.get("addresses")[0]
|
|
||||||
):
|
|
||||||
if not customer.address_external_id == str(
|
if not customer.address_external_id == str(
|
||||||
tryton_party.get("addresses")[0]
|
tryton_party.get("addresses")[0]
|
||||||
):
|
):
|
||||||
@@ -517,10 +512,7 @@ class CustomersFromTrytonView(APIView):
|
|||||||
customer = Customer()
|
customer = Customer()
|
||||||
customer.name = tryton_party.get("name")
|
customer.name = tryton_party.get("name")
|
||||||
customer.external_id = tryton_party.get("id")
|
customer.external_id = tryton_party.get("id")
|
||||||
if (
|
if tryton_party.get("addresses") and tryton_party.get("addresses")[0]:
|
||||||
tryton_party.get("addresses")
|
|
||||||
and tryton_party.get("addresses")[0]
|
|
||||||
):
|
|
||||||
customer.address_external_id = tryton_party.get("addresses")[0]
|
customer.address_external_id = tryton_party.get("addresses")[0]
|
||||||
customer.save()
|
customer.save()
|
||||||
return customer
|
return customer
|
||||||
@@ -528,9 +520,6 @@ class CustomersFromTrytonView(APIView):
|
|||||||
def __update_customer(self, customer, tryton_party):
|
def __update_customer(self, customer, tryton_party):
|
||||||
customer.name = tryton_party.get("name")
|
customer.name = tryton_party.get("name")
|
||||||
customer.external_id = tryton_party.get("id")
|
customer.external_id = tryton_party.get("id")
|
||||||
if (
|
if tryton_party.get("addresses") and tryton_party.get("addresses")[0]:
|
||||||
tryton_party.get("addresses")
|
|
||||||
and tryton_party.get("addresses")[0]
|
|
||||||
):
|
|
||||||
customer.address_external_id = tryton_party.get("addresses")[0]
|
customer.address_external_id = tryton_party.get("addresses")[0]
|
||||||
customer.save()
|
customer.save()
|
||||||
|
|||||||
18
tienda_ilusion/don_confiao/migrations/0046_product_active.py
Normal file
18
tienda_ilusion/don_confiao/migrations/0046_product_active.py
Normal file
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -14,6 +14,7 @@ class ProductCategory(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Product(models.Model):
|
class Product(models.Model):
|
||||||
|
active = models.BooleanField(default=True)
|
||||||
name = models.CharField(max_length=100, unique=True)
|
name = models.CharField(max_length=100, unique=True)
|
||||||
price = models.DecimalField(max_digits=9, decimal_places=2)
|
price = models.DecimalField(max_digits=9, decimal_places=2)
|
||||||
measuring_unit = models.CharField(
|
measuring_unit = models.CharField(
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ class ProductSerializer(serializers.ModelSerializer):
|
|||||||
fields = [
|
fields = [
|
||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
|
"active",
|
||||||
"price",
|
"price",
|
||||||
"measuring_unit",
|
"measuring_unit",
|
||||||
"categories",
|
"categories",
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
from rest_framework import status
|
||||||
from ..models.products import ProductCategory, Product
|
from ..models.products import ProductCategory, Product
|
||||||
|
from .Mixins import LoginMixin
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
@@ -36,6 +39,211 @@ class TestProducts(TestCase):
|
|||||||
app_dir = os.path.join(settings.BASE_DIR, app_name)
|
app_dir = os.path.join(settings.BASE_DIR, app_name)
|
||||||
example_csv = os.path.join(app_dir, csv_file)
|
example_csv = os.path.join(app_dir, csv_file)
|
||||||
with open(example_csv, "rb") as csv:
|
with open(example_csv, "rb") as csv:
|
||||||
self.client.post(
|
self.client.post("/don_confiao/importar_productos", {"csv_file": csv})
|
||||||
"/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)
|
||||||
|
|||||||
Reference in New Issue
Block a user