guias-beetrack-Ricardo-Fuentes 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- guias_beetrack_ricardo_fuentes-0.0.1/.env.example +36 -0
- guias_beetrack_ricardo_fuentes-0.0.1/.gitignore +191 -0
- guias_beetrack_ricardo_fuentes-0.0.1/.vscode/settings.json +3 -0
- guias_beetrack_ricardo_fuentes-0.0.1/LICENSE +21 -0
- guias_beetrack_ricardo_fuentes-0.0.1/PKG-INFO +423 -0
- guias_beetrack_ricardo_fuentes-0.0.1/README.md +382 -0
- guias_beetrack_ricardo_fuentes-0.0.1/guias_beetrack/__init__.py +4 -0
- guias_beetrack_ricardo_fuentes-0.0.1/guias_beetrack/__main__.py +5 -0
- guias_beetrack_ricardo_fuentes-0.0.1/guias_beetrack/dynamics/__init__.py +0 -0
- guias_beetrack_ricardo_fuentes-0.0.1/guias_beetrack/dynamics/app_config.py +49 -0
- guias_beetrack_ricardo_fuentes-0.0.1/guias_beetrack/dynamics/clases_guias_dynamics.py +140 -0
- guias_beetrack_ricardo_fuentes-0.0.1/guias_beetrack/dynamics/config_env.py +27 -0
- guias_beetrack_ricardo_fuentes-0.0.1/guias_beetrack/dynamics/guias.py +301 -0
- guias_beetrack_ricardo_fuentes-0.0.1/guias_beetrack/dynamics/guias_dynamics.py +125 -0
- guias_beetrack_ricardo_fuentes-0.0.1/guias_beetrack/dynamics/servicio_dynamics.py +144 -0
- guias_beetrack_ricardo_fuentes-0.0.1/guias_beetrack/dynamics/token_dynamics.py +118 -0
- guias_beetrack_ricardo_fuentes-0.0.1/guias_beetrack/ejemplos.json +32 -0
- guias_beetrack_ricardo_fuentes-0.0.1/guias_beetrack/main.py +121 -0
- guias_beetrack_ricardo_fuentes-0.0.1/guias_beetrack/tests/__init__.py +0 -0
- guias_beetrack_ricardo_fuentes-0.0.1/guias_beetrack/tests/test_token_dynamics.py +77 -0
- guias_beetrack_ricardo_fuentes-0.0.1/guias_beetrack/utils/correlation.py +5 -0
- guias_beetrack_ricardo_fuentes-0.0.1/guias_beetrack/utils/logger.py +99 -0
- guias_beetrack_ricardo_fuentes-0.0.1/pyproject.toml +42 -0
- guias_beetrack_ricardo_fuentes-0.0.1/requirements.txt +14 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Dynamics 365 - Autenticación OAuth2
|
|
2
|
+
DYNAMICS_URL_TOKEN=https://login.microsoftonline.com/<TENANT_ID>/oauth2/token
|
|
3
|
+
DYNAMICS_URL_ACCESO=https://<ENVIRONMENT>.operations.dynamics.com/
|
|
4
|
+
DYNAMICS_ID_CLIENTE=<CLIENT_ID>
|
|
5
|
+
DYNAMICS_CLAVE_CLIENTE=<CLIENT_SECRET>
|
|
6
|
+
DYNAMICS_TIPO_CREDENCIAL=client_credentials
|
|
7
|
+
|
|
8
|
+
# Beetrack / DispatchTrack
|
|
9
|
+
BEETRACK_URL_ACCESO=https://<INSTANCIA>.dispatchtrack.com/api/external/v1/dispatches
|
|
10
|
+
BEETRACK_TOKEN_ACCESO=<API_TOKEN>
|
|
11
|
+
|
|
12
|
+
# SalesIds excluidos del envío a Beetrack (separados por coma)
|
|
13
|
+
BEETRACK_SALES_IDS_EXCLUIDOS=OV-000000,OV-000001
|
|
14
|
+
|
|
15
|
+
# SalesIds excluidos de la consulta en Dynamics (separados por coma)
|
|
16
|
+
DYNAMICS_SALES_IDS_EXCLUIDOS=OV-000000,OV-000001
|
|
17
|
+
|
|
18
|
+
# Bodega de origen (pickup)
|
|
19
|
+
PICKUP_ADDRESS_NAME=Bodega Zona 4
|
|
20
|
+
PICKUP_LATITUDE=14.621669674128709
|
|
21
|
+
PICKUP_LONGITUDE=-90.51804696621112
|
|
22
|
+
|
|
23
|
+
# Ventana de entrega (horas de offset desde DeliveryCustomerDateTime)
|
|
24
|
+
DELIVERY_MIN_OFFSET_HOURS=7
|
|
25
|
+
DELIVERY_MAX_OFFSET_HOURS=6
|
|
26
|
+
|
|
27
|
+
# Parámetros de despacho
|
|
28
|
+
DISPATCH_PRIORITY=1
|
|
29
|
+
DISPATCH_MODE=3
|
|
30
|
+
DISPATCH_PLACE=Servicio Express
|
|
31
|
+
|
|
32
|
+
# Validación de correo del cliente (separados por coma)
|
|
33
|
+
DOMINIOS_EMAIL_VALIDOS=@centrodistribuidor.com,@servir.com.gt
|
|
34
|
+
|
|
35
|
+
# Grupos de ventas válidos para correo (separados por coma)
|
|
36
|
+
GRUPOS_VENTAS_VALIDOS=CVTA-0006,CVTA-0029,CVTA-0034,CVTA-0076,CVTA-0077,CVTA-0088,CVTA-0091,CVTA-0094,CVTA-0095,CVTA-0102,CVTA-0109,CVTA-0106,CVTA-0103,CVTA-0104,CVTA-0107,CVTA-0005
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Created by https://www.toptal.com/developers/gitignore/api/python,venv
|
|
2
|
+
# Edit at https://www.toptal.com/developers/gitignore?templates=python,venv
|
|
3
|
+
|
|
4
|
+
### Python ###
|
|
5
|
+
# Byte-compiled / optimized / DLL files
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
*$py.class
|
|
9
|
+
|
|
10
|
+
# C extensions
|
|
11
|
+
*.so
|
|
12
|
+
|
|
13
|
+
# Distribution / packaging
|
|
14
|
+
.Python
|
|
15
|
+
build/
|
|
16
|
+
develop-eggs/
|
|
17
|
+
dist/
|
|
18
|
+
downloads/
|
|
19
|
+
eggs/
|
|
20
|
+
.eggs/
|
|
21
|
+
lib/
|
|
22
|
+
lib64/
|
|
23
|
+
parts/
|
|
24
|
+
sdist/
|
|
25
|
+
var/
|
|
26
|
+
wheels/
|
|
27
|
+
share/python-wheels/
|
|
28
|
+
*.egg-info/
|
|
29
|
+
.installed.cfg
|
|
30
|
+
*.egg
|
|
31
|
+
MANIFEST
|
|
32
|
+
|
|
33
|
+
# PyInstaller
|
|
34
|
+
# Usually these files are written by a python script from a template
|
|
35
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
36
|
+
*.manifest
|
|
37
|
+
*.spec
|
|
38
|
+
|
|
39
|
+
# Installer logs
|
|
40
|
+
pip-log.txt
|
|
41
|
+
pip-delete-this-directory.txt
|
|
42
|
+
|
|
43
|
+
# Unit test / coverage reports
|
|
44
|
+
htmlcov/
|
|
45
|
+
.tox/
|
|
46
|
+
.nox/
|
|
47
|
+
.coverage
|
|
48
|
+
.coverage.*
|
|
49
|
+
.cache
|
|
50
|
+
nosetests.xml
|
|
51
|
+
coverage.xml
|
|
52
|
+
*.cover
|
|
53
|
+
*.py,cover
|
|
54
|
+
.hypothesis/
|
|
55
|
+
.pytest_cache/
|
|
56
|
+
cover/
|
|
57
|
+
|
|
58
|
+
# Translations
|
|
59
|
+
*.mo
|
|
60
|
+
*.pot
|
|
61
|
+
|
|
62
|
+
# Django stuff:
|
|
63
|
+
*.log
|
|
64
|
+
local_settings.py
|
|
65
|
+
db.sqlite3
|
|
66
|
+
db.sqlite3-journal
|
|
67
|
+
|
|
68
|
+
# Flask stuff:
|
|
69
|
+
instance/
|
|
70
|
+
.webassets-cache
|
|
71
|
+
|
|
72
|
+
# Scrapy stuff:
|
|
73
|
+
.scrapy
|
|
74
|
+
|
|
75
|
+
# Sphinx documentation
|
|
76
|
+
docs/_build/
|
|
77
|
+
|
|
78
|
+
# PyBuilder
|
|
79
|
+
.pybuilder/
|
|
80
|
+
target/
|
|
81
|
+
|
|
82
|
+
# Jupyter Notebook
|
|
83
|
+
.ipynb_checkpoints
|
|
84
|
+
|
|
85
|
+
# IPython
|
|
86
|
+
profile_default/
|
|
87
|
+
ipython_config.py
|
|
88
|
+
|
|
89
|
+
# pyenv
|
|
90
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
91
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
92
|
+
# .python-version
|
|
93
|
+
|
|
94
|
+
# pipenv
|
|
95
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
96
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
97
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
98
|
+
# install all needed dependencies.
|
|
99
|
+
#Pipfile.lock
|
|
100
|
+
|
|
101
|
+
# poetry
|
|
102
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
103
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
104
|
+
# commonly ignored for libraries.
|
|
105
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
106
|
+
#poetry.lock
|
|
107
|
+
|
|
108
|
+
# pdm
|
|
109
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
110
|
+
#pdm.lock
|
|
111
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
112
|
+
# in version control.
|
|
113
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
114
|
+
.pdm.toml
|
|
115
|
+
|
|
116
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
117
|
+
__pypackages__/
|
|
118
|
+
|
|
119
|
+
# Celery stuff
|
|
120
|
+
celerybeat-schedule
|
|
121
|
+
celerybeat.pid
|
|
122
|
+
|
|
123
|
+
# SageMath parsed files
|
|
124
|
+
*.sage.py
|
|
125
|
+
|
|
126
|
+
# Environments
|
|
127
|
+
.env
|
|
128
|
+
.venv
|
|
129
|
+
env/
|
|
130
|
+
venv/
|
|
131
|
+
ENV/
|
|
132
|
+
env.bak/
|
|
133
|
+
venv.bak/
|
|
134
|
+
|
|
135
|
+
# Spyder project settings
|
|
136
|
+
.spyderproject
|
|
137
|
+
.spyproject
|
|
138
|
+
|
|
139
|
+
# Rope project settings
|
|
140
|
+
.ropeproject
|
|
141
|
+
|
|
142
|
+
# mkdocs documentation
|
|
143
|
+
/site
|
|
144
|
+
|
|
145
|
+
# mypy
|
|
146
|
+
.mypy_cache/
|
|
147
|
+
.dmypy.json
|
|
148
|
+
dmypy.json
|
|
149
|
+
|
|
150
|
+
# Pyre type checker
|
|
151
|
+
.pyre/
|
|
152
|
+
|
|
153
|
+
# pytype static type analyzer
|
|
154
|
+
.pytype/
|
|
155
|
+
|
|
156
|
+
# Cython debug symbols
|
|
157
|
+
cython_debug/
|
|
158
|
+
|
|
159
|
+
# PyCharm
|
|
160
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
161
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
162
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
163
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
164
|
+
#.idea/
|
|
165
|
+
|
|
166
|
+
### Python Patch ###
|
|
167
|
+
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
|
168
|
+
poetry.toml
|
|
169
|
+
|
|
170
|
+
# ruff
|
|
171
|
+
.ruff_cache/
|
|
172
|
+
|
|
173
|
+
# LSP config files
|
|
174
|
+
pyrightconfig.json
|
|
175
|
+
|
|
176
|
+
### venv ###
|
|
177
|
+
# Virtualenv
|
|
178
|
+
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
|
|
179
|
+
[Bb]in
|
|
180
|
+
[Ii]nclude
|
|
181
|
+
[Ll]ib
|
|
182
|
+
[Ll]ib64
|
|
183
|
+
[Ll]ocal
|
|
184
|
+
[Ss]cripts
|
|
185
|
+
pyvenv.cfg
|
|
186
|
+
pip-selfcheck.json
|
|
187
|
+
|
|
188
|
+
# End of https://www.toptal.com/developers/gitignore/api/python,venv
|
|
189
|
+
|
|
190
|
+
tareas_auto
|
|
191
|
+
logs
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ricardo Fuentes
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: guias_beetrack_Ricardo_Fuentes
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Sincronizacion Dynamics 365 -> Beetrack
|
|
5
|
+
Project-URL: Homepage, https://github.com/pypa/sampleproject
|
|
6
|
+
Project-URL: Issues, https://github.com/pypa/sampleproject/issues
|
|
7
|
+
Author-email: Ricardo Fuentes <ricardof_EXT@centrodistribuidor.com>
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 Ricardo Fuentes
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
31
|
+
Classifier: Operating System :: OS Independent
|
|
32
|
+
Classifier: Programming Language :: Python :: 3
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
36
|
+
Requires-Python: >=3.9
|
|
37
|
+
Requires-Dist: apscheduler>=3.11.2
|
|
38
|
+
Requires-Dist: httpx>=0.28.1
|
|
39
|
+
Requires-Dist: python-dotenv>=1.2.1
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# Sincronización Dynamics 365 → Beetrack
|
|
43
|
+
|
|
44
|
+
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.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Índice
|
|
49
|
+
|
|
50
|
+
1. [¿Qué hace el sistema?](#qué-hace-el-sistema)
|
|
51
|
+
2. [Estructura del proyecto](#estructura-del-proyecto)
|
|
52
|
+
3. [Flujo de ejecución paso a paso](#flujo-de-ejecución-paso-a-paso)
|
|
53
|
+
4. [Módulos](#módulos)
|
|
54
|
+
5. [Configuración (.env)](#configuración-env)
|
|
55
|
+
6. [Instalación y ejecución](#instalación-y-ejecución)
|
|
56
|
+
7. [Logs](#logs)
|
|
57
|
+
8. [Manejo de errores](#manejo-de-errores)
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## ¿Qué hace el sistema?
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
Dynamics 365 ──────────────────────────────────► Beetrack
|
|
65
|
+
(pedidos con DocState = "Pending") (guías de despacho)
|
|
66
|
+
│ │
|
|
67
|
+
└──── al confirmar envío ────────────────────────── ┘
|
|
68
|
+
DocState → "Recived"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
El scheduler corre cada **5 minutos**. Si no hay pedidos pendientes, termina silenciosamente. Si los hay, ejecuta el pipeline completo.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Estructura del proyecto
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
tareas_atomaticas/
|
|
79
|
+
│
|
|
80
|
+
├── .env # Variables de entorno (credenciales, configuración)
|
|
81
|
+
├── .env.example # Plantilla de variables (sin valores reales)
|
|
82
|
+
├── requirements.txt # Dependencias Python
|
|
83
|
+
├── pending_dynamics_updates.json # Guías enviadas a Beetrack pero Dynamics no confirmó (auto-generado)
|
|
84
|
+
│
|
|
85
|
+
└── guias_beetrack/
|
|
86
|
+
│
|
|
87
|
+
├── main.py # Punto de entrada, scheduler APScheduler
|
|
88
|
+
│
|
|
89
|
+
├── dynamics/ # Lógica de negocio e integración
|
|
90
|
+
│ ├── app_config.py # Centraliza todas las constantes configurables
|
|
91
|
+
│ ├── config_env.py # Lectura de variables de entorno (.env)
|
|
92
|
+
│ ├── token_dynamics.py # Gestión del token OAuth2 de Dynamics (con caché)
|
|
93
|
+
│ ├── clases_guias_dynamics.py # Modelos de datos (dataclasses)
|
|
94
|
+
│ ├── guias_dynamics.py # Consultas OData a Dynamics 365
|
|
95
|
+
│ ├── guias.py # Orquestación: filtros, armado y envío de guías
|
|
96
|
+
│ └── servicio_dynamics.py # Capa HTTP (GET/PATCH Dynamics, POST Beetrack) con retry
|
|
97
|
+
│
|
|
98
|
+
├── utils/
|
|
99
|
+
│ ├── logger.py # Logger con salida a consola y archivos rotativos
|
|
100
|
+
│ └── correlation.py # ID de correlación por ciclo (ContextVar)
|
|
101
|
+
│
|
|
102
|
+
└── tests/
|
|
103
|
+
└── test_token_dynamics.py
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Flujo de ejecución paso a paso
|
|
109
|
+
|
|
110
|
+
Cada 5 minutos `main.py` ejecuta el siguiente pipeline:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
114
|
+
│ INICIO DE CICLO (run_id: a3f7c1d2) │
|
|
115
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
116
|
+
│ │
|
|
117
|
+
│ 0. Revisar pendientes │
|
|
118
|
+
│ └─ ¿Hay guías en pending_dynamics_updates.json? │
|
|
119
|
+
│ → Sí: avisar en log (se procesarán en el paso 5) │
|
|
120
|
+
│ → No: continuar │
|
|
121
|
+
│ │
|
|
122
|
+
│ 1. Consultar pedidos [Dynamics: CNDBeeTrackSalesTablesEntity] │
|
|
123
|
+
│ └─ Filtros: DocState=Pending, fecha=hoy, SalesStatus=Invoiced │
|
|
124
|
+
│ IdRoute≠CARGO EXPRESO, grupos IG6/CD6/BC/CAB/CDI │
|
|
125
|
+
│ │
|
|
126
|
+
│ 2. Datos primarios (3 consultas en paralelo) │
|
|
127
|
+
│ ├─ Facturas [CUSTINVOICEJOURCNDsEntity] → por SalesId │
|
|
128
|
+
│ ├─ Clientes [CustomersV3] → por InvoiceAccount │
|
|
129
|
+
│ └─ Productos [CNDBeeTrackSalesLinesEntity] → por SalesId │
|
|
130
|
+
│ │
|
|
131
|
+
│ 3. Datos secundarios (3 consultas en paralelo) │
|
|
132
|
+
│ ├─ Ubicaciones [CDSPostalAddressHistoryV2] → lat/lng │
|
|
133
|
+
│ ├─ Direcciones [LogisticsPostalAddressBiEntities] → dirección │
|
|
134
|
+
│ └─ Dimensiones [WHSPHYSDIMUOMCNDsEntity] → peso/alto/ancho │
|
|
135
|
+
│ │
|
|
136
|
+
│ 4. Armar guías │
|
|
137
|
+
│ └─ Combina todos los datos en objetos GuiaResponse │
|
|
138
|
+
│ │
|
|
139
|
+
│ 5. Enviar guías a Beetrack (máx. 10 concurrentes) │
|
|
140
|
+
│ Por cada guía: │
|
|
141
|
+
│ ┌─ ¿Está en pendientes? │
|
|
142
|
+
│ │ → Sí: saltar POST, solo reintentar PATCH a Dynamics │
|
|
143
|
+
│ │ → No: POST a Beetrack → si OK: PATCH a Dynamics │
|
|
144
|
+
│ └─ Si PATCH falla: guardar en pending_dynamics_updates.json │
|
|
145
|
+
│ │
|
|
146
|
+
│ FIN DE CICLO │
|
|
147
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Módulos
|
|
153
|
+
|
|
154
|
+
### `main.py`
|
|
155
|
+
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.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
### `dynamics/app_config.py`
|
|
160
|
+
Centraliza **todas las constantes configurables** del negocio. Los valores se leen del `.env` con defaults. Nunca se hardcodean en la lógica.
|
|
161
|
+
|
|
162
|
+
| Constante | Default | Descripción |
|
|
163
|
+
|-----------|---------|-------------|
|
|
164
|
+
| `BEETRACK_SALES_IDS_EXCLUIDOS` | `OV-904710,OV-904736` | Guías que nunca se envían a Beetrack |
|
|
165
|
+
| `DYNAMICS_SALES_IDS_EXCLUIDOS` | `OV-912698,OV-912941` | Pedidos excluidos de la consulta OData |
|
|
166
|
+
| `PICKUP_ADDRESS_NAME` | `Bodega Zona 4` | Nombre de la bodega de origen |
|
|
167
|
+
| `PICKUP_LATITUDE/LONGITUDE` | `14.62…, -90.51…` | Coordenadas de la bodega |
|
|
168
|
+
| `DELIVERY_MIN_OFFSET_HOURS` | `7` | Horas antes del `DeliveryCustomerDateTime` para ventana mínima |
|
|
169
|
+
| `DELIVERY_MAX_OFFSET_HOURS` | `6` | Horas antes del `DeliveryCustomerDateTime` para ventana máxima |
|
|
170
|
+
| `DISPATCH_PRIORITY` | `1` | Prioridad de despacho en Beetrack |
|
|
171
|
+
| `DISPATCH_MODE` | `3` | Modo de despacho en Beetrack |
|
|
172
|
+
| `DISPATCH_PLACE` | `Servicio Express` | Nombre del servicio en Beetrack |
|
|
173
|
+
| `DOMINIOS_EMAIL_VALIDOS` | `@centrodistribuidor.com,…` | Dominios que habilitan envío de correo al cliente |
|
|
174
|
+
| `GRUPOS_VENTAS_VALIDOS` | `CVTA-0006,…` | Grupos de venta que habilitan correo |
|
|
175
|
+
| `BEETRACK_MAX_CONCURRENT` | `10` | Máximo de guías enviándose en paralelo |
|
|
176
|
+
| `PENDING_DYNAMICS_UPDATES_FILE` | `pending_dynamics_updates.json` | Archivo de guías pendientes de confirmación |
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
### `dynamics/token_dynamics.py`
|
|
181
|
+
Gestiona el token **OAuth2** para Dynamics 365.
|
|
182
|
+
|
|
183
|
+
- El token se guarda en memoria con su tiempo de expiración.
|
|
184
|
+
- Cada consulta llama a `validacion_token()`: si el token es válido lo reutiliza, si expiró solicita uno nuevo.
|
|
185
|
+
- Esto evita hacer una llamada de autenticación en cada request HTTP.
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
validacion_token()
|
|
189
|
+
└─ ¿Token en memoria vigente?
|
|
190
|
+
→ Sí: devuelve el token cacheado
|
|
191
|
+
→ No: POST a Azure AD → nuevo token → guardar en memoria → devolver
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
### `dynamics/guias_dynamics.py`
|
|
197
|
+
Consultas OData a Dynamics 365. Cada método es estático y async.
|
|
198
|
+
|
|
199
|
+
| Método | Entidad Dynamics | Para qué |
|
|
200
|
+
|--------|-----------------|----------|
|
|
201
|
+
| `obtener_pedidos` | `CNDBeeTrackSalesTablesEntity` | Pedidos del día con filtros de estado/grupo |
|
|
202
|
+
| `obtener_info_factura` | `CUSTINVOICEJOURCNDsEntity` | InvoiceId y dirección postal por SalesId |
|
|
203
|
+
| `obtener_info_cliente` | `CustomersV3` | Teléfono, email, ubicación por cuenta |
|
|
204
|
+
| `obtener_info_location_id` | `CDSPostalAddressHistoryV2` | Coordenadas GPS por LocationId |
|
|
205
|
+
| `obtener_info_direccion` | `LogisticsPostalAddressBiEntities` | Dirección en texto por SourceKey |
|
|
206
|
+
| `obtener_info_productos` | `CNDBeeTrackSalesLinesEntity` | Líneas de producto por SalesId |
|
|
207
|
+
| `obtener_info_dimensiones` | `WHSPHYSDIMUOMCNDsEntity` | Alto, peso, ancho, profundidad por ItemId |
|
|
208
|
+
| `actualizar_estado_guia` | `SALESTABLECNDsEntity` | PATCH: cambia DocState a "Recived" |
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
### `dynamics/guias.py`
|
|
213
|
+
Módulo principal de orquestación. Tiene tres clases y una función:
|
|
214
|
+
|
|
215
|
+
**`FiltrosGuiasBeetrack`** — Construye los strings de filtro OData a partir de los datos recuperados. El helper interno `_armar_filtro()` evita repetición de código.
|
|
216
|
+
|
|
217
|
+
**`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.
|
|
218
|
+
|
|
219
|
+
**`ArmarGuiasBeetrack`** — Combina todos los datos en objetos `GuiaResponse` y los envía a Beetrack con control de concurrencia y seguridad transaccional.
|
|
220
|
+
|
|
221
|
+
**`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.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
### `dynamics/servicio_dynamics.py`
|
|
226
|
+
Capa HTTP pura. Tres funciones async:
|
|
227
|
+
|
|
228
|
+
- `get_consultar_dynamics()` — GET a OData con token Bearer. Elimina metadatos `@odata.etag`.
|
|
229
|
+
- `patch_actualizar_dynamics()` — PATCH para actualizar estado en Dynamics.
|
|
230
|
+
- `post_beetrack()` — POST a la API de Beetrack con X-AUTH-TOKEN.
|
|
231
|
+
|
|
232
|
+
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).
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
### `dynamics/clases_guias_dynamics.py`
|
|
237
|
+
Modelos de datos (Python `dataclasses`):
|
|
238
|
+
|
|
239
|
+
| Clase | Descripción |
|
|
240
|
+
|-------|-------------|
|
|
241
|
+
| `InformacionConsulta` | Respuesta genérica: `consulta: bool`, `mensaje: str`, `data` |
|
|
242
|
+
| `PickupAddress` | Dirección de recogida (bodega de origen) |
|
|
243
|
+
| `Dimension` | Par nombre/valor para dimensiones de producto |
|
|
244
|
+
| `ProductoBeetrack` | Producto con descripción, cantidad, código y dimensiones |
|
|
245
|
+
| `GuiaResponse` | Guía completa lista para enviar a Beetrack |
|
|
246
|
+
|
|
247
|
+
`GuiaResponse` se construye en etapas mediante setters:
|
|
248
|
+
1. `set_info_facturas()` — datos del pedido y ventana de entrega
|
|
249
|
+
2. `set_info_cliente()` — contacto, dirección, validación de correo/grupo
|
|
250
|
+
3. `set_info_localizacion()` — coordenadas GPS
|
|
251
|
+
4. `set_dir_facturas()` — InvoiceId y dirección postal
|
|
252
|
+
5. `set_info_productos()` — lista de productos
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
### `utils/logger.py`
|
|
257
|
+
Logger con salida dual:
|
|
258
|
+
|
|
259
|
+
| Destino | Nivel | Retención | Formato |
|
|
260
|
+
|---------|-------|-----------|---------|
|
|
261
|
+
| **Consola** | INFO y superior | — | `HH:MM:SS \| NIVEL \| [run_id] \| mensaje` |
|
|
262
|
+
| `logs/app_DD-MM-YYYY.log` | INFO y WARNING | 7 días | Formato completo con archivo:línea |
|
|
263
|
+
| `logs/errors_DD-MM-YYYY.log` | ERROR y CRITICAL | 30 días | Formato completo con archivo:línea |
|
|
264
|
+
|
|
265
|
+
Cada línea incluye el `run_id` del ciclo activo, lo que permite filtrar todos los eventos de una ejecución específica.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
### `utils/correlation.py`
|
|
270
|
+
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.
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Configuración (.env)
|
|
275
|
+
|
|
276
|
+
Copiar `.env.example` a `.env` y completar los valores:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
cp .env.example .env
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Variables obligatorias
|
|
283
|
+
|
|
284
|
+
```env
|
|
285
|
+
# Autenticación Dynamics 365 (Azure AD)
|
|
286
|
+
DYNAMICS_URL_TOKEN=https://login.microsoftonline.com/<TENANT_ID>/oauth2/token
|
|
287
|
+
DYNAMICS_URL_ACCESO=https://<ENVIRONMENT>.operations.dynamics.com/
|
|
288
|
+
DYNAMICS_ID_CLIENTE=<CLIENT_ID>
|
|
289
|
+
DYNAMICS_CLAVE_CLIENTE=<CLIENT_SECRET>
|
|
290
|
+
DYNAMICS_TIPO_CREDENCIAL=client_credentials
|
|
291
|
+
|
|
292
|
+
# Beetrack / DispatchTrack
|
|
293
|
+
BEETRACK_URL_ACCESO=https://<INSTANCIA>.dispatchtrack.com/api/external/v1/dispatches
|
|
294
|
+
BEETRACK_TOKEN_ACCESO=<API_TOKEN>
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Variables opcionales (tienen defaults en app_config.py)
|
|
298
|
+
|
|
299
|
+
```env
|
|
300
|
+
BEETRACK_SALES_IDS_EXCLUIDOS=OV-000000,OV-000001
|
|
301
|
+
DYNAMICS_SALES_IDS_EXCLUIDOS=OV-000000,OV-000001
|
|
302
|
+
PICKUP_ADDRESS_NAME=Bodega Zona 4
|
|
303
|
+
PICKUP_LATITUDE=14.621669674128709
|
|
304
|
+
PICKUP_LONGITUDE=-90.51804696621112
|
|
305
|
+
DELIVERY_MIN_OFFSET_HOURS=7
|
|
306
|
+
DELIVERY_MAX_OFFSET_HOURS=6
|
|
307
|
+
DISPATCH_PRIORITY=1
|
|
308
|
+
DISPATCH_MODE=3
|
|
309
|
+
DISPATCH_PLACE=Servicio Express
|
|
310
|
+
DOMINIOS_EMAIL_VALIDOS=@centrodistribuidor.com,@servir.com.gt
|
|
311
|
+
GRUPOS_VENTAS_VALIDOS=CVTA-0006,CVTA-0029,...
|
|
312
|
+
BEETRACK_MAX_CONCURRENT=10
|
|
313
|
+
PENDING_DYNAMICS_UPDATES_FILE=pending_dynamics_updates.json
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Instalación y ejecución
|
|
319
|
+
|
|
320
|
+
### Instalación desde PyPI
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
pip install guias-beetrack-ricardo-fuentes
|
|
324
|
+
guias-beetrack
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### 1. Crear entorno virtual e instalar dependencias
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
python -m venv tareas_auto
|
|
331
|
+
tareas_auto\Scripts\activate # Windows
|
|
332
|
+
pip install -r requirements.txt
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### 2. Configurar variables de entorno
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
copy .env.example .env
|
|
339
|
+
# Editar .env con las credenciales reales
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### 3. Ejecutar
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
# Desde la raiz del proyecto
|
|
346
|
+
python -m guias_beetrack
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
El proceso corre indefinidamente. Para detenerlo: `Ctrl+C`.
|
|
350
|
+
|
|
351
|
+
### Ejecutar solo una vez (sin scheduler)
|
|
352
|
+
|
|
353
|
+
Descomentar en `main.py`:
|
|
354
|
+
```python
|
|
355
|
+
if __name__ == "__main__":
|
|
356
|
+
main()
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Logs
|
|
362
|
+
|
|
363
|
+
Al ejecutarse se verá en consola:
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
10:05:00 | INFO | [- ] =======================================================
|
|
367
|
+
10:05:00 | INFO | [- ] INICIO DE CICLO Dynamics → Beetrack
|
|
368
|
+
10:05:00 | INFO | [- ] =======================================================
|
|
369
|
+
10:05:00 | INFO | [a3f7c1d2] [1/5] Consultando pedidos en Dynamics...
|
|
370
|
+
10:05:01 | INFO | [a3f7c1d2] [1/5] Pedidos encontrados: 8
|
|
371
|
+
10:05:01 | INFO | [a3f7c1d2] [2/5] Obteniendo facturas, clientes y productos (paralelo)...
|
|
372
|
+
10:05:02 | INFO | [a3f7c1d2] [2/5] Facturas: 8 | Clientes: 5 | Productos para 8 pedidos
|
|
373
|
+
10:05:02 | INFO | [a3f7c1d2] [3/5] Obteniendo ubicaciones, direcciones y dimensiones (paralelo)...
|
|
374
|
+
10:05:03 | INFO | [a3f7c1d2] [3/5] Ubicaciones: 5 | Direcciones: 8 | Dimensiones: 12
|
|
375
|
+
10:05:03 | INFO | [a3f7c1d2] [4/5] Armando guías...
|
|
376
|
+
10:05:03 | INFO | [a3f7c1d2] [4/5] 8 guías armadas.
|
|
377
|
+
10:05:03 | INFO | [a3f7c1d2] [5/5] Enviando 8 guías a Beetrack...
|
|
378
|
+
10:05:03 | INFO | [a3f7c1d2] OV-905123 [1/8] → Enviando a Beetrack...
|
|
379
|
+
10:05:04 | INFO | [a3f7c1d2] OV-905123 [1/8] → Beetrack OK. Actualizando estado en Dynamics...
|
|
380
|
+
10:05:04 | INFO | [a3f7c1d2] OV-905123 [1/8] → Completada
|
|
381
|
+
10:05:05 | INFO | [a3f7c1d2] Envío finalizado: 8/8 guías completadas.
|
|
382
|
+
10:05:05 | INFO | [a3f7c1d2] =======================================================
|
|
383
|
+
10:05:05 | INFO | [a3f7c1d2] FIN DE CICLO
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Los archivos de log se guardan en `guias_beetrack/logs/`.
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
## Manejo de errores
|
|
391
|
+
|
|
392
|
+
### Errores de red (timeout, conexión caída)
|
|
393
|
+
`servicio_dynamics.py` reintenta automáticamente hasta 3 veces con espera exponencial:
|
|
394
|
+
- Intento 1 falla → espera 1s → intento 2
|
|
395
|
+
- Intento 2 falla → espera 2s → intento 3
|
|
396
|
+
- Intento 3 falla → propaga el error
|
|
397
|
+
|
|
398
|
+
### Error al enviar a Beetrack
|
|
399
|
+
La guía falla silenciosamente para ese ciclo. Las demás guías no se ven afectadas. Se registra en el log de errores.
|
|
400
|
+
|
|
401
|
+
### Error al actualizar Dynamics (después de enviar a Beetrack)
|
|
402
|
+
Este es el caso crítico: la guía **ya llegó a Beetrack** pero Dynamics aún la ve como pendiente.
|
|
403
|
+
|
|
404
|
+
1. Se guarda el `SalesId` en `pending_dynamics_updates.json`.
|
|
405
|
+
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**.
|
|
406
|
+
3. Solo reintenta el PATCH a Dynamics.
|
|
407
|
+
4. Si el PATCH tiene éxito, se elimina del archivo.
|
|
408
|
+
5. Si sigue fallando, permanece en el archivo para el ciclo siguiente.
|
|
409
|
+
|
|
410
|
+
```
|
|
411
|
+
pending_dynamics_updates.json
|
|
412
|
+
{
|
|
413
|
+
"OV-905124": {
|
|
414
|
+
"data_area_id": "CND",
|
|
415
|
+
"rec_id_1": "5637145328",
|
|
416
|
+
"estado": "{\"DocState\": \"Recived\"}"
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Errores por guía vs errores globales
|
|
422
|
+
- Errores **dentro del envío** (Beetrack o Dynamics por guía): aislados, no afectan al resto.
|
|
423
|
+
- 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.
|