Saltar a contenido

📚 Unidad ISS-12 · Consulta y anulación de ventas — capa 🧠 APRENDER

🛠 Construir este bloque → · ✅ Condiciones de cierre (GATE) · 📝 Evaluación

Capa Página Para qué
🧠 Aprender esta página comprender, explicar y relacionar
🛠 Construir Consulta y anulación de ventas ejecutar, programar y verificar
✅ GATE Cierre de la unidad condición para pasar al bloque siguiente

Mapa de correspondencias. Cada fila enlaza el mismo tema en las dos capas; los enlaces apuntan a secciones reales del material.

Tema 🧠 Aprender (esta página) 🛠 Construir (ISS técnico)
Objetivo A. Objetivos y Requisitos de ISS-12 Objetivo
Recorrido C. Estructura de Archivos y Paquetes Construcción
Cierre J. Criterios de Aceptación (AC-12-01 al AC-12-04) Criterios de aceptación · GATE
Evaluación L. Cuestionario de Preguntas de Defensa Oral Técnica GATE

🖥 Presentación del ISS

Presentación de la unidad. Diapositivas de ISS-12 — Consulta y anulación de ventas (9 diapositivas). Se visualiza aquí, dentro del sitio.

⛶ Ver presentación completa ⬇ Archivo editable (.pptx)

9 diapositivas · se visualiza dentro del sitio.

Infografía

Mapa conceptual

  • Vistas y mixins
  • SaleReadViewSet
  • ListModelMixin
  • RetrieveModelMixin
  • DestroyModelMixin
  • GenericViewSet
  • Exclusión de Update y Create
  • Enrutamiento manual
  • apps/sale/urls.py
  • Detalle sales//
  • Acciones retrieve y destroy
  • Reutilización de sales/ para listado
  • Método void e idempotencia
  • Verificador de status INACTIVE
  • Prevención de doble stock
  • transaction.atomic()
  • select_for_update()
  • Preservación de precio histórico
  • Inmutabilidad de unit_price
  • Independencia de Product.price
  • Verificación y pruebas APITestCase
  • test_detail_keeps_historical_price
  • test_list_returns_the_registered_sale
  • test_void_restores_stock_once
  • Acceso OPEN
  • Criterios y gate
  • AC-12-01: Detalle con precio histórico
  • AC-12-02: DELETE responde 204
  • AC-12-03: Fila sales en INACTIVE
  • AC-12-04: Idempotencia de stock
  • EVI-12-01: Evidencia PASS

Guía de Estudio Pedagógica: ISS-12 — Consulta y Anulación de Ventas

A. Objetivos y Requisitos de ISS-12

Objetivo Principal

Implementar los mecanismos de lectura detallada, listado y anulación comercial del recurso Venta (Sale) y sus líneas de detalle (ProductSale), garantizando la preservación irrestricta del histórico de transacciones en la base de datos (baja lógica) y la reposición exacta e idempotente del inventario de productos.

Requisitos Previos

  • Unidad ISS-11 superada: Existencia del mecanismo transaccional de registro de ventas a través de SaleRegisterAPIView / Sale.objects.register y la definición de modelos en apps/sale/models.py.
  • Entorno activo: Intérprete Python del entorno aislado (.venv) configurado y suite de pruebas previa en estado funcional.

