openapi: 3.0.3

info:
  title: "CDC Tablero — Módulo Compras (Implementación ODOO)"
  version: "1.0.0"
  description: |
    Especificación del módulo **Compras** del Tablero CDC para ser implementada por ODOO.

    ## Arquitectura de seguridad

    Esta API **no gestiona autenticación ni permisos de usuario**. Esa responsabilidad
    recae en el **BFF Redpoint**, que intercepta cada petición, resuelve los permisos del
    usuario consultando su propia base de datos (RBAC) y luego reenvía la petición a esta
    API inyectando los filtros de alcance (scope) como headers HTTP.

    ODOO debe leer esos headers y aplicarlos como restricciones `WHERE` en cada consulta.

    ```
    Navegador → BFF Redpoint (RBAC) → ODOO API (esta spec)
    ```

    ## Headers de scope (obligatorios)

    El BFF Redpoint enviará siempre los siguientes headers en cada petición:

    | Header | Tipo | Descripción |
    |---|---|---|
    | `X-Scope-Marcas` | JSON Array string | IDs de marcas visibles para el usuario. `[]` = sin restricción (rol global). |
    | `X-Scope-Lineas` | JSON Array string | IDs de líneas de negocio visibles. `[]` = sin restricción. |
    | `X-Scope-Sucursales` | JSON Array string | IDs de sucursales visibles. `[]` = sin restricción. |

    **Ejemplos:**
    ```
    X-Scope-Marcas:     ["mk001","mk002"]
    X-Scope-Lineas:     ["ln003","ln007"]
    X-Scope-Sucursales: ["sc001","sc005"]
    ```

    Si alguno de estos headers **no está presente**, ODOO debe responder `400 SCOPE_REQUIRED`.
    Si el array tiene valores, ODOO aplica un filtro `IN (...)` en la dimensión correspondiente.
    Si el array está vacío `[]`, no aplica restricción en esa dimensión.

    ## Notas sobre el scope en Compras

    - El scope de **sucursal** no restringe consultas de productos/compras (aplica solo a inventario).
    - El scope `proyectos_pm` activa el header adicional `X-Scope-PM-Email` (ver endpoints de proyectos).
    - Endpoints de detalle de producto individual (`/productos/resumen-general`, `/inventario`, etc.)
      **no aplican scope en SQL** — el BFF ya habrá validado el acceso antes de reenviar.

    ## Convenciones del módulo Compras

    - **Moneda:** Siempre `MXN`.
    - **Periodo (campo `periodo`):** `"anual"` | `"Q1"` | `"Q2"` | `"Q3"` | `"Q4"`. Nota: diferente al módulo Ventas — aquí son mayúsculas y no acepta números.
    - **Año (campo `año`):** entero 2000–2100. Obligatorio en la mayoría de endpoints.
    - **Año en resumen-general:** acepta tanto `año` como `anio` (compatibilidad).
    - **Estado de órdenes:** `Entregado` | `Pendiente` | `En tránsito` | `Retrasado`.
    - **Rotación:** `Alta` | `Media` | `Baja` | `N/D` (texto proveniente de BD).
    - **Marca:** siempre en minúsculas en la respuesta.
    - **Año de métricas:** configurable con variable `METRICS_YEAR` (default 2025).

  contact:
    name: Equipo CDC / Redpoint

servers:
  - url: /api
    description: Prefijo base de todos los endpoints

security:
  - BearerAuth: []

