# SPEC-PRICING-RULES: Reglas de Precios y Listas de Precios

## Metadata
- **Código**: SPEC-MGN-007
- **Módulo**: Ventas / Pricing
- **Gap Relacionado**: GAP-MGN-007-002
- **Prioridad**: P1
- **Esfuerzo Estimado**: 13 SP
- **Versión**: 1.0
- **Última Actualización**: 2025-01-09
- **Referencias**: Odoo product.pricelist, product.pricelist.item

---

## 1. Resumen Ejecutivo

### 1.1 Objetivo
Implementar un sistema flexible de reglas de precios y listas de precios que permita:
- Precios fijos, porcentuales y basados en fórmulas
- Descuentos escalonados por cantidad (tiered pricing)
- Validez temporal de reglas (ofertas por tiempo limitado)
- Soporte multi-moneda con conversión automática
- Listas de precios en cascada (derivadas de otras)
- Códigos promocionales para e-commerce

### 1.2 Alcance
- Modelo de datos para pricelists y reglas
- Algoritmo de selección de reglas por prioridad
- Métodos de cálculo de precio (fixed, percentage, formula)
- Descuentos escalonados por cantidad
- Integración con órdenes de venta
- Soporte multi-moneda
- API REST para consulta de precios

### 1.3 Tipos de Reglas de Precio

| Tipo | Descripción | Caso de Uso |
|------|-------------|-------------|
| `fixed` | Precio fijo absoluto | Precios especiales, liquidaciones |
| `percentage` | Descuento porcentual sobre base | Descuentos por cliente/temporada |
| `formula` | Fórmula con markup, redondeo, márgenes | Costo + margen, precios .99 |

---

## 2. Modelo de Datos

### 2.1 Diagrama Entidad-Relación

```
┌─────────────────────────────────┐
│      pricing.pricelists         │
│─────────────────────────────────│
│   id (PK)                       │
│   name                          │
│   code (promo code)             │
│   currency_id (FK)              │
│   company_id (FK)               │
│   sequence                      │
│   is_active                     │
│   selectable                    │
│   country_group_ids             │
└────────────────┬────────────────┘
                 │
                 │ 1:N
                 ▼
┌─────────────────────────────────┐
│    pricing.pricelist_items      │
│─────────────────────────────────│
│   id (PK)                       │
│   pricelist_id (FK)             │
│   applied_on                    │
│   product_id (FK)               │
│   product_tmpl_id (FK)          │
│   category_id (FK)              │
│   min_quantity                  │
│   date_start                    │
│   date_end                      │
│   compute_price                 │
│   base                          │
│   base_pricelist_id (FK)        │
│   fixed_price                   │
│   percent_price                 │
│   price_discount                │
│   price_markup                  │
│   price_round                   │
│   price_surcharge               │
│   price_min_margin              │
│   price_max_margin              │
└─────────────────────────────────┘

          ▲
          │ Referencia para cascada
          │
┌─────────────────────────────────┐
│   pricing.country_groups        │
│─────────────────────────────────│
│   id (PK)                       │
│   name                          │
│   country_ids                   │
└─────────────────────────────────┘
```

### 2.2 Definición de Tablas

#### 2.2.1 pricing.pricelists

```sql
CREATE TABLE pricing.pricelists (
    -- Identificación
    id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
    name VARCHAR(128) NOT NULL,
    code VARCHAR(32),  -- Código promocional

    -- Moneda
    currency_id UUID NOT NULL REFERENCES core.currencies(id),

    -- Empresa
    company_id UUID REFERENCES core.companies(id),

    -- Ordenamiento
    sequence INTEGER NOT NULL DEFAULT 16,

    -- Estado
    is_active BOOLEAN NOT NULL DEFAULT true,

    -- Disponibilidad
    selectable BOOLEAN NOT NULL DEFAULT true,  -- Mostrar en selección de cliente
    website_only BOOLEAN NOT NULL DEFAULT false,

    -- Restricciones geográficas
    country_group_ids UUID[] DEFAULT '{}',

    -- Auditoría
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_by UUID REFERENCES core.users(id),

    -- Constraints
    CONSTRAINT uk_pricelist_code UNIQUE (code) WHERE code IS NOT NULL
);

-- Índices
CREATE INDEX idx_pricelists_company ON pricing.pricelists(company_id);
CREATE INDEX idx_pricelists_currency ON pricing.pricelists(currency_id);
CREATE INDEX idx_pricelists_sequence ON pricing.pricelists(sequence) WHERE is_active;
CREATE INDEX idx_pricelists_code ON pricing.pricelists(code) WHERE code IS NOT NULL;
```