B. Conceptos Esenciales y Vocabulario Técnico

  • SaleReadViewSet: Vista basada en clases que compone mixins explícitos de Django REST Framework (DRF) para gestionar la lectura (listado y detalle) y la baja lógica (anulación) de ventas, restringiendo deliberadamente cualquier mutación directa de importes.
  • ListModelMixin: Mixin de DRF que provee el comportamiento reutilizable para responder a peticiones HTTP GET de colección, retornando una lista serializada de instancias de ventas.
  • RetrieveModelMixin: Mixin de DRF que provee la lógica para responder a peticiones HTTP GET de un recurso individual identificable por su clave primaria (pk).
  • DestroyModelMixin: Mixin de DRF que intercepta peticiones HTTP DELETE para ejecutar la eliminación de un recurso, el cual se sobreescribe para redirigir la acción hacia una baja lógica comercial.
  • GenericViewSet: Clase base de DRF que combina GenericAPIView con la infraestructura de manejo de acciones de un ViewSet, sin incluir métodos HTTP preconfigurados por defecto.
  • as_view({'get': 'retrieve', 'delete': 'destroy'}): Mapeo explícito de verbos HTTP a métodos de acción del viewset, utilizado al definir la ruta en el URLconf sin depender de un enrutador automático.
  • sale.void(): Método de dominio encapsulado en el modelo Sale que ejecuta la reversión transaccional del stock de productos e inactiva la venta y sus líneas activas dentro de un bloque atómico.
  • select_related('client'): Optimización del ORM de Django que realiza un JOIN SQL en la misma consulta para traer la información del cliente de la venta, evitando consultas adicionales.
  • prefetch_related('lines__product'): Optimización del ORM de Django que ejecuta una consulta adicional controlada para precargar las líneas de detalle (ProductSale) y sus respectivos productos vinculados (Product), solucionando el problema de rendimiento N+1.
  • Idempotencia: Propiedad de una operación comercial y técnica por la cual múltiples ejecuciones consecutivas de la misma acción (ej. anular una venta mediante DELETE) producen exactamente el mismo estado en el sistema sin generar efectos secundarios no deseados (como sobre-incrementar el stock).
  • AllowAny: Clase de permiso de DRF utilizada en la Fase I (acceso OPEN) que permite el consumo del endpoint sin exigir encabezados de autenticación ni tokens JWT.

C. Estructura de Archivos y Paquetes

La implementación del ISS-12 requiere la actualización y sincronización de tres archivos principales dentro de la aplicación apps/sale:

apps/sale/
├── models.py      # Lógica de dominio: método sale.void() con transacción atómica
├── views.py       # Exposición de la API: SaleReadViewSet y actualización de SaleCollectionAPIView
├── urls.py        # Mapeo explícito de endpoints HTTP para colección y detalle
└── tests.py       # Pruebas de integración HTTP para consulta, precio histórico y anulación

D. Explicación por Bloques Semánticos de SaleReadViewSet en apps/sale/views.py

Código Fuente de SaleReadViewSet

from rest_framework import mixins, viewsets
from rest_framework.permissions import AllowAny

from apps.sale.models import Sale
from apps.sale.serializers import SaleSerializer


class SaleReadViewSet(
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    mixins.DestroyModelMixin,
    viewsets.GenericViewSet,
):
    serializer_class = SaleSerializer
    permission_classes = [AllowAny]
    lookup_value_converter = "int"

    def get_queryset(self):
        return Sale.objects.select_related("client").prefetch_related("lines__product")

    def perform_destroy(self, instance):
        instance.void()

Explicación Bloque por Bloque

  1. Herencia de Mixins Explícitos:
  2. Qué es: Se hereda explícitamente de mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.DestroyModelMixin y viewsets.GenericViewSet.
  3. Por qué existe: En lugar de heredar de ModelViewSet (que expone implícitamente operaciones PUT/PATCH para edición), se seleccionan únicamente las capacidades de listar (List), obtener detalle (Retrieve) y eliminar (Destroy).
  4. Efecto: Garantiza por diseño que la interfaz de la API no exponga endpoints para actualizar de forma arbitraria ventas ya registradas.

  5. Atributos de Configuración de la Vista:

  6. serializer_class = SaleSerializer: Especifica el serializador de lectura que transforma la estructura compleja de la venta (incluyendo la cabecera y el anidamiento de líneas) a representación JSON en sintaxis camelCase (saleDate, unitPrice, lineTotal, etc.).
  7. permission_classes = [AllowAny]: Define el nivel de acceso en Fase I como abierto (OPEN), prescindiendo de autenticación por token.
  8. lookup_value_converter = "int": Fuerza a que el argumento de búsqueda capturado desde la URL sea transformado y validado explícitamente como un número entero.

  9. Método get_queryset:

  10. Qué hace: Retorna la consulta base del modelo Sale optimizada mediante consultas avanzadas del ORM: .select_related("client").prefetch_related("lines__product").
  11. Por qué existe: Evita el problema de rendimiento N+1 consultas. Al serializar una venta con sus líneas y productos, sin estas optimizaciones el ORM ejecutaría 1 consulta para la venta, 1 para el cliente, 1 para las líneas y N consultas individuales para cada producto de las líneas. Con esta configuración, el sistema realiza únicamente 2 consultas SQL en total independientemente del volumen de ítems.

  12. Sobreescritura de perform_destroy(instance):

  13. Qué hace: Reemplaza el comportamiento predeterminado del mixin (que ejecutaría instance.delete() eliminando el registro de la base de datos) delegando la acción directamente en instance.void().
  14. Efecto: Redirige la petición HTTP DELETE para ejecutar la baja lógica de la venta y la reposición de inventario de manera segura.