# ─────────────────────────────────────────────
# Componentes reutilizables
# ─────────────────────────────────────────────
components:

  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        Token estático de API (`API_BEARER_TOKEN`). Autentica el origen (BFF Redpoint),
        **no** identifica al usuario final. El usuario se identifica mediante los headers `X-Scope-*`.

  parameters:
    ScopeMarcas:
      in: header
      name: X-Scope-Marcas
      required: true
      schema:
        type: string
      description: |
        Array JSON serializado de IDs de marcas permitidas.
        - `["mk001","mk002"]` → filtrar solo esas marcas.
        - `[]` → sin restricción (rol global).
      example: '["mk001","mk002"]'

    ScopeLineas:
      in: header
      name: X-Scope-Lineas
      required: true
      schema:
        type: string
      description: |
        Array JSON serializado de IDs de líneas de negocio permitidas.
        - `["ln001","ln003"]` → filtrar solo esas líneas.
        - `[]` → sin restricción.
      example: '["ln001","ln003"]'

    ScopeSucursales:
      in: header
      name: X-Scope-Sucursales
      required: true
      schema:
        type: string
      description: |
        Array JSON serializado de IDs de sucursales.
        - **Nota:** en Compras el scope de sucursal solo afecta consultas de inventario,
          no las de productos/órdenes.
        - `[]` → sin restricción.
      example: '["sc001","sc005"]'

  schemas:

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [codigo, mensaje]
          properties:
            codigo:
              type: string
              enum:
                - BAD_REQUEST
                - UNAUTHORIZED
                - FORBIDDEN
                - NOT_FOUND
                - SCOPE_REQUIRED
                - RATE_LIMITED
            mensaje:
              type: string
              example: "Sin permiso para consultar esta marca"
            detalles:
              nullable: true
      example:
        error:
          codigo: SCOPE_REQUIRED
          mensaje: "Headers de scope requeridos: X-Scope-Marcas, X-Scope-Lineas, X-Scope-Sucursales"
          detalles: null

    PeriodoCompras:
      type: object
      description: |
        Objeto de periodo para el módulo Compras. **Diferente al módulo Ventas:**
        usa mayúsculas (`Q1`–`Q4`) y solo acepta `anual` o trimestres, no meses individuales.
      required: [año]
      properties:
        año:
          type: integer
          description: Año (2000–2100).
          example: 2025
        periodo:
          type: string
          description: Granularidad. `"anual"` o `"Q1"` a `"Q4"`.
          enum: [anual, Q1, Q2, Q3, Q4]
          default: anual

    PeriodoComprasResponse:
      type: object
      properties:
        tipo:
          type: string
          enum: [anual, trimestral]
        año:
          type: integer
        trimestre:
          type: string
          nullable: true
          enum: [Q1, Q2, Q3, Q4]

    Pagination:
      type: object
      properties:
        page:
          type: integer
          example: 1
        page_size:
          type: integer
          example: 10
        total_items:
          type: integer
        total_pages:
          type: integer

    Indicador:
      type: object
      description: KPI operativo con valor actual, meta, unidad y tendencia.
      properties:
        valor:
          type: number
          format: double
        meta:
          type: number
          format: double
        unidad:
          type: string
        tendencia:
          type: string
          enum: [estable, mejorando, empeorando]

    OrdenCompra:
      type: object
      properties:
        fecha:
          type: string
          format: date
        cantidad:
          type: number
        precio_unitario:
          type: number
        total:
          type: number
        estatus:
          type: string
          enum: [Entregado, Pendiente, "En tránsito", Retrasado]

    ContactoProveedor:
      type: object
      properties:
        nombre:
          type: string
          nullable: true
        telefono:
          type: string
          nullable: true
        correo:
          type: string
          nullable: true

    Proveedor:
      type: object
      properties:
        id_proveedor:
          type: string
        nombre:
          type: string
          description: Nombre del proveedor o `"N/D"` si no está registrado.
        contacto:
          $ref: '#/components/schemas/ContactoProveedor'
        total_compras:
          type: number
          format: double
        ordenes:
          type: array
          items:
            type: object
            properties:
              numero_orden:
                type: string
              fecha:
                type: string
                format: date
              cantidad:
                type: number
              precio_unitario:
                type: number
              total:
                type: number
              estatus:
                type: string
                enum: [Entregado, Pendiente, "En tránsito", Retrasado]

    ProveedorProyecto:
      type: object
      description: Shape de proveedor/fabricante en el contexto de proyectos especiales.
      properties:
        nombre:
          type: string
        contacto:
          type: object
          properties:
            nombre:
              type: string
              nullable: true
            telefono:
              type: string
              nullable: true
            email:
              type: string
              nullable: true
              description: Nota — en proyectos el campo se llama `email`, no `correo`.
        total_compras:
          type: number
        ordenes:
          type: array
          items:
            type: object
            properties:
              no_orden:
                type: string
                description: Nota — en proyectos el campo se llama `no_orden`, no `numero_orden`.
              fecha:
                type: string
                format: date
              estado:
                type: string
              items:
                type: array
                items:
                  type: object
                  properties:
                    producto:
                      type: string
                    marca:
                      type: string
                    cantidad:
                      type: number
                    precio_unit:
                      type: number
                    total:
                      type: number

    ProyectosComprasResponse:
      type: object
      properties:
        moneda:
          type: string
          example: MXN
        periodo:
          type: object
          properties:
            tipo:
              type: string
              enum: [anual, trimestral]
            anio:
              type: integer
            trimestre:
              type: string
              nullable: true
        resumen:
          type: object
          properties:
            total_proyectos:
              type: integer
            monto_total:
              type: number
            clientes_activos:
              type: integer
            lineas_involucradas:
              type: integer
        proyectos:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              nombre:
                type: string
              descripcion:
                type: string
              monto:
                type: number
              id_cliente:
                type: string
              cliente:
                type: string
              id_pjm:
                type: string
              project_manager:
                type: string
              lineas_negocio:
                type: array
                items:
                  type: string
              productos:
                type: integer
              proveedores:
                type: array
                items:
                  $ref: '#/components/schemas/ProveedorProyecto'
              fabricantes:
                type: array
                items:
                  $ref: '#/components/schemas/ProveedorProyecto'
              compras:
                type: object
                properties:
                  proveedores_total:
                    type: integer
                  fabricantes_total:
                    type: integer
                  monto_total_compras:
                    type: number
                  proveedores:
                    type: array
                    items:
                      $ref: '#/components/schemas/ProveedorProyecto'
                  fabricantes:
                    type: array
                    items:
                      $ref: '#/components/schemas/ProveedorProyecto'