#### 2.2.2 pricing.pricelist_items

```sql
CREATE TABLE pricing.pricelist_items (
    -- Identificación
    id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),

    -- Lista de precios padre
    pricelist_id UUID NOT NULL REFERENCES pricing.pricelists(id) ON DELETE CASCADE,

    -- Ámbito de aplicación
    applied_on VARCHAR(32) NOT NULL DEFAULT '3_global'
        CHECK (applied_on IN (
            '3_global',           -- Todos los productos
            '2_product_category', -- Categoría específica
            '1_product',          -- Producto (template)
            '0_product_variant'   -- Variante específica
        )),

    -- Filtros de producto
    product_id UUID REFERENCES products.products(id),
    product_tmpl_id UUID REFERENCES products.product_templates(id),
    category_id UUID REFERENCES products.product_categories(id),

    -- Umbral de cantidad (para descuentos escalonados)
    min_quantity DECIMAL(20,6) NOT NULL DEFAULT 0,

    -- Validez temporal
    date_start TIMESTAMPTZ,
    date_end TIMESTAMPTZ,

    -- Método de cálculo
    compute_price VARCHAR(16) NOT NULL DEFAULT 'fixed'
        CHECK (compute_price IN ('fixed', 'percentage', 'formula')),

    -- Base para el cálculo
    base VARCHAR(16) NOT NULL DEFAULT 'list_price'
        CHECK (base IN ('list_price', 'standard_price', 'pricelist')),
    base_pricelist_id UUID REFERENCES pricing.pricelists(id),

    -- Parámetros de precio fijo
    fixed_price DECIMAL(20,6),

    -- Parámetros de porcentaje
    percent_price DECIMAL(10,4) DEFAULT 0,  -- % de descuento

    -- Parámetros de fórmula
    price_discount DECIMAL(10,4) DEFAULT 0,   -- % descuento (para list_price)
    price_markup DECIMAL(10,4) DEFAULT 0,     -- % markup (para cost)
    price_round DECIMAL(20,6),                -- Precisión de redondeo
    price_surcharge DECIMAL(20,6) DEFAULT 0,  -- Cargo/descuento fijo
    price_min_margin DECIMAL(20,6),           -- Margen mínimo
    price_max_margin DECIMAL(20,6),           -- Margen máximo

    -- Auditoría
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    -- Constraints
    CONSTRAINT chk_date_range CHECK (date_end IS NULL OR date_start IS NULL OR date_end >= date_start),
    CONSTRAINT chk_min_quantity CHECK (min_quantity >= 0),
    CONSTRAINT chk_base_pricelist CHECK (
        base != 'pricelist' OR base_pricelist_id IS NOT NULL
    ),
    CONSTRAINT chk_fixed_price CHECK (
        compute_price != 'fixed' OR fixed_price IS NOT NULL
    ),
    CONSTRAINT chk_applied_on_product CHECK (
        (applied_on = '0_product_variant' AND product_id IS NOT NULL) OR
        (applied_on = '1_product' AND product_tmpl_id IS NOT NULL) OR
        (applied_on = '2_product_category' AND category_id IS NOT NULL) OR
        (applied_on = '3_global')
    )
);

-- Índices optimizados para búsqueda de reglas
CREATE INDEX idx_pricelist_items_pricelist
    ON pricing.pricelist_items(pricelist_id);

CREATE INDEX idx_pricelist_items_lookup
    ON pricing.pricelist_items(
        pricelist_id,
        applied_on,
        min_quantity DESC,
        category_id,
        id DESC
    );

CREATE INDEX idx_pricelist_items_product
    ON pricing.pricelist_items(product_id) WHERE product_id IS NOT NULL;

CREATE INDEX idx_pricelist_items_template
    ON pricing.pricelist_items(product_tmpl_id) WHERE product_tmpl_id IS NOT NULL;

CREATE INDEX idx_pricelist_items_category
    ON pricing.pricelist_items(category_id) WHERE category_id IS NOT NULL;

CREATE INDEX idx_pricelist_items_dates
    ON pricing.pricelist_items(date_start, date_end)
    WHERE date_start IS NOT NULL OR date_end IS NOT NULL;
```

#### 2.2.3 pricing.country_groups