E. Explicación por Bloques Semánticos de apps/sale/urls.py

Código Fuente de apps/sale/urls.py

from django.urls import path

from apps.sale.views import SaleCollectionAPIView, SaleReadViewSet

urlpatterns = [
    path("sales/", SaleCollectionAPIView.as_view(), name="sale-collection"),
    path(
        "sales/<int:pk>/",
        SaleReadViewSet.as_view({"get": "retrieve", "delete": "destroy"}),
        name="sale-detail",
    ),
]

Explicación por Bloques

  1. Mapeo Explícito en sales/<int:pk>/:
  2. Qué hace: Asocia la ruta de detalle a las acciones específicas de SaleReadViewSet utilizando un diccionario en el método .as_view(): {"get": "retrieve", "delete": "destroy"}.
  3. Efecto: Cuando llega una petición GET /api/sales/1/, DRF invoca la función retrieve. Cuando llega una petición DELETE /api/sales/1/, invoca destroy. Verbos no mapeados como PUT o PATCH responderán automáticamente con un código de estado HTTP 405 Method Not Allowed.

  4. Reutilización del Endpoint sales/ con SaleCollectionAPIView:

  5. Qué hace: El path sales/ atiende peticiones de la colección. En ISS-11 se configuró para peticiones POST de alta. En ISS-12, SaleCollectionAPIView implementa el método get reutilizando el get_queryset() y el serializer_class de SaleReadViewSet.
  6. Efecto: Permite que la misma URL POST /api/sales/ (crear venta) y GET /api/sales/ (listar ventas) funcione de manera coherente sin generar conflictos de enrutamiento.

  7. Justificación de la No Utilización de SimpleRouter:

  8. Razón de Arquitectura: Un SimpleRouter automatiza el registro de rutas basadas en convenciones de un ViewSet. Si se registrara SaleReadViewSet en un enrutador, este intentaría mapear la raíz de la colección y los detalles en patrones genéricos. Dado que el alta transaccional de ventas requiere una lógica de entrada compleja encapsulada en una APIView específica (SaleRegisterAPIView / SaleCollectionAPIView) y la venta no admite modificaciones genéricas, el uso de enrutadores implícitos rompería la separación de responsabilidades y expondría acciones no deseadas. Las rutas escritas manualmente con path() garantizan control absoluto sobre el contrato HTTP.

F. Lógica Profunda del Método sale.void() en apps/sale/models.py

Código Fuente de sale.void()

def void(self):
    with transaction.atomic():
        sale = Sale.objects.select_for_update().get(pk=self.pk)
        if sale.status == RecordStatus.INACTIVE:
            return sale
        lines = sale.lines.select_related("product").select_for_update().order_by("product_id")
        for line in lines:
            if line.status != RecordStatus.ACTIVE:
                continue
            product = Product.objects.select_for_update().get(pk=line.product_id)
            product.stock += line.quantity
            product.save(update_fields=["stock", "updated_at"])
            line.status = RecordStatus.INACTIVE
            line.save(update_fields=["status", "updated_at"])
        sale.status = RecordStatus.INACTIVE
        sale.save(update_fields=["status", "updated_at"])
        return sale

