actualización de documentos *.md
This commit is contained in:
1 parent
69cd3b70d7
commit
b777d57833
3 files changed
+80
-36
No files matched your search
+64
-25
@@ -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 `<order>` | 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 `<order>`, 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.
|
||||
@@ -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
|
||||
|
||||
|
||||
+10
-5
@@ -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:**
|
||||
|
||||
|
||||
Reference in new issue
Block a user