```sql
CREATE TABLE pricing.country_groups (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
    name VARCHAR(64) NOT NULL,
    country_ids CHAR(2)[] NOT NULL,  -- Array de códigos ISO

    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Datos de ejemplo
INSERT INTO pricing.country_groups (name, country_ids) VALUES
('Norteamérica', ARRAY['MX', 'US', 'CA']),
('Unión Europea', ARRAY['ES', 'FR', 'DE', 'IT', 'PT', 'NL', 'BE']),
('Latinoamérica', ARRAY['MX', 'AR', 'BR', 'CL', 'CO', 'PE']);
```

---

## 3. Algoritmo de Selección de Reglas

### 3.1 Orden de Prioridad

```
┌─────────────────────────────────────────────────────────────────────┐
│                    PRIORIDAD DE REGLAS                               │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  1. ESPECIFICIDAD (applied_on)                                      │
│     ┌──────────────────────────────────────────────────────────┐    │
│     │ 0_product_variant  ◄── Más específico (prioridad alta)   │    │
│     │         ↓                                                  │    │
│     │ 1_product                                                  │    │
│     │         ↓                                                  │    │
│     │ 2_product_category                                         │    │
│     │         ↓                                                  │    │
│     │ 3_global           ◄── Menos específico (prioridad baja) │    │
│     └──────────────────────────────────────────────────────────┘    │
│                                                                      │
│  2. CANTIDAD MÍNIMA (min_quantity DESC)                             │
│     - Regla con mayor min_quantity que cumple se aplica primero     │
│     - Permite descuentos escalonados                                │
│                                                                      │
│  3. CATEGORÍA (category_id DESC)                                    │
│     - Categorías más específicas primero                            │
│                                                                      │
│  4. ID (id DESC)                                                    │
│     - Reglas más recientes tienen precedencia                       │
│                                                                      │
│  ═══════════════════════════════════════════════════════════════    │
│  REGLA: LA PRIMERA REGLA QUE COINCIDE SE APLICA                     │
│  ═══════════════════════════════════════════════════════════════    │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘
```

### 3.2 Pseudocódigo del Algoritmo