Análisis Técnico Paso a Paso

  1. Contexto Transaccional Atómico (with transaction.atomic():):
  2. Encapsula todas las lecturas y escrituras de la base de datos en una única transacción de BD. Si ocurre un fallo en cualquier punto de la ejecución, la transacción realiza un rollback completo, evitando que el inventario se restablezca parcialmente o que la venta quede inactiva sin devolver el stock.

  3. Bloqueo Pautado y Verificación de Idempotencia Commercial:

  4. Se ejecuta Sale.objects.select_for_update().get(pk=self.pk) para bloquear la fila de la venta a nivel de motor de base de datos hasta que finalice la transacción, previniendo condiciones de carrera (race conditions).
  5. Evaluación de Estado: if sale.status == RecordStatus.INACTIVE: return sale. Si la venta ya fue anulada previamente, el método retorna la instancia inmediatamente sin alterar el inventario ni modificar registros. Esto garantiza que la operación sea idempotente.

  6. Recorrido de Líneas de Detalle (ProductSale) y Reversión de Stock:

  7. Se consultan las líneas de la venta bloqueando cada fila con select_for_update().order_by("product_id"). El ordenamiento determinista por product_id previene bloqueos mutuos (deadlocks) en la base de datos bajo alta concurrencia.
  8. Para cada línea con estado ACTIVE:

    1. Obtiene el producto asociado bloqueando su registro: Product.objects.select_for_update().get(pk=line.product_id).
    2. Devuelve las unidades vendidas al inventario: product.stock += line.quantity.
    3. Guarda la actualización del producto limitando los campos en la sentencia SQL: product.save(update_fields=["stock", "updated_at"]).
    4. Transiciona la línea de detalle a inactiva: line.status = RecordStatus.INACTIVE y persiste el cambio.
  9. Inactivación de la Cabecera de la Venta:

  10. Se actualiza el estado de la venta principal a inactivo: sale.status = RecordStatus.INACTIVE.
  11. Se persiste la cabecera mediante sale.save(update_fields=["status", "updated_at"]).
  12. Se retorna la instancia actualizada.

G. Pruebas de Integración HTTP en Acceso OPEN (apps/sale/tests.py)

Las pruebas del ISS-12 validan el comportamiento end-to-end de la API en la capa HTTP. Se agregan a la clase SaleTransactionTests en apps/sale/tests.py:

def test_detail_keeps_historical_price(self):
    response = self.client.post(
        "/api/sales/",
        self._payload([{"productId": self.water.id, "quantity": 1}]),
        format="json",
    )
    Product.objects.filter(pk=self.water.pk).update(price=Decimal("99.00"))
    detail = self.client.get(f"/api/sales/{response.data['id']}/")
    self.assertEqual(detail.status_code, 200)
    self.assertEqual(detail.data["lines"][0]["unit_price"], "10.00")

def test_list_returns_the_registered_sale(self):
    created = self.client.post(
        "/api/sales/",
        self._payload([{"productId": self.water.id, "quantity": 1}]),
        format="json",
    )
    listed = self.client.get("/api/sales/")
    self.assertEqual(listed.status_code, 200)
    self.assertTrue(any(row["id"] == created.data["id"] for row in listed.data))

def test_void_restores_stock_once(self):
    created = self.client.post(
        "/api/sales/",
        self._payload([{"productId": self.water.id, "quantity": 2}]),
        format="json",
    )
    sale_id = created.data["id"]
    first = self.client.delete(f"/api/sales/{sale_id}/")
    second = self.client.delete(f"/api/sales/{sale_id}/")
    self.assertEqual(first.status_code, 204)
    self.assertEqual(second.status_code, 204)
    self.water.refresh_from_db()
    self.assertEqual(self.water.stock, 5)
    sale = Sale.objects.get(pk=sale_id)
    self.assertEqual(sale.status, RecordStatus.INACTIVE)

