Metadata-Version: 2.4
Name: guias_beetrack_Ricardo_Fuentes
Version: 0.0.1
Summary: Sincronizacion Dynamics 365 -> Beetrack
Project-URL: Homepage, https://github.com/pypa/sampleproject
Project-URL: Issues, https://github.com/pypa/sampleproject/issues
Author-email: Ricardo Fuentes <ricardof_EXT@centrodistribuidor.com>
License: MIT License
        
        Copyright (c) 2026 Ricardo Fuentes
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Requires-Python: >=3.9
Requires-Dist: apscheduler>=3.11.2
Requires-Dist: httpx>=0.28.1
Requires-Dist: python-dotenv>=1.2.1
Description-Content-Type: text/markdown

# Sincronización Dynamics 365 → Beetrack

Servicio automático que consulta pedidos pendientes en **Microsoft Dynamics 365**, construye guías de despacho y las envía a **Beetrack / DispatchTrack**. Una vez confirmado el envío, actualiza el estado del pedido en Dynamics para evitar duplicados.

---

## Índice

1. [¿Qué hace el sistema?](#qué-hace-el-sistema)
2. [Estructura del proyecto](#estructura-del-proyecto)
3. [Flujo de ejecución paso a paso](#flujo-de-ejecución-paso-a-paso)
4. [Módulos](#módulos)
5. [Configuración (.env)](#configuración-env)
6. [Instalación y ejecución](#instalación-y-ejecución)
7. [Logs](#logs)
8. [Manejo de errores](#manejo-de-errores)

---

## ¿Qué hace el sistema?

```
Dynamics 365  ──────────────────────────────────►  Beetrack
  (pedidos con DocState = "Pending")                 (guías de despacho)
        │                                                   │
        └──── al confirmar envío ────────────────────────── ┘
                DocState → "Recived"
```

El scheduler corre cada **5 minutos**. Si no hay pedidos pendientes, termina silenciosamente. Si los hay, ejecuta el pipeline completo.

---

## Estructura del proyecto

```
tareas_atomaticas/
│
├── .env                          # Variables de entorno (credenciales, configuración)
├── .env.example                  # Plantilla de variables (sin valores reales)
├── requirements.txt              # Dependencias Python
├── pending_dynamics_updates.json # Guías enviadas a Beetrack pero Dynamics no confirmó (auto-generado)
│
└── guias_beetrack/
    │
    ├── main.py                   # Punto de entrada, scheduler APScheduler
    │
    ├── dynamics/                 # Lógica de negocio e integración
    │   ├── app_config.py         # Centraliza todas las constantes configurables
    │   ├── config_env.py         # Lectura de variables de entorno (.env)
    │   ├── token_dynamics.py     # Gestión del token OAuth2 de Dynamics (con caché)
    │   ├── clases_guias_dynamics.py  # Modelos de datos (dataclasses)
    │   ├── guias_dynamics.py     # Consultas OData a Dynamics 365
    │   ├── guias.py              # Orquestación: filtros, armado y envío de guías
    │   └── servicio_dynamics.py  # Capa HTTP (GET/PATCH Dynamics, POST Beetrack) con retry
    │
    ├── utils/
    │   ├── logger.py             # Logger con salida a consola y archivos rotativos
    │   └── correlation.py        # ID de correlación por ciclo (ContextVar)
    │
    └── tests/
        └── test_token_dynamics.py
```

---

## Flujo de ejecución paso a paso

Cada 5 minutos `main.py` ejecuta el siguiente pipeline:

```
┌─────────────────────────────────────────────────────────────────────┐
│  INICIO DE CICLO  (run_id: a3f7c1d2)                                │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  0. Revisar pendientes                                              │
│     └─ ¿Hay guías en pending_dynamics_updates.json?                 │
│        → Sí: avisar en log (se procesarán en el paso 5)             │
│        → No: continuar                                              │
│                                                                     │
│  1. Consultar pedidos  [Dynamics: CNDBeeTrackSalesTablesEntity]      │
│     └─ Filtros: DocState=Pending, fecha=hoy, SalesStatus=Invoiced   │
│        IdRoute≠CARGO EXPRESO, grupos IG6/CD6/BC/CAB/CDI             │
│                                                                     │
│  2. Datos primarios (3 consultas en paralelo)                       │
│     ├─ Facturas    [CUSTINVOICEJOURCNDsEntity]  → por SalesId       │
│     ├─ Clientes    [CustomersV3]                → por InvoiceAccount │
│     └─ Productos   [CNDBeeTrackSalesLinesEntity] → por SalesId       │
│                                                                     │
│  3. Datos secundarios (3 consultas en paralelo)                     │
│     ├─ Ubicaciones [CDSPostalAddressHistoryV2]  → lat/lng            │
│     ├─ Direcciones [LogisticsPostalAddressBiEntities] → dirección    │
│     └─ Dimensiones [WHSPHYSDIMUOMCNDsEntity]   → peso/alto/ancho    │
│                                                                     │
│  4. Armar guías                                                     │
│     └─ Combina todos los datos en objetos GuiaResponse              │
│                                                                     │
│  5. Enviar guías a Beetrack  (máx. 10 concurrentes)                 │
│     Por cada guía:                                                  │
│     ┌─ ¿Está en pendientes?                                         │
│     │   → Sí: saltar POST, solo reintentar PATCH a Dynamics         │
│     │   → No: POST a Beetrack → si OK: PATCH a Dynamics             │
│     └─ Si PATCH falla: guardar en pending_dynamics_updates.json     │
│                                                                     │
│  FIN DE CICLO                                                       │
└─────────────────────────────────────────────────────────────────────┘
```

---

## Módulos

### `main.py`
Punto de entrada. Contiene la función `guias_beetrack()` que orquesta el pipeline completo, y el bloque `__main__` que inicia el `BlockingScheduler` con intervalo de 5 minutos.

---

### `dynamics/app_config.py`
Centraliza **todas las constantes configurables** del negocio. Los valores se leen del `.env` con defaults. Nunca se hardcodean en la lógica.

| Constante | Default | Descripción |
|-----------|---------|-------------|
| `BEETRACK_SALES_IDS_EXCLUIDOS` | `OV-904710,OV-904736` | Guías que nunca se envían a Beetrack |
| `DYNAMICS_SALES_IDS_EXCLUIDOS` | `OV-912698,OV-912941` | Pedidos excluidos de la consulta OData |
| `PICKUP_ADDRESS_NAME` | `Bodega Zona 4` | Nombre de la bodega de origen |
| `PICKUP_LATITUDE/LONGITUDE` | `14.62…, -90.51…` | Coordenadas de la bodega |
| `DELIVERY_MIN_OFFSET_HOURS` | `7` | Horas antes del `DeliveryCustomerDateTime` para ventana mínima |
| `DELIVERY_MAX_OFFSET_HOURS` | `6` | Horas antes del `DeliveryCustomerDateTime` para ventana máxima |
| `DISPATCH_PRIORITY` | `1` | Prioridad de despacho en Beetrack |
| `DISPATCH_MODE` | `3` | Modo de despacho en Beetrack |
| `DISPATCH_PLACE` | `Servicio Express` | Nombre del servicio en Beetrack |
| `DOMINIOS_EMAIL_VALIDOS` | `@centrodistribuidor.com,…` | Dominios que habilitan envío de correo al cliente |
| `GRUPOS_VENTAS_VALIDOS` | `CVTA-0006,…` | Grupos de venta que habilitan correo |
| `BEETRACK_MAX_CONCURRENT` | `10` | Máximo de guías enviándose en paralelo |
| `PENDING_DYNAMICS_UPDATES_FILE` | `pending_dynamics_updates.json` | Archivo de guías pendientes de confirmación |

---

### `dynamics/token_dynamics.py`
Gestiona el token **OAuth2** para Dynamics 365.

- El token se guarda en memoria con su tiempo de expiración.
- Cada consulta llama a `validacion_token()`: si el token es válido lo reutiliza, si expiró solicita uno nuevo.
- Esto evita hacer una llamada de autenticación en cada request HTTP.

```
validacion_token()
  └─ ¿Token en memoria vigente?
       → Sí: devuelve el token cacheado
       → No: POST a Azure AD → nuevo token → guardar en memoria → devolver
```

---

### `dynamics/guias_dynamics.py`
Consultas OData a Dynamics 365. Cada método es estático y async.

| Método | Entidad Dynamics | Para qué |
|--------|-----------------|----------|
| `obtener_pedidos` | `CNDBeeTrackSalesTablesEntity` | Pedidos del día con filtros de estado/grupo |
| `obtener_info_factura` | `CUSTINVOICEJOURCNDsEntity` | InvoiceId y dirección postal por SalesId |
| `obtener_info_cliente` | `CustomersV3` | Teléfono, email, ubicación por cuenta |
| `obtener_info_location_id` | `CDSPostalAddressHistoryV2` | Coordenadas GPS por LocationId |
| `obtener_info_direccion` | `LogisticsPostalAddressBiEntities` | Dirección en texto por SourceKey |
| `obtener_info_productos` | `CNDBeeTrackSalesLinesEntity` | Líneas de producto por SalesId |
| `obtener_info_dimensiones` | `WHSPHYSDIMUOMCNDsEntity` | Alto, peso, ancho, profundidad por ItemId |
| `actualizar_estado_guia` | `SALESTABLECNDsEntity` | PATCH: cambia DocState a "Recived" |

---

### `dynamics/guias.py`
Módulo principal de orquestación. Tiene tres clases y una función:

**`FiltrosGuiasBeetrack`** — Construye los strings de filtro OData a partir de los datos recuperados. El helper interno `_armar_filtro()` evita repetición de código.

**`GuiasInfoDynamics`** — Wraps de las consultas OData. Recibe el filtro, llama a `guias_dynamics.py`, y devuelve un diccionario indexado por la clave correspondiente (SalesId, CustomerAccount, etc.) para búsqueda O(1) al armar las guías.

**`ArmarGuiasBeetrack`** — Combina todos los datos en objetos `GuiaResponse` y los envía a Beetrack con control de concurrencia y seguridad transaccional.

**`procesar_actualizaciones_pendientes()`** — Función que al inicio de cada ciclo informa si hay guías cuyo PATCH a Dynamics quedó pendiente del ciclo anterior.

---

### `dynamics/servicio_dynamics.py`
Capa HTTP pura. Tres funciones async:

- `get_consultar_dynamics()` — GET a OData con token Bearer. Elimina metadatos `@odata.etag`.
- `patch_actualizar_dynamics()` — PATCH para actualizar estado en Dynamics.
- `post_beetrack()` — POST a la API de Beetrack con X-AUTH-TOKEN.

Todas usan `_con_reintentos()`: ante errores de red (timeout, conexión) o HTTP transitorio (429, 500-504) reintenta hasta 3 veces con backoff exponencial (1s → 2s → 4s).

---

### `dynamics/clases_guias_dynamics.py`
Modelos de datos (Python `dataclasses`):

| Clase | Descripción |
|-------|-------------|
| `InformacionConsulta` | Respuesta genérica: `consulta: bool`, `mensaje: str`, `data` |
| `PickupAddress` | Dirección de recogida (bodega de origen) |
| `Dimension` | Par nombre/valor para dimensiones de producto |
| `ProductoBeetrack` | Producto con descripción, cantidad, código y dimensiones |
| `GuiaResponse` | Guía completa lista para enviar a Beetrack |

`GuiaResponse` se construye en etapas mediante setters:
1. `set_info_facturas()` — datos del pedido y ventana de entrega
2. `set_info_cliente()` — contacto, dirección, validación de correo/grupo
3. `set_info_localizacion()` — coordenadas GPS
4. `set_dir_facturas()` — InvoiceId y dirección postal
5. `set_info_productos()` — lista de productos

---

### `utils/logger.py`
Logger con salida dual:

| Destino | Nivel | Retención | Formato |
|---------|-------|-----------|---------|
| **Consola** | INFO y superior | — | `HH:MM:SS \| NIVEL \| [run_id] \| mensaje` |
| `logs/app_DD-MM-YYYY.log` | INFO y WARNING | 7 días | Formato completo con archivo:línea |
| `logs/errors_DD-MM-YYYY.log` | ERROR y CRITICAL | 30 días | Formato completo con archivo:línea |

Cada línea incluye el `run_id` del ciclo activo, lo que permite filtrar todos los eventos de una ejecución específica.

---

### `utils/correlation.py`
Define `run_id`, un `ContextVar` que se inicializa al comienzo de cada ciclo con un UUID corto (8 caracteres). Al usar `asyncio`, el valor se propaga automáticamente a todas las corrutinas del mismo `asyncio.run()`, sin necesidad de pasarlo como parámetro.

---

## Configuración (.env)

Copiar `.env.example` a `.env` y completar los valores:

```bash
cp .env.example .env
```

### Variables obligatorias

```env
# Autenticación Dynamics 365 (Azure AD)
DYNAMICS_URL_TOKEN=https://login.microsoftonline.com/<TENANT_ID>/oauth2/token
DYNAMICS_URL_ACCESO=https://<ENVIRONMENT>.operations.dynamics.com/
DYNAMICS_ID_CLIENTE=<CLIENT_ID>
DYNAMICS_CLAVE_CLIENTE=<CLIENT_SECRET>
DYNAMICS_TIPO_CREDENCIAL=client_credentials

# Beetrack / DispatchTrack
BEETRACK_URL_ACCESO=https://<INSTANCIA>.dispatchtrack.com/api/external/v1/dispatches
BEETRACK_TOKEN_ACCESO=<API_TOKEN>
```

### Variables opcionales (tienen defaults en app_config.py)

```env
BEETRACK_SALES_IDS_EXCLUIDOS=OV-000000,OV-000001
DYNAMICS_SALES_IDS_EXCLUIDOS=OV-000000,OV-000001
PICKUP_ADDRESS_NAME=Bodega Zona 4
PICKUP_LATITUDE=14.621669674128709
PICKUP_LONGITUDE=-90.51804696621112
DELIVERY_MIN_OFFSET_HOURS=7
DELIVERY_MAX_OFFSET_HOURS=6
DISPATCH_PRIORITY=1
DISPATCH_MODE=3
DISPATCH_PLACE=Servicio Express
DOMINIOS_EMAIL_VALIDOS=@centrodistribuidor.com,@servir.com.gt
GRUPOS_VENTAS_VALIDOS=CVTA-0006,CVTA-0029,...
BEETRACK_MAX_CONCURRENT=10
PENDING_DYNAMICS_UPDATES_FILE=pending_dynamics_updates.json
```

---

## Instalación y ejecución

### Instalación desde PyPI

```bash
pip install guias-beetrack-ricardo-fuentes
guias-beetrack
```

### 1. Crear entorno virtual e instalar dependencias

```bash
python -m venv tareas_auto
tareas_auto\Scripts\activate      # Windows
pip install -r requirements.txt
```

### 2. Configurar variables de entorno

```bash
copy .env.example .env
# Editar .env con las credenciales reales
```

### 3. Ejecutar

```bash
# Desde la raiz del proyecto
python -m guias_beetrack
```

El proceso corre indefinidamente. Para detenerlo: `Ctrl+C`.

### Ejecutar solo una vez (sin scheduler)

Descomentar en `main.py`:
```python
if __name__ == "__main__":
    main()
```

---

## Logs

Al ejecutarse se verá en consola:

```
10:05:00 | INFO    | [-       ] =======================================================
10:05:00 | INFO    | [-       ] INICIO DE CICLO  Dynamics → Beetrack
10:05:00 | INFO    | [-       ] =======================================================
10:05:00 | INFO    | [a3f7c1d2] [1/5] Consultando pedidos en Dynamics...
10:05:01 | INFO    | [a3f7c1d2] [1/5] Pedidos encontrados: 8
10:05:01 | INFO    | [a3f7c1d2] [2/5] Obteniendo facturas, clientes y productos (paralelo)...
10:05:02 | INFO    | [a3f7c1d2] [2/5] Facturas: 8 | Clientes: 5 | Productos para 8 pedidos
10:05:02 | INFO    | [a3f7c1d2] [3/5] Obteniendo ubicaciones, direcciones y dimensiones (paralelo)...
10:05:03 | INFO    | [a3f7c1d2] [3/5] Ubicaciones: 5 | Direcciones: 8 | Dimensiones: 12
10:05:03 | INFO    | [a3f7c1d2] [4/5] Armando guías...
10:05:03 | INFO    | [a3f7c1d2] [4/5] 8 guías armadas.
10:05:03 | INFO    | [a3f7c1d2] [5/5] Enviando 8 guías a Beetrack...
10:05:03 | INFO    | [a3f7c1d2] OV-905123 [1/8] → Enviando a Beetrack...
10:05:04 | INFO    | [a3f7c1d2] OV-905123 [1/8] → Beetrack OK. Actualizando estado en Dynamics...
10:05:04 | INFO    | [a3f7c1d2] OV-905123 [1/8] → Completada
10:05:05 | INFO    | [a3f7c1d2] Envío finalizado: 8/8 guías completadas.
10:05:05 | INFO    | [a3f7c1d2] =======================================================
10:05:05 | INFO    | [a3f7c1d2] FIN DE CICLO
```

Los archivos de log se guardan en `guias_beetrack/logs/`.

---

## Manejo de errores

### Errores de red (timeout, conexión caída)
`servicio_dynamics.py` reintenta automáticamente hasta 3 veces con espera exponencial:
- Intento 1 falla → espera 1s → intento 2
- Intento 2 falla → espera 2s → intento 3
- Intento 3 falla → propaga el error

### Error al enviar a Beetrack
La guía falla silenciosamente para ese ciclo. Las demás guías no se ven afectadas. Se registra en el log de errores.

### Error al actualizar Dynamics (después de enviar a Beetrack)
Este es el caso crítico: la guía **ya llegó a Beetrack** pero Dynamics aún la ve como pendiente.

1. Se guarda el `SalesId` en `pending_dynamics_updates.json`.
2. En el siguiente ciclo, cuando esa guía aparece de nuevo en la consulta (porque Dynamics aún la ve como Pending), el sistema **detecta que ya fue enviada** y **omite el POST a Beetrack**.
3. Solo reintenta el PATCH a Dynamics.
4. Si el PATCH tiene éxito, se elimina del archivo.
5. Si sigue fallando, permanece en el archivo para el ciclo siguiente.

```
pending_dynamics_updates.json
{
  "OV-905124": {
    "data_area_id": "CND",
    "rec_id_1": "5637145328",
    "estado": "{\"DocState\": \"Recived\"}"
  }
}
```

### Errores por guía vs errores globales
- Errores **dentro del envío** (Beetrack o Dynamics por guía): aislados, no afectan al resto.
- Errores **antes del envío** (fallo en consulta a Dynamics, token inválido): detienen el ciclo completo y se registran en el log de errores.