```python
from decimal import Decimal
from typing import List, Optional, Tuple
from datetime import datetime
from uuid import UUID
from dataclasses import dataclass

@dataclass
class PriceResult:
    """Resultado del cálculo de precio."""
    price: Decimal
    rule_id: Optional[UUID]
    currency_id: UUID
    discount_percent: Optional[Decimal] = None

class PricingEngine:
    """
    Motor de cálculo de precios basado en reglas.
    """

    # Orden SQL para reglas
    RULE_ORDER = """
        applied_on ASC,
        min_quantity DESC,
        category_id DESC,
        id DESC
    """

    def __init__(self, db_session):
        self.db = db_session

    def get_product_price(
        self,
        pricelist_id: UUID,
        product_id: UUID,
        quantity: Decimal = Decimal('1'),
        uom_id: Optional[UUID] = None,
        date: Optional[datetime] = None,
        currency_id: Optional[UUID] = None
    ) -> PriceResult:
        """
        Obtiene el precio de un producto aplicando la lista de precios.

        Args:
            pricelist_id: ID de la lista de precios
            product_id: ID del producto
            quantity: Cantidad a cotizar
            uom_id: Unidad de medida (opcional)
            date: Fecha para validez de reglas
            currency_id: Moneda de salida (opcional)

        Returns:
            PriceResult con precio y regla aplicada
        """
        date = date or datetime.now()

        # Obtener pricelist
        pricelist = self._get_pricelist(pricelist_id)
        if not pricelist:
            raise ValueError(f"Pricelist {pricelist_id} no encontrada")

        # Obtener producto
        product = self._get_product(product_id)
        if not product:
            raise ValueError(f"Producto {product_id} no encontrado")

        # Convertir cantidad a UoM del producto
        qty_in_product_uom = self._convert_quantity(
            quantity, uom_id, product['uom_id']
        )

        # Buscar regla aplicable
        rule = self._find_applicable_rule(
            pricelist_id=pricelist_id,
            product=product,
            quantity=qty_in_product_uom,
            date=date
        )

        # Calcular precio
        if rule:
            price = self._compute_rule_price(
                rule=rule,
                product=product,
                quantity=qty_in_product_uom,
                uom_id=uom_id,
                date=date
            )
        else:
            # Sin regla: usar precio de lista
            price = Decimal(str(product['list_price']))

        # Convertir a UoM solicitada
        if uom_id and uom_id != product['uom_id']:
            price = self._convert_price_uom(price, product['uom_id'], uom_id)

        # Convertir a moneda solicitada
        target_currency = currency_id or pricelist['currency_id']
        if pricelist['currency_id'] != target_currency:
            price = self._convert_currency(
                price,
                pricelist['currency_id'],
                target_currency,
                date
            )

        # Calcular descuento para mostrar
        discount_percent = None
        if rule and rule['compute_price'] == 'percentage':
            discount_percent = Decimal(str(rule['percent_price']))

        return PriceResult(
            price=price,
            rule_id=rule['id'] if rule else None,
            currency_id=target_currency,
            discount_percent=discount_percent
        )

    def _find_applicable_rule(
        self,
        pricelist_id: UUID,
        product: dict,
        quantity: Decimal,
        date: datetime
    ) -> Optional[dict]:
        """
        Encuentra la primera regla aplicable según prioridad.
        """
        # Construir dominio de búsqueda
        query = """
            SELECT *
            FROM pricing.pricelist_items
            WHERE pricelist_id = :pricelist_id
              -- Filtro de fechas
              AND (date_start IS NULL OR date_start <= :date)
              AND (date_end IS NULL OR date_end >= :date)
              -- Filtro de cantidad
              AND min_quantity <= :quantity
              -- Filtro de producto/categoría
              AND (
                -- Global
                (applied_on = '3_global')
                -- Categoría (incluyendo padres)
                OR (applied_on = '2_product_category'
                    AND category_id IN (
                        SELECT id FROM products.product_categories
                        WHERE :category_path LIKE parent_path || '%'
                    ))
                -- Template
                OR (applied_on = '1_product'
                    AND product_tmpl_id = :product_tmpl_id)
                -- Variante
                OR (applied_on = '0_product_variant'
                    AND product_id = :product_id)
              )
            ORDER BY {order}
            LIMIT 1
        """.format(order=self.RULE_ORDER)

        result = self.db.execute(query, {
            'pricelist_id': pricelist_id,
            'date': date,
            'quantity': float(quantity),
            'category_path': product['category_path'],
            'product_tmpl_id': product['product_tmpl_id'],
            'product_id': product['id']
        }).fetchone()

        return dict(result) if result else None

    def _compute_rule_price(
        self,
        rule: dict,
        product: dict,
        quantity: Decimal,
        uom_id: Optional[UUID],
        date: datetime
    ) -> Decimal:
        """
        Aplica la regla para calcular el precio.
        """
        compute_price = rule['compute_price']

        if compute_price == 'fixed':
            # Precio fijo
            return self._apply_fixed_price(rule, product, uom_id)

        elif compute_price == 'percentage':
            # Descuento porcentual
            base_price = self._get_base_price(rule, product, quantity, date)
            percent = Decimal(str(rule['percent_price']))
            return base_price * (1 - percent / 100)

        elif compute_price == 'formula':
            # Fórmula completa
            return self._apply_formula(rule, product, quantity, uom_id, date)

        # Fallback: precio de lista
        return Decimal(str(product['list_price']))

    def _apply_fixed_price(
        self,
        rule: dict,
        product: dict,
        uom_id: Optional[UUID]
    ) -> Decimal:
        """Aplica precio fijo con conversión de UoM."""
        fixed_price = Decimal(str(rule['fixed_price']))

        # Convertir si UoM diferente
        if uom_id and uom_id != product['uom_id']:
            fixed_price = self._convert_price_uom(
                fixed_price, product['uom_id'], uom_id
            )

        return fixed_price

    def _apply_formula(
        self,
        rule: dict,
        product: dict,
        quantity: Decimal,
        uom_id: Optional[UUID],
        date: datetime
    ) -> Decimal:
        """
        Aplica fórmula de precio:
        1. Obtener precio base
        2. Aplicar descuento/markup
        3. Redondear
        4. Aplicar surcharge
        5. Aplicar restricciones de margen
        """
        # 1. Precio base
        base_price = self._get_base_price(rule, product, quantity, date)
        price_limit = base_price  # Para cálculo de márgenes

        # 2. Aplicar descuento o markup
        if rule['base'] == 'standard_price':
            # Para costo: usar markup (positivo = incremento)
            markup = Decimal(str(rule['price_markup'] or 0))
            price = base_price * (1 + markup / 100)
        else:
            # Para precio de lista: usar descuento (positivo = reducción)
            discount = Decimal(str(rule['price_discount'] or 0))
            price = base_price * (1 - discount / 100)

        # 3. Redondear
        if rule['price_round']:
            round_precision = Decimal(str(rule['price_round']))
            price = self._round_price(price, round_precision)

        # 4. Aplicar surcharge (después del redondeo)
        if rule['price_surcharge']:
            surcharge = Decimal(str(rule['price_surcharge']))
            # Convertir surcharge a UoM si es necesario
            if uom_id and uom_id != product['uom_id']:
                surcharge = self._convert_price_uom(
                    surcharge, product['uom_id'], uom_id
                )
            price += surcharge

        # 5. Aplicar restricciones de margen
        if rule['price_min_margin']:
            min_margin = Decimal(str(rule['price_min_margin']))
            if uom_id and uom_id != product['uom_id']:
                min_margin = self._convert_price_uom(
                    min_margin, product['uom_id'], uom_id
                )
            price = max(price, price_limit + min_margin)

        if rule['price_max_margin']:
            max_margin = Decimal(str(rule['price_max_margin']))
            if uom_id and uom_id != product['uom_id']:
                max_margin = self._convert_price_uom(
                    max_margin, product['uom_id'], uom_id
                )
            price = min(price, price_limit + max_margin)

        return price

    def _get_base_price(
        self,
        rule: dict,
        product: dict,
        quantity: Decimal,
        date: datetime
    ) -> Decimal:
        """
        Obtiene el precio base según configuración de la regla.
        """
        base = rule['base']

        if base == 'pricelist':
            # Precio de otra lista (cascada)
            base_pricelist_id = rule['base_pricelist_id']
            result = self.get_product_price(
                pricelist_id=base_pricelist_id,
                product_id=product['id'],
                quantity=quantity,
                date=date
            )
            return result.price

        elif base == 'standard_price':
            # Precio de costo
            return Decimal(str(product['standard_price']))

        else:  # list_price
            # Precio de lista
            return Decimal(str(product['list_price']))

    def _round_price(
        self,
        price: Decimal,
        precision: Decimal
    ) -> Decimal:
        """
        Redondea el precio a la precisión especificada.

        Ejemplos:
        - precision=0.01 → redondea a centavos
        - precision=1.0 → redondea a unidad
        - precision=5.0 → redondea a múltiplo de 5
        - precision=10.0 → redondea a decena
        """
        if precision <= 0:
            return price

        # Redondeo matemático estándar
        return (price / precision).quantize(Decimal('1')) * precision

    def get_tiered_prices(
        self,
        pricelist_id: UUID,
        product_id: UUID,
        quantities: List[Decimal],
        date: Optional[datetime] = None
    ) -> List[dict]:
        """
        Obtiene precios para múltiples cantidades (mostrar tabla de descuentos).

        Returns:
            Lista de {quantity, price, discount_percent, rule_id}
        """
        results = []

        for qty in sorted(quantities):
            result = self.get_product_price(
                pricelist_id=pricelist_id,
                product_id=product_id,
                quantity=qty,
                date=date
            )

            # Calcular descuento sobre precio de lista
            product = self._get_product(product_id)
            list_price = Decimal(str(product['list_price']))
            discount_pct = Decimal('0')

            if list_price > 0 and result.price < list_price:
                discount_pct = ((list_price - result.price) / list_price * 100
                               ).quantize(Decimal('0.01'))

            results.append({
                'quantity': float(qty),
                'price': float(result.price),
                'discount_percent': float(discount_pct),
                'rule_id': str(result.rule_id) if result.rule_id else None
            })

        return results
```