Desglose de las Aserciones de Prueba

  1. test_detail_keeps_historical_price:
  2. Registra una venta del producto "Agua" (precio original 10.00).
  3. Vía ORM actualiza directamente el precio vivo en el catálogo de productos a 99.00.
  4. Realiza una petición GET /api/sales/<id>/.
  5. Aserción: Valida que el código sea HTTP 200 y que el campo unit_price reportado en las líneas del JSON mantenga el valor histórico de "10.00", demostrando el desacoplamiento entre el catálogo y la transacción del detalle.

  6. test_list_returns_the_registered_sale:

  7. Registra una venta e invoca GET /api/sales/.
  8. Aserción: Valida respuesta HTTP 200 y comprueba que la venta creada esté presente en el arreglo de elementos retornado.

  9. test_void_restores_stock_once:

  10. Registra una venta por 2 unidades de "Agua" (stock inicial era 5; al vender baja a 3).
  11. Ejecuta la primera petición DELETE /api/sales/<sale_id>/ y verifica código HTTP 204.
  12. Ejecuta inmediatamente una segunda petición DELETE /api/sales/<sale_id>/ sobre la misma venta.
  13. Aserciones:
    • Ambos llamadas responden HTTP 204 (respuesta idempotente).
    • Refresca el objeto desde la BD (self.water.refresh_from_db()) y confirma que el stock se incrementó exactamente a 5 (valor original), demostrando que la segunda petición no sumó stock por segunda vez (previene stock = 7).
    • Valida que el campo status de la venta permanezca en inactive.

H. Análisis Comparativo y Decisiones de Arquitectura

Tipo de Vista Considerado Decisión Justificación Técnica y Comercial
ModelViewSet Descartado Un ModelViewSet genera automáticamente rutas y manejadores para PUT y PATCH. Permitir peticiones PUT/PATCH sobre una venta permitiría alterar importes, clientes o cantidades de líneas en directo. Dichas alteraciones eludirían el registro de inventario y los cálculos transaccionales de subtotales, impuestos y descuentos.
ReadOnlyModelViewSet Descartado Un ReadOnlyModelViewSet solo ofrece los mixins ListModelMixin y RetrieveModelMixin. Si bien protege la integridad impidiendo mutaciones de datos, bloquearía por completo la capacidad de ejecutar peticiones DELETE para la anulación de ventas.
SaleReadViewSet (Mixins Explícitos) SELECCIONADO Se compone combinando ListModelMixin, RetrieveModelMixin, DestroyModelMixin y GenericViewSet. Expone de forma estricta únicamente la lectura (GET) y la anulación (DELETE), impidiendo sintácticamente la modificación de montos o líneas.
Endpoint para ProductSale Descartado Las líneas de venta (ProductSale) carecen de ciclo de vida independiente. No existe el concepto de "crear o borrar una línea de venta suelta" fuera del contexto de su cabecera. Por ende, las líneas se exponen como una colección anidada de solo lectura dentro de la respuesta serializada de la venta (lines).

I. Errores Frecuentes

  1. Uso de QuerySet.delete() o instance.delete() (Borrado Físico):
  2. Error: Permitir que la petición DELETE elimine físicamente la fila en la tabla sales o en product_sales.
  3. Impacto: Se destruye la evidencia contable y fiscal de la transacción, violando la integridad de datos del sistema.
  4. Solución: Sobreescribir perform_destroy en la vista e invocar el método de dominio instance.void().

  5. Omitir la Verificación de Idempotencia en sale.void():

  6. Error: No comprobar si sale.status == RecordStatus.INACTIVE antes de procesar las líneas.
  7. Impacto: Múltiples peticiones DELETE consecutivas (por reintentos de red o clics repetidos del cliente) sumarían el stock N veces en el catálogo de productos.
  8. Solución: Retornar tempranamente si el estado de la venta ya es INACTIVE.

  9. Habilitar Endpoints PUT / PATCH en Ventas:

  10. Error: Usar ModelViewSet o implementar vistas de edición de ventas.
  11. Impacto: Los clientes de la API podrían modificar montos monetarios o productos de una venta ya cerrada sin auditoría ni control de inventario.
  12. Solución: Usar herencia explícita de mixins limitados a List, Retrieve y Destroy.

  13. Omisión de prefetch_related e Incurrir en Consultas N+1:

  14. Error: Definir get_queryset como Sale.objects.all() sin optimización de relaciones.
  15. Impacto: Degradación severa del rendimiento de la base de datos al listar ventas en producción debido a cientos de consultas consecutivas para recuperar clientes y productos línea por línea.
  16. Solución: Configurar Sale.objects.select_related("client").prefetch_related("lines__product").