# ─────────────────────────────────────────────
# Paths
# ─────────────────────────────────────────────
paths:

  # ════════════════════════════════════════
  # RESUMEN DE COMPRAS
  # ════════════════════════════════════════
  /compras:
    post:
      tags: [Resumen]
      summary: Resumen general de compras
      description: |
        Devuelve los 4 indicadores operativos de compras y el top 10 de productos por gasto anual.

        **Scope aplicado en SQL** para las dimensiones `X-Scope-Marcas` y `X-Scope-Lineas`.
        El scope de sucursal no restringe este endpoint.

        ### Indicadores operativos
        | Indicador | Cálculo | Meta |
        |---|---|---|
        | `rotacion_inventario` | Promedio numérico de rotación (Alta=7, Media=4.5, Baja=2) | Anual: 4.5 / Trimestral: 1.125 |
        | `tiempo_entrega_promedio` | Promedio días `(fecha_estimada_entrega − fecha_orden)` de OC Entregado | 25 días |
        | `nivel_servicio` | % de OC en estado Entregado vs total | 95% |
        | `precision_inventario` | `disp / (disp + defectuoso)` en inventario | 98% |

        **Tendencias:** comparan con el año anterior. `precision_inventario` siempre es `"estable"`.

        ### Top productos por gasto anual
        - Solo OC en estado `Entregado` del año completo (el trimestre **no** restringe el ranking).
        - `gasto_anual` = `SUM(cantidad) × AVG(precio_unitario)`.
        - Top 10 ordenados descendente.
      parameters:
        - $ref: '#/components/parameters/ScopeMarcas'
        - $ref: '#/components/parameters/ScopeLineas'
        - $ref: '#/components/parameters/ScopeSucursales'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [año]
              properties:
                año:
                  type: integer
                  minimum: 2000
                  maximum: 2100
                  example: 2025
                periodo:
                  type: string
                  enum: [anual, Q1, Q2, Q3, Q4]
                  default: anual
                  description: Si se envía un trimestre (Q1–Q4), los indicadores operativos se filtran por ese trimestre. El ranking top productos siempre es anual.
                id_marca:
                  oneOf:
                    - type: string
                    - type: array
                      items:
                        type: string
                  nullable: true
                  description: Filtro adicional opcional por marca (debe estar dentro del scope).
                id_linea:
                  oneOf:
                    - type: string
                    - type: array
                      items:
                        type: string
                  nullable: true
                  description: Filtro adicional opcional por línea de negocio.
            example:
              año: 2025
              periodo: anual
      responses:
        '200':
          description: Resumen de compras con KPIs y top productos.
          content:
            application/json:
              schema:
                type: object
                properties:
                  moneda:
                    type: string
                    example: MXN
                  periodo:
                    $ref: '#/components/schemas/PeriodoComprasResponse'
                  indicadores_operativos:
                    type: object
                    properties:
                      rotacion_inventario:
                        $ref: '#/components/schemas/Indicador'
                      tiempo_entrega_promedio:
                        $ref: '#/components/schemas/Indicador'
                      nivel_servicio:
                        $ref: '#/components/schemas/Indicador'
                      precision_inventario:
                        $ref: '#/components/schemas/Indicador'
                    example:
                      rotacion_inventario:
                        valor: 4.5
                        meta: 4.5
                        unidad: "veces/año"
                        tendencia: "estable"
                      tiempo_entrega_promedio:
                        valor: 25
                        meta: 25
                        unidad: "días"
                        tendencia: "mejorando"
                      nivel_servicio:
                        valor: 95.0
                        meta: 95
                        unidad: "%"
                        tendencia: "estable"
                      precision_inventario:
                        valor: 98.0
                        meta: 98
                        unidad: "%"
                        tendencia: "estable"
                  top_productos_por_gasto_anual:
                    type: array
                    maxItems: 10
                    items:
                      type: object
                      properties:
                        id_producto:
                          type: string
                        ranking:
                          type: integer
                          description: Posición 1–10.
                        producto:
                          type: string
                        marca:
                          type: string
                          description: Siempre en minúsculas.
                        categoria:
                          type: string
                          description: Nombre de la línea de negocio.
                        rotacion:
                          type: string
                          enum: [Alta, Media, Baja, "N/D"]
                        stock:
                          type: string
                          description: 'Formato "123 pza".'
                          example: "50 pza"
                        gasto_anual:
                          type: number
                          format: double
        '400':
          description: Parámetros inválidos, año fuera de rango o headers de scope ausentes.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Filtro fuera del alcance asignado al usuario.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ════════════════════════════════════════
  # GESTIÓN DE PRODUCTOS
  # ════════════════════════════════════════
  /compras/productos:
    post:
      tags: [Productos]
      summary: Listado paginado de productos con métricas de compra
      description: |
        Devuelve los productos que tienen al menos una orden de compra en estado `Entregado`
        para el año/trimestre indicado, con sus métricas de stock, rotación, última compra
        y órdenes pendientes.

        **Scope aplicado en SQL** para `X-Scope-Marcas` y `X-Scope-Lineas`.

        ### Cálculos
        - `stock_actual.valor` → `SUM(product_inventory.stock_actual)` de todas las sucursales.
        - `ordenes_pendientes` → COUNT de OC en estados `Pendiente`, `En tránsito`, `Retrasado`.
        - `ultima_compra` → última OC `Entregado` en el periodo especificado.
        - `valor_total_inventario` → `ROUND(stock × precio_unitario)`.
      parameters:
        - $ref: '#/components/parameters/ScopeMarcas'
        - $ref: '#/components/parameters/ScopeLineas'
        - $ref: '#/components/parameters/ScopeSucursales'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [año, periodo]
              properties:
                año:
                  type: integer
                  minimum: 2000
                  maximum: 2100
                  example: 2025
                periodo:
                  type: string
                  enum: [anual, Q1, Q2, Q3, Q4]
                  description: Obligatorio en este endpoint.
                id_marca:
                  oneOf:
                    - type: string
                    - type: array
                      items:
                        type: string
                  nullable: true
                id_linea:
                  oneOf:
                    - type: string
                    - type: array
                      items:
                        type: string
                  nullable: true
                page:
                  type: integer
                  default: 1
                  minimum: 1
                page_size:
                  type: integer
                  default: 10
                  minimum: 1
                  maximum: 200
            example:
              año: 2025
              periodo: anual
              page: 1
              page_size: 10
      responses:
        '200':
          description: Lista paginada de productos con métricas de compra.
          content:
            application/json:
              schema:
                type: object
                properties:
                  moneda:
                    type: string
                    example: MXN
                  periodo:
                    $ref: '#/components/schemas/PeriodoComprasResponse'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
                  gestion_productos:
                    type: array
                    items:
                      type: object
                      properties:
                        id_producto:
                          type: string
                        producto:
                          type: string
                        fabricante:
                          type: string
                          description: Nombre del fabricante o `"N/D"`.
                        id_marca:
                          type: string
                        marca:
                          type: string
                          description: Siempre en minúsculas.
                        id_linea_negocio:
                          type: string
                        linea_negocio:
                          type: string
                        stock_actual:
                          type: object
                          properties:
                            valor:
                              type: number
                            unidad:
                              type: string
                              example: pza
                        precio_unitario:
                          type: number
                          format: double
                        lead_time_dias:
                          type: integer
                          description: Días promedio de entrega configurados para el producto.
                        rotacion:
                          type: string
                          enum: [Alta, Media, Baja, "N/D"]
                        valor_total_inventario:
                          type: number
                          format: double
                          description: "`ROUND(stock × precio_unitario)`"
                        ultima_compra:
                          type: object
                          nullable: true
                          properties:
                            fecha:
                              type: string
                              format: date
                            cantidad:
                              type: number
                            precio_unitario:
                              type: number
                        ordenes_pendientes:
                          type: integer
                          description: OC en estados Pendiente, En tránsito o Retrasado.

  # ════════════════════════════════════════
  # DETALLE DE PRODUCTO — subendpoints
  # ════════════════════════════════════════
  /compras/productos/resumen-general:
    post:
      tags: [Detalle Producto]
      summary: Resumen general de un producto
      description: |
        Stock agregado de todas las sucursales, precio, lead time, valor de inventario,
        última compra y gasto acumulado anual para un producto específico.

        **Nota de scope:** el BFF valida el acceso al producto antes de reenviar; ODOO
        no aplica scope adicional en SQL en este endpoint.

        **Nota de periodo:** `gasto_acumulado_anual` siempre usa el año completo,
        independientemente del `periodo` enviado.

        **Campos de año:** acepta tanto `año` como `anio` (compatibilidad).
      parameters:
        - $ref: '#/components/parameters/ScopeMarcas'
        - $ref: '#/components/parameters/ScopeLineas'
        - $ref: '#/components/parameters/ScopeSucursales'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id_producto]
              properties:
                id_producto:
                  type: string
                  description: ID del producto. Responde 404 si no existe.
                año:
                  type: integer
                  description: Año (alternativa a `anio`). Default = año actual.
                  example: 2025
                anio:
                  type: integer
                  description: Alias de `año`. Si ambos están presentes, tiene precedencia `año`.
                periodo:
                  type: string
                  enum: [anual, Q1, Q2, Q3, Q4]
                  default: anual
            example:
              id_producto: "pr001"
              año: 2025
              periodo: anual
      responses:
        '200':
          description: Resumen general del producto.
          content:
            application/json:
              schema:
                type: object
                properties:
                  producto_id:
                    type: string
                  producto:
                    type: string
                  fabricante:
                    type: string
                    description: Nombre o `"N/D"`.
                  marca:
                    type: string
                    description: Siempre en minúsculas.
                  marca_id:
                    type: string
                  linea_negocio:
                    type: string
                  linea_id:
                    type: string
                  rotacion:
                    type: string
                    description: Prefijo `"Rotación "` + valor de BD. Ej `"Rotación Alta"`. O `"Rotación N/D"`.
                    example: "Rotación Alta"
                  moneda:
                    type: string
                    example: MXN
                  periodo:
                    $ref: '#/components/schemas/PeriodoComprasResponse'
                  resumen_general:
                    type: object
                    properties:
                      stock_actual:
                        type: object
                        properties:
                          cantidad:
                            type: number
                            description: SUM de stock_actual en todas las sucursales.
                          unidad:
                            type: string
                            example: pza
                          minimo:
                            type: number
                            description: MIN de minimo configurado.
                          maximo:
                            type: number
                            description: MAX de maximo configurado.
                          ideal:
                            type: number
                            description: AVG de ideal configurado.
                      precio_unitario:
                        type: object
                        properties:
                          monto:
                            type: number
                          unidad:
                            type: string
                            example: pza
                      lead_time:
                        type: object
                        properties:
                          valor:
                            type: integer
                            description: Días de lead time.
                          unidad:
                            type: string
                            example: días
                      valor_inventario:
                        type: object
                        properties:
                          monto:
                            type: number
                            description: ROUND(stock × precio_unitario).
                      ultima_compra:
                        type: object
                        nullable: true
                        properties:
                          fecha:
                            type: string
                            format: date
                          precio_unitario:
                            type: number
                          cantidad:
                            type: number
                          unidad:
                            type: string
                      gasto_acumulado_anual:
                        type: object
                        properties:
                          anio:
                            type: integer
                          monto:
                            type: number
                            description: SUM(cantidad × precio_unitario) de OC Entregado en el año completo.
        '400':
          description: id_producto faltante, año/periodo inválido.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Producto no encontrado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /compras/productos/inventario:
    post:
      tags: [Detalle Producto]
      summary: Inventario del producto por sucursal
      description: |
        Muestra el stock, mínimos, máximos e ideal de un producto en cada sucursal.
        `estado_stock` es `"crítico"` si `stock_actual < minimo`, de lo contrario `"óptimo"`.

        **Scope de sucursal:** si `X-Scope-Sucursales` no está vacío, ODOO debe filtrar
        solo las sucursales incluidas en el scope.
      parameters:
        - $ref: '#/components/parameters/ScopeMarcas'
        - $ref: '#/components/parameters/ScopeLineas'
        - $ref: '#/components/parameters/ScopeSucursales'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id_producto]
              properties:
                id_producto:
                  type: string
                id_sucursal:
                  type: string
                  nullable: true
                  description: Filtro opcional — devuelve solo esa sucursal si se envía.
            example:
              id_producto: "pr001"
      responses:
        '200':
          description: Inventario por sucursal.
          content:
            application/json:
              schema:
                type: object
                properties:
                  producto_id:
                    type: string
                  inventario_por_sucursal:
                    type: array
                    items:
                      type: object
                      properties:
                        sucursal_id:
                          type: string
                        nombre_sucursal:
                          type: string
                        stock_actual:
                          type: number
                        unidad:
                          type: string
                          example: pza
                        minimo:
                          type: number
                        maximo:
                          type: number
                        ideal:
                          type: number
                        estado_stock:
                          type: string
                          enum: [óptimo, crítico]
                          description: "`crítico` si stock_actual < minimo."
        '404':
          description: Producto no encontrado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /compras/productos/historial-compras:
    post:
      tags: [Detalle Producto]
      summary: Historial de compras de un producto
      description: |
        Lista de órdenes de compra en estado `Entregado` para el producto y periodo indicados,
        ordenadas por fecha descendente.
      parameters:
        - $ref: '#/components/parameters/ScopeMarcas'
        - $ref: '#/components/parameters/ScopeLineas'
        - $ref: '#/components/parameters/ScopeSucursales'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id_producto, año, periodo]
              properties:
                id_producto:
                  type: string
                año:
                  type: integer
                  minimum: 2000
                  maximum: 2100
                periodo:
                  type: string
                  enum: [anual, Q1, Q2, Q3, Q4]
            example:
              id_producto: "pr001"
              año: 2025
              periodo: anual
      responses:
        '200':
          description: Historial de compras del producto.
          content:
            application/json:
              schema:
                type: object
                properties:
                  moneda:
                    type: string
                  producto_id:
                    type: string
                  periodo:
                    $ref: '#/components/schemas/PeriodoComprasResponse'
                  historial_compras:
                    type: array
                    items:
                      type: object
                      properties:
                        fecha:
                          type: string
                          format: date
                        cantidad:
                          type: number
                        unidad:
                          type: string
                        proveedor:
                          type: string
                          description: Nombre del proveedor o `"N/D"`.
                        precio_unitario:
                          type: number
                          format: double
                        total:
                          type: number
                          format: double

  /compras/productos/ordenes-pendientes:
    post:
      tags: [Detalle Producto]
      summary: Órdenes de compra pendientes de un producto
      description: |
        Lista las órdenes en estados `Pendiente`, `En tránsito` o `Retrasado` para el producto.
        `valor_estimado` = `ROUND(cantidad × precio_unitario)`.
      parameters:
        - $ref: '#/components/parameters/ScopeMarcas'
        - $ref: '#/components/parameters/ScopeLineas'
        - $ref: '#/components/parameters/ScopeSucursales'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id_producto]
              properties:
                id_producto:
                  type: string
            example:
              id_producto: "pr001"
      responses:
        '200':
          description: Lista de órdenes pendientes.
          content:
            application/json:
              schema:
                type: object
                properties:
                  moneda:
                    type: string
                  producto_id:
                    type: string
                  ordenes_pendientes:
                    type: array
                    items:
                      type: object
                      properties:
                        fecha_orden_compra:
                          type: string
                          format: date
                        fecha_estimada_entrega:
                          type: string
                          format: date
                        proveedor:
                          type: string
                        estado:
                          type: string
                          enum: [Pendiente, "En tránsito", Retrasado]
                        cantidad:
                          type: number
                        unidad:
                          type: string
                        valor_estimado:
                          type: number
                          description: ROUND(cantidad × precio_unitario).

  /compras/productos/proveedores:
    post:
      tags: [Detalle Producto]
      summary: Proveedores de un producto
      description: |
        Lista de proveedores con sus órdenes de compra para el producto indicado.
        Incluye todas las órdenes (todos los estados) agrupadas por proveedor.
      parameters:
        - $ref: '#/components/parameters/ScopeMarcas'
        - $ref: '#/components/parameters/ScopeLineas'
        - $ref: '#/components/parameters/ScopeSucursales'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id_producto]
              properties:
                id_producto:
                  type: string
            example:
              id_producto: "pr001"
      responses:
        '200':
          description: Proveedores del producto con historial de órdenes.
          content:
            application/json:
              schema:
                type: object
                properties:
                  moneda:
                    type: string
                  producto_id:
                    type: string
                  proveedores:
                    type: array
                    items:
                      $ref: '#/components/schemas/Proveedor'

  /compras/productos/fabricante:
    post:
      tags: [Detalle Producto]
      summary: Fabricante de un producto
      description: |
        Devuelve el fabricante principal del producto con todas sus órdenes de compra.

        Responde `404` si el producto no tiene `fabricante_nombre` registrado en BD.
      parameters:
        - $ref: '#/components/parameters/ScopeMarcas'
        - $ref: '#/components/parameters/ScopeLineas'
        - $ref: '#/components/parameters/ScopeSucursales'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id_producto]
              properties:
                id_producto:
                  type: string
            example:
              id_producto: "pr001"
      responses:
        '200':
          description: Fabricante del producto con historial de órdenes.
          content:
            application/json:
              schema:
                type: object
                properties:
                  moneda:
                    type: string
                  producto_id:
                    type: string
                  fabricante:
                    type: object
                    properties:
                      id_fabricante:
                        type: string
                      nombre:
                        type: string
                      contacto:
                        $ref: '#/components/schemas/ContactoProveedor'
                      total_compras:
                        type: number
                      ordenes:
                        type: array
                        items:
                          type: object
                          properties:
                            numero_orden:
                              type: string
                            fecha:
                              type: string
                              format: date
                            cantidad:
                              type: number
                            precio_unitario:
                              type: number
                            total:
                              type: number
                            estatus:
                              type: string
                              enum: [Entregado, Pendiente, "En tránsito", Retrasado]
        '404':
          description: Producto sin fabricante registrado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ════════════════════════════════════════
  # MÉTRICAS CLAVE
  # ════════════════════════════════════════
  /compras/metricas-clave:
    get:
      tags: [Métricas]
      summary: KPIs globales de compras
      description: |
        Devuelve las 5 métricas clave del módulo de compras para el año configurado
        (`METRICS_YEAR`, default 2025).

        **Scope:** se obtiene del usuario pero **no se aplica en las queries SQL** de este
        endpoint — las métricas son globales para que sirvan como indicadores de portada
        del módulo.

        - `valor_total_inventario` → `SUM(stock_actual × precio_unitario)` en todos los productos.
        - `productos_comprados_anio` → `SUM(cantidad)` de OC Entregado en el año.
        - `rotacion_promedio` → promedio numérico de la columna rotación en `product_procurement`.
        - `ultima_compra` → OC Entregado más reciente (producto, cantidad, unidad).
        - `tendencia` → hardcoded `"Estable"`.
      parameters:
        - $ref: '#/components/parameters/ScopeMarcas'
        - $ref: '#/components/parameters/ScopeLineas'
        - $ref: '#/components/parameters/ScopeSucursales'
      responses:
        '200':
          description: Métricas clave del módulo compras.
          content:
            application/json:
              schema:
                type: object
                properties:
                  metricas_clave:
                    type: object
                    properties:
                      valor_total_inventario:
                        type: number
                        format: double
                        description: Valor total del inventario en MXN.
                      productos_comprados_anio:
                        type: number
                        description: Suma de unidades compradas en el año (OC Entregado).
                      rotacion_promedio:
                        type: number
                        format: double
                        description: Promedio de rotación numérico.
                      ultima_compra:
                        type: object
                        properties:
                          producto:
                            type: string
                          cantidad:
                            type: number
                          unidad:
                            type: string
                      tendencia:
                        type: string
                        description: Actualmente siempre `"Estable"`.
                        example: Estable

  # ════════════════════════════════════════
  # PROYECTOS ESPECIALES
  # ════════════════════════════════════════
  /compras/proyectos-especiales:
    get:
      tags: [Proyectos Especiales]
      summary: Lista de proyectos especiales (GET)
      description: |
        Versión GET. Útil para carga inicial sin filtros de periodo.

        A diferencia del módulo Ventas, aquí los proyectos incluyen proveedores y fabricantes
        con detalles de órdenes (`no_orden`, `items[]`).

        **Nota sobre campos de contacto:** en proyectos el contacto usa `email` (no `correo`).
        **Nota sobre órdenes:** en proyectos el número de orden se llama `no_orden` (no `numero_orden`).

        Si el scope es `proyectos_pm`, el BFF envía `X-Scope-PM-Email` y ODOO debe filtrar
        proyectos por ese email de Project Manager.
      parameters:
        - $ref: '#/components/parameters/ScopeMarcas'
        - $ref: '#/components/parameters/ScopeLineas'
        - $ref: '#/components/parameters/ScopeSucursales'
        - name: X-Scope-PM-Email
          in: header
          required: false
          schema:
            type: string
            format: email
          description: |
            Email del Project Manager. El BFF lo envía cuando el rol del usuario es `proyectos_pm`.
            Si está presente, ODOO debe filtrar `WHERE id_pjm = email`.
        - name: projectManagerId
          in: query
          required: false
          schema:
            type: string
          description: Filtro adicional por ID/email del PM. Ignorado si `X-Scope-PM-Email` tiene valor.
      responses:
        '200':
          description: Lista de proyectos con desglose de compras.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProyectosComprasResponse'
        '404':
          description: Sin proyectos para el PM o filtros indicados.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

    post:
      tags: [Proyectos Especiales]
      summary: Lista de proyectos especiales (POST con periodo)
      description: |
        Igual que el GET pero acepta filtros de periodo en el body.

        **Diferencias con proyectos de Ventas:**
        - El `monto` del proyecto se calcula desde órdenes de compra (`purchase_orders`), no de `sale_lines`.
        - Incluye desglose de `proveedores` y `fabricantes` con sus órdenes.
        - El objeto `compras` en cada proyecto resume el total de proveedores, fabricantes y monto de compras.
      parameters:
        - $ref: '#/components/parameters/ScopeMarcas'
        - $ref: '#/components/parameters/ScopeLineas'
        - $ref: '#/components/parameters/ScopeSucursales'
        - name: X-Scope-PM-Email
          in: header
          required: false
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                periodo:
                  type: object
                  properties:
                    tipo:
                      type: string
                      enum: [anual, trimestral, q1, q2, q3, q4]
                    anio:
                      type: integer
                      example: 2025
                    trimestre:
                      type: string
                      enum: [Q1, Q2, Q3, Q4]
                      description: Requerido si tipo = trimestral.
            example:
              periodo:
                tipo: anual
                anio: 2025
      responses:
        '200':
          description: Lista de proyectos con desglose de compras.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProyectosComprasResponse'
        '404':
          description: Sin proyectos para los filtros indicados.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /compras/proyectos-especiales/{proyectoId}:
    post:
      tags: [Proyectos Especiales]
      summary: Detalle de un proyecto especial (vista Compras)
      description: |
        Detalle completo del proyecto incluyendo componentes, proveedores con órdenes e ítems
        y fabricantes. Este endpoint expone el módulo de compras (`exposeCompras: true`).

        **Sin filtro temporal** — devuelve todo el historial del proyecto.

        ### Diferencias vs. Ventas (mismo URL base, distinto módulo)
        | Campo | Ventas | Compras |
        |---|---|---|
        | `proveedores` | Array vacío | Con datos de OC |
        | `fabricantes` | Array vacío | Con datos de OC |
        | `monto_total` | Desde `sale_lines` | Desde `purchase_orders` |
        | Productos en componentes | Con `estado`, `almacen` | `estado: null`, `almacen: null` |

        El BFF valida que el usuario tenga acceso al `proyectoId` antes de reenviar.
      parameters:
        - $ref: '#/components/parameters/ScopeMarcas'
        - $ref: '#/components/parameters/ScopeLineas'
        - $ref: '#/components/parameters/ScopeSucursales'
        - name: proyectoId
          in: path
          required: true
          schema:
            type: string
          description: ID del proyecto especial.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id_proyecto]
              properties:
                id_proyecto:
                  type: string
                  description: Debe coincidir con el path param `proyectoId`.
            example:
              id_proyecto: "pe001"
      responses:
        '200':
          description: Detalle del proyecto especial (vista Compras).
          content:
            application/json:
              schema:
                type: object
                properties:
                  moneda:
                    type: string
                    example: MXN
                  id:
                    type: string
                  periodo:
                    type: object
                    properties:
                      tipo:
                        type: string
                        example: anual
                      anio:
                        type: integer
                  nombre:
                    type: string
                  descripcion:
                    type: string
                  monto_total:
                    type: number
                    description: Calculado desde purchase_orders; fallback a suma de componentes.
                  utilidad:
                    type: object
                    properties:
                      porcentaje:
                        type: number
                        example: 0
                      valor:
                        type: number
                        example: 0
                  id_cliente:
                    type: string
                  cliente_integrador:
                    type: string
                  id_pjm:
                    type: string
                  project_manager:
                    type: string
                  credito_disponible:
                    type: number
                    example: 0
                  saldo_vencido:
                    type: number
                    example: 0
                  productos_total:
                    type: integer
                  lineas_total:
                    type: integer
                  marcas:
                    type: array
                    items:
                      type: string
                  proveedores:
                    type: array
                    description: Proveedores con órdenes de compra del proyecto.
                    items:
                      $ref: '#/components/schemas/ProveedorProyecto'
                  fabricantes:
                    type: array
                    description: Fabricantes con órdenes de compra del proyecto.
                    items:
                      $ref: '#/components/schemas/ProveedorProyecto'
                  componentes:
                    type: array
                    items:
                      type: object
                      properties:
                        id_linea_negocio:
                          type: string
                        linea_negocio:
                          type: string
                        monto_total:
                          type: number
                        marcas:
                          type: array
                          items:
                            type: object
                            properties:
                              id_marca:
                                type: string
                              nombre:
                                type: string
                              monto:
                                type: number
                              id_pm:
                                type: string
                              pm:
                                type: string
                              productos:
                                type: array
                                items:
                                  type: object
                                  properties:
                                    id_producto:
                                      type: string
                                    nombre:
                                      type: string
                                    cantidad:
                                      type: number
                                    monto:
                                      type: number
                                    precio_unitario:
                                      type: number
                                    fecha:
                                      type: string
                                      format: date
                                    antiguedad_dias:
                                      type: integer
                                    estado:
                                      type: string
                                      nullable: true
                                      description: Siempre null en la vista Compras.
                                    proyecto_asignacion:
                                      type: string
                                      nullable: true
                                      description: Siempre null en la vista Compras.
                                    almacen:
                                      type: string
                                      nullable: true
                                      description: Siempre null en la vista Compras.
        '400':
          description: id_proyecto no coincide con el path param.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Proyecto no encontrado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