---

## 4. Métodos de Cálculo

### 4.1 Precio Fijo

```python
# Configuración:
compute_price = 'fixed'
fixed_price = 99.00

# Resultado:
precio = 99.00  # Independiente del precio base
```

### 4.2 Descuento Porcentual

```python
# Configuración:
compute_price = 'percentage'
base = 'list_price'  # o 'standard_price' o 'pricelist'
percent_price = 15.0  # 15% de descuento

# Cálculo:
base_price = producto.list_price  # ej: 100.00
precio = base_price * (1 - 15/100)  # = 85.00
```

### 4.3 Fórmula Completa

```python
# Configuración:
compute_price = 'formula'
base = 'list_price'
price_discount = 10.0      # 10% descuento
price_round = 5.0          # Redondear a múltiplo de 5
price_surcharge = -0.01    # Restar 1 centavo
price_min_margin = 20.0    # Margen mínimo $20
price_max_margin = 50.0    # Margen máximo $50

# Cálculo paso a paso:
base_price = 100.00              # Precio de lista

# Paso 1: Aplicar descuento
precio = 100.00 * (1 - 10/100)   # = 90.00

# Paso 2: Redondear
precio = round(90.00, 5.0)       # = 90.00 (ya es múltiplo de 5)

# Paso 3: Aplicar surcharge
precio = 90.00 + (-0.01)         # = 89.99

# Paso 4: Verificar márgenes
# min_margin: precio >= base_price + 20 → 89.99 >= 120 → NO
# Se ajusta a margen mínimo si está por debajo
# max_margin: precio <= base_price + 50 → 89.99 <= 150 → SÍ

# Resultado final (si no hay restricción de margen activa):
precio = 89.99
```