J. Criterios de Aceptación (AC-12-01 al AC-12-04)

  • AC-12-01: La petición HTTP GET /api/sales/<id>/ retorna la información completa de la venta, incluyendo el arreglo anidado lines donde cada línea conserva su unit_price histórico almacenado en la transacción.
  • AC-12-02: La petición HTTP DELETE /api/sales/<id>/ procesa la anulación de la venta y responde con el código de estado HTTP 204 No Content.
  • AC-12-03: Tras ejecutar la anulación, el registro de la venta en la tabla física sales no se elimina; su campo status cambia a inactive (baja lógica).
  • AC-12-04: La ejecución consecutiva o repetida de anulaciones (DELETE) sobre la misma venta es idempotente: la venta permanece en estado inactive y el stock de los productos involucrados regresa exactamente a su valor original sin incrementarse de más.

K. Verificación, Evidencias (EVI-12-01) y Tabla GATE

Comandos de Verificación

# Ejecución aislada de la suite de pruebas de la aplicación de ventas
python manage.py test apps.sale

Evidencia de Ejecución (EVI-12-01)

Verificación exitosa de las pruebas de integración en el módulo de ventas, incluyendo la aserción de la respuesta HTTP 204 y la conservación del stock exacto tras anulaciones repetidas.

Tabla GATE

Criterio de Aceptación Verificación Técnica Evidencia Resultado
AC-12-01 Inspección del JSON retornado por GET /api/sales/<id>/ verificando estructura de lines y unit_price histórico. EVI-12-01 (test_detail_keeps_historical_price PASS) PASS
AC-12-02 Ejecución de petición DELETE /api/sales/<id>/ en el entorno de prueba. EVI-12-01 (test_void_restores_stock_once PASS) PASS
AC-12-03 Inspección del estado en la tabla sales mediante el ORM comprobando que la fila existe con status = inactive. EVI-12-01 (test_void_restores_stock_once PASS) PASS
AC-12-04 Ejecución consecutiva de dos llamadas DELETE validando respuesta 204 y consistencia del campo stock en Product. EVI-12-01 (test_void_restores_stock_once PASS) PASS

L. Cuestionario de Preguntas de Defensa Oral Técnica

Pregunta 1: ¿Por qué en SaleReadViewSet se utilizó herencia de mixins explícitos (mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.DestroyModelMixin, viewsets.GenericViewSet) en lugar de heredar directamente de ModelViewSet o ReadOnlyModelViewSet?

Respuesta Detallada y Justificación Pedagogíca: Heredar de ModelViewSet habría sido un error de arquitectura grave, ya que este incluye automáticamente manejadores para los métodos HTTP PUT y PATCH (UpdateModelMixin) y POST (CreateModelMixin). En un sistema de ventas, permitir mutaciones HTTP genéricas (PUT/PATCH) sobre una venta registrada violaría la integridad financiera y el control de inventario, pues un cliente de la API podría alterar los totales o las cantidades vendidas sin dejar registro ni actualizar el stock. Por otro lado, heredar de ReadOnlyModelViewSet tampoco era viable porque este restringe la API exclusivamente a lecturas (GET), bloqueando la capacidad de ejecutar peticiones DELETE para anular la venta. La combinación explícita de List, Retrieve y Destroy sobre GenericViewSet expone la interfaz mínima requerida (listar, ver detalle y anular) garantizando por diseño que no existan puntos de entrada para modificaciones no autorizadas.


Pregunta 2: ¿Cómo garantiza el método sale.void() la idempotencia comercial y técnica durante la anulación de una venta, y qué sucedería bajo peticiones HTTP DELETE concurrentes o repetidas si faltara esta verificación?

