From b777d578337733af3795cbf91354eb3d089bcbd5 Mon Sep 17 00:00:00 2001 From: George Date: Wed, 5 Aug 2026 07:54:08 -0500 Subject: [PATCH] =?UTF-8?q?actualizaci=C3=B3n=20de=20documentos=20*.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/flujo.md | 89 +++++++++++++++++++++++++++++++------------- docs/logging_spec.md | 12 +++--- docs/parser_spec.md | 15 +++++--- 3 files changed, 80 insertions(+), 36 deletions(-) diff --git a/docs/flujo.md b/docs/flujo.md index e388df2..190d369 100644 --- a/docs/flujo.md +++ b/docs/flujo.md @@ -16,9 +16,9 @@ │ main.py │ │ (servicio NSSM, loop con schedule) │ │ │ - │ Lun–Vie 08:00–18:00 → cada 10 min │ - │ Lun–Vie 18:00–23:00 → cada 1 hora │ - │ Fuera de horario / finde → dormido │ + │ Lun–Dom 08:00–18:00 → cada 10 min │ + │ Lun–Dom 18:00–23:00 → cada 1 hora │ + │ Fuera de horario → dormido │ └────────┬─────────────────────────────────────────┘ │ importa service.py ▼ @@ -34,8 +34,8 @@ │ 7. Generar email HTML → data/html/*_email.html │ │ 8. Construir payload Draft │ │ 9. Validar precios contra SAP (todas las líneas) │ - │ ├── Todo OK → POST /Drafts │ - │ └── Diferencias → log + skip Draft │ + │ ├── Todo OK → POST /Orders │ + │ └── Diferencias → log + envío email │ │ 10. Mover XML a data/processed/ │ └────────┬──────────────────────────────────────────┘ │ @@ -51,29 +51,36 @@ ``` comercio-net/ ├── main.py ← Entry point del servicio NSSM -├── logger_manager.py ← Configuración de logging rotativo diario +├── logger_manager.py ← Configuración de logging jerárquico con formato de árbol ├── service.py ← Lógica: parseo, descuentos, JSON, HTML, SAP -├── sapb1_manager.py ← Conexión SAP B1 Service Layer (login, Draft) +├── sapb1_manager.py ← Conexión SAP B1 Service Layer (login, create_orders) ├── sapb1_mapper.py ← Transforma JSON intermedio → payload Draft SAP -├── sql_manager.py ← Conexión SQL Server (catálogo OSCN, precios ITM1, CardName) +├── sql_manager.py ← Conexión SQL Server (catálogo OSCN, precios ITM1, CardCode) ├── email_reporter.py ← Genera HTML para correo con validación de precios +├── mail_sender.py ← Envío SMTP de correos (texto plano + HTML) ├── template.html ← Plantilla HTML flat (Jinja2) -├── .env.dev ← Credenciales SAP B1 y SQL Server +├── .env ← Credenciales SAP B1, SQL Server y correo +├── .gitignore ← Excluye logs/, __pycache__/, .env, .DS_Store ├── requirements.txt ← Dependencias Python -├── install_service.bat ← Instalación como servicio Windows (NSSM) │ ├── data/ │ ├── input/ ← XML entrantes (el servicio vacía esta carpeta) │ ├── processed/ ← XML ya procesados (backup) │ ├── output/ ← JSON intermedio generado │ ├── html/ ← HTML visual + HTML para correo (*_email.html) -│ └── sap_b1/ ← Payload Draft SAP (*_sap.json, siempre generado) +│ ├── sap_b1/ ← Payload Draft SAP (*_sap.json, siempre generado) +│ └── errors/ ← XML que fallaron (ej: socio de negocios no encontrado) │ ├── logs/ ← Logs rotativos diarios (30 días de respaldo) │ └── comercio-net.log │ +├── test/ +│ ├── b1.json ← Plantilla de payload SAP para pruebas +│ └── sl.http ← Peticiones HTTP para VS Code REST Client +│ └── docs/ ├── parser_spec.md ← Especificación detallada del parseo XML + ├── logging_spec.md ← Especificación del sistema de logging └── flujo.md ← Este documento ``` @@ -85,8 +92,8 @@ comercio-net/ main() ├── signal.signal(SIGINT/SIGTERM) → _running = False ├── Loop infinito while _running: -│ ├── ¿Día hábil? (lun–vie) -│ │ └── No → dormir hasta próximo día hábil +│ ├── ¿Día activo? (Lun–Dom, todos los días) +│ │ └── No → dormir hasta próximo día activo │ ├── ¿Dentro de horario? │ │ ├── 08:00–18:00 → intervalo = 600s (10 min) │ │ ├── 18:00–23:00 → intervalo = 3600s (1 hora) @@ -99,6 +106,7 @@ main() ``` process_all() +├── Crea data/input/ si no existe ├── Leer archivos de data/input/ ├── Por cada archivo: │ ├── process_file(xml_path) @@ -106,7 +114,8 @@ process_all() │ │ ├── write_json(data, output/*.json) │ │ ├── write_html(data, html/*.html) │ │ ├── [SAP habilitado]: -│ │ │ ├── get_cardcode_by_gln(emisor, lugar) → CardCode +│ │ │ ├── get_client_by_gln(emisor, lugar) → {CardCode, City, County} +│ │ │ │ └── Si no encontrado → send_mail() + mover a data/errors/ │ │ │ ├── get_item_catalog(cardcode) → {UPC: ItemCode} │ │ │ ├── get_price_list(cardcode) → ListNum │ │ │ ├── get_prices(ListNum) → {ItemCode: Price} @@ -115,8 +124,8 @@ process_all() │ │ │ ├── to_draft_payload() → payload Draft │ │ │ ├── write_json(payload, sap_b1/*_sap.json) │ │ │ ├── Validar todas las líneas (precio XML vs SAP) -│ │ │ │ ├── Todas OK → create_draft(payload) -│ │ │ │ └── Diferencias → log + omitir Draft +│ │ │ │ ├── Todas OK → create_orders(payload) +│ │ │ │ └── Diferencias → send_validation_email() + omitir envío │ │ │ └── (errores no bloquean, se loguean) │ │ └── move a data/processed/ │ └── Si error → log + continúa con siguiente @@ -125,7 +134,7 @@ process_all() ### 3. Parseo del XML (`service.py.parse_order`) ``` -parse_order(xml_bytes) +parse_order(xml_bytes, filename) ├── _strip_ns(root) ← Elimina namespaces (soporta con/sin prefijo) ├── Extraer cabecera: │ ├── Emisor / Receptor @@ -232,10 +241,28 @@ Generado por `sapb1_mapper.to_draft_payload()`. Se crea **siempre** (incluso si | Módulo | Función | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -| `sql_manager.py` | Conexión SQL Server vía `pymssql`. Queries a CRD1 (CardCode por GLN), OSCN (catálogo por BP), OCRD (lista precios), ITM1 (precios), OCRD (CardName) | -| `sapb1_mapper.py` | `to_draft_payload()` — transforma JSON intermedio + datos resueltos → payload Draft SAP B1 | -| `sapb1_manager.py` | `login()` + `create_draft()` — autenticación Service Layer y POST /Drafts | +| `sql_manager.py` | Conexión SQL Server vía `pymssql`. Queries a CRD1 (CardCode/City/County por GLN), OSCN (catálogo por BP), OCRD (lista precios, CardName), ITM1 (precios) | +| `sapb1_mapper.py` | `to_draft_payload()` — transforma JSON intermedio + datos resueltos → payload Draft SAP B1 (incluye AddressExtension con City/County) | +| `sapb1_manager.py` | `login()` + `create_orders()` — autenticación Service Layer y POST /Orders | | `email_reporter.py` | `generate_email_html()` — genera HTML para correo con tabla de validación | +| `mail_sender.py` | `send_mail()` + `send_validation_email()` — envío SMTP de notificaciones y validación de precios | + +## Funciones SQL (`sql_manager.py`) + +| Función | Retorna | Descripción | +| ---------------------- | --------------- | -------------------------------------------------------- | +| `get_client_by_gln()` | `{CardCode, City, County}` | Busca socio de negocios por GLN en CRD1 (dirección envío) | +| `get_item_catalog()` | `{UPC: ItemCode}` | Catálogo de sustitutos (UPC → código SAP) | +| `get_price_list()` | `int` o `None` | Número de lista de precios del socio de negocios | +| `get_prices()` | `{ItemCode: Price}` | Todos los precios de una lista dada | +| `get_card_name()` | `str` o `None` | Nombre del socio de negocios | + +## Funciones correo (`mail_sender.py`) + +| Función | Descripción | +| ------------------------ | ------------------------------------------------------------------------ | +| `send_mail()` | Envía correo de texto plano (notificación de error, ej: CardCode no encontrado) | +| `send_validation_email()`| Envía correo HTML con tabla de validación de precios y catálogo | ## Validación de precios @@ -246,16 +273,25 @@ Por cada línea del detalle se calcula `precio_xml = importe / cantidad_unid` y - **Precio difiere**: precio XML ≠ precio SAP - **OK**: todo coincide -Si **alguna** línea tiene diferencia, el Draft **no** se crea en SAP (pero el payload y el email HTML se generan igual). +Si **alguna** línea tiene diferencia, se envía correo de validación (pero el payload y el email HTML se generan igual). El envío a SAP (`create_orders`) se omite. + +## Errores y carpetas + +| Escenario | Acción | +| -------------------------------- | --------------------------------------------------- | +| XML malformado o sin `` | Se loguea error, se continúa con siguiente archivo | +| CardCode no encontrado en CRD1 | Se envía `send_mail()`, se mueve XML a `data/errors/` | +| Error de conexión SAP/SQL | Se loguea, no bloquea procesamiento de siguientes archivos | ## Schedule del servicio | Días | Rato | Intervalo | | ------- | ------------- | ---------- | -| Lun–Vie | 08:00 – 18:00 | 10 minutos | -| Lun–Vie | 18:00 – 23:00 | 1 hora | -| Lun–Vie | 23:00 – 08:00 | Dormido | -| Sáb–Dom | Todo el día | Dormido | +| Lun–Dom | 08:00 – 18:00 | 10 minutos | +| Lun–Dom | 18:00 – 23:00 | 1 hora | +| Lun–Dom | 23:00 – 08:00 | Dormido | + +> **Nota**: El servicio está activo **todos los días** de la semana (lunes a domingo). ## Notas @@ -263,3 +299,6 @@ Si **alguna** línea tiene diferencia, el Draft **no** se crea en SAP (pero el p - El archivo se mueve a `processed/` solo después de generar exitosamente JSON, HTML visual, email HTML y payload SAP. - Si un archivo falla (XML mal formado, sin elemento ``, etc.), se loguea el error y se continúa con el siguiente. - Los errores de conexión SAP o SQL Server no bloquean el procesamiento de archivos posteriores. +- Las variables de entorno están en `.env` (no `.env.dev`). +- `INPUT_DIR` puede ser ruta absoluta (ej: `C:\Comercionet\IN` en producción) o relativa (`data/input` por defecto). +- El payload SAP incluye `AddressExtension` con `BillToCity` y `BillToCounty` obtenidos de CRD1. diff --git a/docs/logging_spec.md b/docs/logging_spec.md index 4f7f21c..2f1bed7 100644 --- a/docs/logging_spec.md +++ b/docs/logging_spec.md @@ -107,12 +107,12 @@ Se usa `contextvars` para mantener el stack de profundidad por cada hilo / conte | Archivo | Cambio | |---|---| -| `logger_manager.py` | Nuevas clases `TaskLogger`, `TreeFormatter`, `_TaskContext`. `get_logger()` devuelve `TaskLogger`. | -| `service.py` | Cambiar `logging.getLogger()` por `get_logger()`. Envolver `process_file()` y bloques SAP con `task()`. | -| `sapb1_manager.py` | Reemplazar `print()` por `log.info()/warning()/error()`. | -| `sql_manager.py` | Reemplazar `print()` por `log.error()`. | -| `main.py` | Ajustes menores para usar `task()` en el ciclo principal. | -| (nuevo) `.gitignore` | Excluir `logs/`, `__pycache__/`, `*.pyc`, `.env`, `.DS_Store`. | +| `logger_manager.py` | Clases `TaskLogger`, `TreeFormatter`, `_TaskContext`. `get_logger()` devuelve `TaskLogger`. | +| `service.py` | Usa `get_logger()`. `process_file()` y bloques SAP envueltos con `task()`. | +| `sapb1_manager.py` | Usa `log.info()/warning()/error()` en lugar de `print()`. | +| `sql_manager.py` | Usa `log.error()` en lugar de `print()`. | +| `main.py` | Usa `get_logger()` y `task()` en el ciclo principal. | +| `mail_sender.py` | Usa `log.info()/warning()` para logging de envío de correos. | ## Ejemplo completo de salida diff --git a/docs/parser_spec.md b/docs/parser_spec.md index e5ab293..4900b4c 100644 --- a/docs/parser_spec.md +++ b/docs/parser_spec.md @@ -121,23 +121,28 @@ Orden de columnas: ### Algoritmo de Descuentos (allowanceCharge) -Cada línea puede tener 0 o más bloques `allowanceCharge`. Solo se procesan los de tipo `ALLOWANCE` con `percentage > 0`. +Cada línea puede tener 0 o más bloques `allowanceCharge`. Se procesan según su tipo: ``` base_total = netPrice × requestedQuantity -para cada allowanceCharge con allowanceOrChargeType="ALLOWANCE" y percentage > 0: - descuento = round(base_total × percentage / 100) ← round a entero, .5 hacia arriba +para cada allowanceCharge: + si percentage > 0: + descuento = round(base_total × percentage / 100) ← round a entero, .5 hacia arriba + si no: + descuento = round(amount) ← monto fijo base_total = base_total - descuento totalAmount (calculado) = base_total +precio_efectivo = round(totalAmount / requestedQuantity) ``` **Reglas:** - Los descuentos se aplican **secuencialmente** sobre el saldo restante (no todos sobre el original) -- El percentage y amount en el XML son por unidad, informativos. El cálculo real se hace sobre el total de la línea +- Si `percentage > 0`, se usa el porcentaje sobre el total actual; si no, se usa el `amount` como monto fijo - El resultado final debe coincidir con `lineItem/totalAmount/amount` -- Si hay múltiples ALLOWANCE, se encadenan: primero desc % sobre el total original, luego otro % sobre el resultado anterior +- Si hay múltiples allowance, se encadenan: primero el primero sobre el total original, luego el siguiente sobre el resultado anterior +- `precio_efectivo` se calcula como el total con descuento dividido por la cantidad solicitada **Ejemplos validados contra archivos reales:**