---

## 5. Descuentos Escalonados (Tiered Pricing)

### 5.1 Configuración de Ejemplo

```python
# Lista de precios con descuentos por volumen:

# Regla 1: 1-9 unidades (sin descuento)
{
    'pricelist_id': pricelist_id,
    'applied_on': '3_global',
    'min_quantity': 0,
    'compute_price': 'formula',
    'base': 'list_price',
    'price_discount': 0
}

# Regla 2: 10-49 unidades (5% descuento)
{
    'pricelist_id': pricelist_id,
    'applied_on': '3_global',
    'min_quantity': 10,
    'compute_price': 'formula',
    'base': 'list_price',
    'price_discount': 5
}

# Regla 3: 50-99 unidades (10% descuento)
{
    'pricelist_id': pricelist_id,
    'applied_on': '3_global',
    'min_quantity': 50,
    'compute_price': 'formula',
    'base': 'list_price',
    'price_discount': 10
}

# Regla 4: 100+ unidades (15% descuento)
{
    'pricelist_id': pricelist_id,
    'applied_on': '3_global',
    'min_quantity': 100,
    'compute_price': 'formula',
    'base': 'list_price',
    'price_discount': 15
}
```

### 5.2 Tabla de Precios Resultante

| Cantidad | Regla Aplicada | Descuento | Precio (base $100) |
|----------|----------------|-----------|-------------------|
| 1-9 | Regla 1 | 0% | $100.00 |
| 10-49 | Regla 2 | 5% | $95.00 |
| 50-99 | Regla 3 | 10% | $90.00 |
| 100+ | Regla 4 | 15% | $85.00 |

---

## 6. API REST

### 6.1 Endpoints