Respuesta Detallada y Justificación Pedagogíca: La idempotencia en sale.void() se garantiza mediante dos mecanismos complementarios: 1. Verificación de estado de cabecera: Al iniciar la transacción atómica, se consulta la venta aplicando bloqueo pesimista mediante select_for_update(). Si el estado de la venta ya es RecordStatus.INACTIVE, el método interrumpe de inmediato su ejecución retornando el objeto sin realizar ninguna operación adicional (if sale.status == RecordStatus.INACTIVE: return sale). 2. Filtrado de líneas activas: Al recorrer las líneas de la venta, solo se procesan e incrementan el stock de aquellos ítems cuyo estado sea RecordStatus.ACTIVE.

Si se omitiera esta verificación, peticiones HTTP DELETE repetidas (por ejemplo, reintentos automáticos del cliente por latencia de red o llamados maliciosos) volverían a sumar la cantidad de las líneas al inventario en cada ejecución. Si una venta fue de 2 unidades de un producto con stock 5 (que quedó en 3 tras vender), la primera anulación subiría el stock a 5, pero una segunda anulación sin verificación lo subiría a 7, corrompiendo la realidad física del inventario.


Respuesta Detallada y Justificación Pedagogíca: select_related("client") y prefetch_related("lines__product") resuelven el problema de rendimiento conocido como N+1 Consultas SQL. * select_related("client") opera sobre relaciones de clave foránea uno-a-uno o muchos-a-uno (FK simple). Funciona realizando un JOIN de SQL directamente en la misma consulta que recupera la venta. Es ideal para traer la información del cliente asociado a la cabecera. * prefetch_related("lines__product") opera sobre relaciones de uno-a-muchos o través de múltiples saltos (como de Sale a sus muchas ProductSale, y de cada ProductSale a su Product). El ORM ejecuta una segunda consulta SQL independiente utilizando un operador IN para traer todas las líneas y productos asociados a las ventas recuperadas en la primera consulta, realizando el acoplamiento en la memoria de Python.

Sin estas optimizaciones, al listar 100 ventas, el ORM realizaría 1 consulta para obtener las ventas, 100 consultas para obtener el cliente de cada venta, 100 consultas para obtener las líneas de cada venta, y N consultas para obtener el producto de cada línea, sumando cientos de peticiones a la BD. Con get_queryset optimizado, el número total de consultas SQL se reduce exactamente a 2, independientemente del volumen de datos retornado.


Pregunta 4: ¿Por qué en la aplicación apps/sale se decidió mapear la ruta sales/<int:pk>/ manualmente con .as_view({'get': 'retrieve', 'delete': 'destroy'}) en lugar de registrar la vista en un SimpleRouter?

Respuesta Detallada y Justificación Pedagogíca: El uso de SimpleRouter requiere que las vistas cumplan con las convenciones estándar de un ViewSet completo para una colección de recursos. En la aplicación sale, la arquitectura de endpoints está dividida por razones de diseño de dominio: 1. La creación de una venta (POST /api/sales/) requiere una lógica de entrada transaccional compleja expuesta mediante una APIView específica (SaleCollectionAPIView / SaleRegisterAPIView). 2. El listado (GET /api/sales/) y la anulación/detalle (GET y DELETE en /api/sales/<int:pk>/) son gestionados por SaleReadViewSet.

Si se registrara SaleReadViewSet en un SimpleRouter, el enrutador intentaría generar automáticamente rutas estandarizadas para el conjunto entero de acciones del ViewSet. Mapear manualmente mediante path() y el método .as_view({'get': 'retrieve', 'delete': 'destroy'}) permite enlazar exactamente las acciones deseadas a los verbos HTTP correspondientes sobre una estructura de URL unificada (sales/ y sales/<int:pk>/), manteniendo la APIView de creación sin interferencias y bloqueando categóricamente cualquier intento de mapear verbos no deseados como PUT o PATCH.


Navegación de la ruta: ← ISS-11 · 🛠 Construir · ↑ Ruta Django · → ISS-12 · 🛠 Construir