```yaml
openapi: 3.0.3
info:
  title: Pricing Rules API
  version: 1.0.0

paths:
  /api/v1/pricing/pricelists:
    get:
      summary: Listar listas de precios
      parameters:
        - name: currency_id
          in: query
          schema:
            type: string
            format: uuid
        - name: is_active
          in: query
          schema:
            type: boolean
        - name: selectable
          in: query
          schema:
            type: boolean
      responses:
        '200':
          description: Lista de pricelists
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Pricelist'

    post:
      summary: Crear lista de precios
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PricelistCreate'
      responses:
        '201':
          description: Pricelist creada

  /api/v1/pricing/pricelists/{id}:
    get:
      summary: Obtener lista de precios con reglas
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Pricelist con reglas
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PricelistDetail'

    put:
      summary: Actualizar lista de precios
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PricelistUpdate'
      responses:
        '200':
          description: Pricelist actualizada

    delete:
      summary: Eliminar lista de precios
      responses:
        '204':
          description: Pricelist eliminada

  /api/v1/pricing/pricelists/{id}/items:
    get:
      summary: Listar reglas de una pricelist
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: product_id
          in: query
          schema:
            type: string
            format: uuid
        - name: category_id
          in: query
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Lista de reglas
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PricelistItem'

    post:
      summary: Crear regla de precios
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PricelistItemCreate'
      responses:
        '201':
          description: Regla creada

  /api/v1/pricing/pricelists/{id}/items/{item_id}:
    put:
      summary: Actualizar regla
      responses:
        '200':
          description: Regla actualizada

    delete:
      summary: Eliminar regla
      responses:
        '204':
          description: Regla eliminada

  /api/v1/pricing/calculate:
    post:
      summary: Calcular precio de producto(s)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                pricelist_id:
                  type: string
                  format: uuid
                products:
                  type: array
                  items:
                    type: object
                    properties:
                      product_id:
                        type: string
                        format: uuid
                      quantity:
                        type: number
                        default: 1
                      uom_id:
                        type: string
                        format: uuid
                date:
                  type: string
                  format: date-time
                currency_id:
                  type: string
                  format: uuid
              required:
                - pricelist_id
                - products
      responses:
        '200':
          description: Precios calculados
          content:
            application/json:
              schema:
                type: object
                properties:
                  prices:
                    type: array
                    items:
                      $ref: '#/components/schemas/PriceCalculation'
                  pricelist:
                    type: object
                    properties:
                      id:
                        type: string
                      name:
                        type: string
                      currency_id:
                        type: string

  /api/v1/pricing/tiered-prices:
    post:
      summary: Obtener tabla de precios escalonados
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                pricelist_id:
                  type: string
                  format: uuid
                product_id:
                  type: string
                  format: uuid
                quantities:
                  type: array
                  items:
                    type: number
                  example: [1, 10, 50, 100]
              required:
                - pricelist_id
                - product_id
      responses:
        '200':
          description: Tabla de precios
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/TieredPrice'

  /api/v1/pricing/validate-promo-code:
    post:
      summary: Validar código promocional
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                code:
                  type: string
                partner_id:
                  type: string
                  format: uuid
              required:
                - code
      responses:
        '200':
          description: Código válido
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid:
                    type: boolean
                  pricelist_id:
                    type: string
                    format: uuid
                  pricelist_name:
                    type: string
                  message:
                    type: string

components:
  schemas:
    Pricelist:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        code:
          type: string
        currency_id:
          type: string
          format: uuid
        currency_name:
          type: string
        company_id:
          type: string
          format: uuid
        sequence:
          type: integer
        is_active:
          type: boolean
        selectable:
          type: boolean
        item_count:
          type: integer

    PricelistDetail:
      allOf:
        - $ref: '#/components/schemas/Pricelist'
        - type: object
          properties:
            items:
              type: array
              items:
                $ref: '#/components/schemas/PricelistItem'

    PricelistItem:
      type: object
      properties:
        id:
          type: string
          format: uuid
        applied_on:
          type: string
          enum: [3_global, 2_product_category, 1_product, 0_product_variant]
        applied_on_display:
          type: string
        product_id:
          type: string
          format: uuid
        product_name:
          type: string
        product_tmpl_id:
          type: string
          format: uuid
        category_id:
          type: string
          format: uuid
        category_name:
          type: string
        min_quantity:
          type: number
        date_start:
          type: string
          format: date-time
        date_end:
          type: string
          format: date-time
        compute_price:
          type: string
          enum: [fixed, percentage, formula]
        base:
          type: string
          enum: [list_price, standard_price, pricelist]
        fixed_price:
          type: number
        percent_price:
          type: number
        price_discount:
          type: number
        price_markup:
          type: number
        price_round:
          type: number
        price_surcharge:
          type: number
        price_min_margin:
          type: number
        price_max_margin:
          type: number

    PricelistItemCreate:
      type: object
      properties:
        applied_on:
          type: string
          enum: [3_global, 2_product_category, 1_product, 0_product_variant]
          default: 3_global
        product_id:
          type: string
          format: uuid
        product_tmpl_id:
          type: string
          format: uuid
        category_id:
          type: string
          format: uuid
        min_quantity:
          type: number
          default: 0
        date_start:
          type: string
          format: date-time
        date_end:
          type: string
          format: date-time
        compute_price:
          type: string
          enum: [fixed, percentage, formula]
        base:
          type: string
          enum: [list_price, standard_price, pricelist]
        base_pricelist_id:
          type: string
          format: uuid
        fixed_price:
          type: number
        percent_price:
          type: number
        price_discount:
          type: number
        price_markup:
          type: number
        price_round:
          type: number
        price_surcharge:
          type: number
        price_min_margin:
          type: number
        price_max_margin:
          type: number
      required:
        - compute_price

    PriceCalculation:
      type: object
      properties:
        product_id:
          type: string
          format: uuid
        quantity:
          type: number
        price:
          type: number
        currency_id:
          type: string
          format: uuid
        rule_id:
          type: string
          format: uuid
        discount_percent:
          type: number
        base_price:
          type: number
          description: Precio antes del descuento

    TieredPrice:
      type: object
      properties:
        quantity:
          type: number
        price:
          type: number
        discount_percent:
          type: number
        rule_id:
          type: string
          format: uuid
        savings:
          type: number
          description: Ahorro vs precio sin descuento
```

---

## 7. Integración con Órdenes de Venta

### 7.1 Flujo de Aplicación

```
┌─────────────────────────────────────────────────────────────────────┐
│                 FLUJO DE CÁLCULO DE PRECIO EN SO                     │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  ┌──────────────────┐                                               │
│  │ Crear Línea de   │                                               │
│  │ Orden de Venta   │                                               │
│  └────────┬─────────┘                                               │
│           │                                                          │
│           ▼                                                          │
│  ┌──────────────────┐                                               │
│  │ Seleccionar      │                                               │
│  │ Producto         │                                               │
│  └────────┬─────────┘                                               │
│           │                                                          │
│           ▼                                                          │
│  ┌──────────────────┐     ┌─────────────────┐                       │
│  │ Obtener          │◄────│ Pricelist del   │                       │
│  │ Pricelist        │     │ Cliente/Orden   │                       │
│  └────────┬─────────┘     └─────────────────┘                       │
│           │                                                          │
│           ▼                                                          │
│  ┌──────────────────┐                                               │
│  │ Calcular Precio  │                                               │
│  │ (PricingEngine)  │                                               │
│  └────────┬─────────┘                                               │
│           │                                                          │
│           ├─────────────────────────────────────┐                   │
│           ▼                                     ▼                   │
│  ┌──────────────────┐                  ┌──────────────────┐        │
│  │ Asignar          │                  │ Calcular         │        │
│  │ price_unit       │                  │ Descuento %      │        │
│  └────────┬─────────┘                  │ (si aplica)      │        │
│           │                            └────────┬─────────┘        │
│           │                                     │                   │
│           └─────────────────┬───────────────────┘                   │
│                             ▼                                        │
│                    ┌──────────────────┐                             │
│                    │ Recalcular       │                             │
│                    │ Subtotal         │                             │
│                    └──────────────────┘                             │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘
```

### 7.2 Campos en Línea de Orden

```python
# Campos calculados automáticamente:
price_unit           # Precio unitario (de pricelist)
discount             # Descuento % (si compute_price='percentage')
price_subtotal       # price_unit * quantity * (1 - discount/100)

# Campo de referencia:
pricelist_item_id    # Regla aplicada (para trazabilidad)
```

---

## 8. Consideraciones de Rendimiento

### 8.1 Índices Optimizados

```sql
-- Búsqueda de reglas por pricelist y producto
CREATE INDEX idx_pricelist_items_search
    ON pricing.pricelist_items(
        pricelist_id,
        applied_on,
        product_id,
        product_tmpl_id,
        category_id
    );

-- Filtro por fechas activas
CREATE INDEX idx_pricelist_items_active_dates
    ON pricing.pricelist_items(date_start, date_end)
    WHERE date_start IS NOT NULL OR date_end IS NOT NULL;

-- Búsqueda por cantidad mínima
CREATE INDEX idx_pricelist_items_qty
    ON pricing.pricelist_items(pricelist_id, min_quantity DESC);
```

### 8.2 Caching

```python
# Cache de pricelists activas
PRICELIST_CACHE = TTLCache(maxsize=100, ttl=300)  # 5 minutos

# Cache de reglas por pricelist
RULES_CACHE = TTLCache(maxsize=1000, ttl=60)  # 1 minuto

# Cache de precios calculados (por sesión de cotización)
PRICE_CACHE = LRUCache(maxsize=10000)

def cache_key(pricelist_id, product_id, quantity, date):
    return f"{pricelist_id}:{product_id}:{quantity}:{date.date()}"
```

---

## 9. Ejemplos de Configuración

### 9.1 Precio Mayorista (Cost + 30%)

```python
pricelist = Pricelist.create({
    'name': 'Precio Mayorista',
    'currency_id': mxn_id,
    'item_ids': [(0, 0, {
        'applied_on': '3_global',
        'compute_price': 'formula',
        'base': 'standard_price',  # Costo
        'price_markup': 30,        # +30%
    })]
})
```

### 9.2 Promoción Temporal

```python
pricelist_item = PricelistItem.create({
    'pricelist_id': main_pricelist_id,
    'applied_on': '2_product_category',
    'category_id': electronics_id,
    'compute_price': 'percentage',
    'percent_price': 20,  # 20% descuento
    'date_start': '2025-12-01',
    'date_end': '2025-12-31',
})
```

### 9.3 Precios Psicológicos (terminados en .99)

```python
pricelist_item = PricelistItem.create({
    'pricelist_id': retail_pricelist_id,
    'applied_on': '3_global',
    'compute_price': 'formula',
    'base': 'list_price',
    'price_discount': 0,
    'price_round': 10.0,       # Redondear a decena
    'price_surcharge': -0.01,  # Restar 1 centavo
    # Resultado: 100 → 100 → 99.99
})
```

---

## 10. Referencias

- Odoo product.pricelist model
- Odoo product.pricelist.item model
- Pricing strategies best practices

---

**Documento generado para ERP-SUITE**
**Versión**: 1.0
**Fecha**: 2025-01-09
