Files

305 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Flujo del Proceso — ComercioNet
## Visión General
```
[Emisor (Walmart, etc.)]
│
│ Envía XML OC vía AS2 / SFTP
▼
┌─────────────────┐
│ data/input/ │ ← Carpeta de llegada
└────────┬────────┘
│
▼
┌──────────────────────────────────────────────────┐
│ main.py │
│ (servicio NSSM, loop con schedule) │
│ │
│ 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
▼
┌─────────────────────────────────────────────────-─┐
│ service.py │
│ │
│ 1. Leer XML de data/input/ │
│ 2. Parsear (strip ns → extraer campos) │
│ 3. Calcular descuentos secuenciales │
│ 4. Generar JSON intermedio → data/output/ │
│ 5. Generar HTML visual → data/html/ │
│ 6. SAP: resolver CardCode, catálogo, precios │
│ 7. Generar email HTML → data/html/*_email.html │
│ 8. Construir payload Draft │
│ 9. Validar precios contra SAP (todas las líneas) │
│ ├── Todo OK → POST /Orders │
│ └── Diferencias → log + envío email │
│ 10. Mover XML a data/processed/ │
└────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────┐
│ data/sap_b1/ │ ← payload Draft (*_sap.json)
│ (siempre se crea) │
└─────────────────────┘
```
## Archivos del sistema
```
comercio-net/
├── main.py ← Entry point del servicio NSSM
├── 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, create_orders)
├── sapb1_mapper.py ← Transforma JSON intermedio → payload Draft SAP
├── 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 ← Credenciales SAP B1, SQL Server y correo
├── .gitignore ← Excluye logs/, __pycache__/, .env, .DS_Store
├── requirements.txt ← Dependencias Python
│
├── 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)
│ └── 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
```
## Flujo detallado del servicio
### 1. Inicio (`main.py`)
```
main()
├── signal.signal(SIGINT/SIGTERM) → _running = False
├── Loop infinito while _running:
│ ├── ¿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)
│ │ └── Fuera → dormir hasta las 08:00
│ ├── Ejecutar process_all()
│ └── Dormir intervalo
```
### 2. Procesamiento (`service.py`)
```
process_all()
├── Crea data/input/ si no existe
├── Leer archivos de data/input/
├── Por cada archivo:
│ ├── process_file(xml_path)
│ │ ├── parse_order(xml_bytes) → dict (JSON intermedio)
│ │ ├── write_json(data, output/*.json)
│ │ ├── write_html(data, html/*.html)
│ │ ├── [SAP habilitado]:
│ │ │ ├── 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}
│ │ │ ├── get_card_name(cardcode) → CardName
│ │ │ ├── generate_email_html() → html/*_email.html
│ │ │ ├── to_draft_payload() → payload Draft
│ │ │ ├── write_json(payload, sap_b1/*_sap.json)
│ │ │ ├── Validar todas las líneas (precio XML vs SAP)
│ │ │ │ ├── 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
```
### 3. Parseo del XML (`service.py.parse_order`)
```
parse_order(xml_bytes, filename)
├── _strip_ns(root) ← Elimina namespaces (soporta con/sin prefijo)
├── Extraer cabecera:
│ ├── Emisor / Receptor
│ ├── N° OC (uniqueCreatorIdentification)
│ ├── Fechas (creationDate, movementDate)
│ ├── Condiciones de pago (paymentTerms)
│ ├── Lugar de entrega (shipParty GLN)
│ ├── Dpto. ventas, Tipo OC, Promoción, Proveedor
│ └── Transporte (mainCarrierTransport)
├── Por cada lineItem:
│ ├── Precio, Cantidad (CS/CSC), Unid/empaque (SIN)
│ ├── GTIN, Item (buyer), Cód. Prov. (vendor)
│ ├── Color / Talla
│ ├── allowanceCharge (0..N)
│ ├── apply_allowances(netPrice, qty, allowances) → total calculado
│ └── precio_efectivo = total_calculado / qty
├── Calcular totales (suma de importes)
└── Devolver dict con metadata, cabecera, detalle, totales
```
### 4. Algoritmo de descuentos (`service.py.apply_allowances`)
```
base_total = netPrice × requestedQuantity
Para cada allowanceCharge con percentage > 0:
descuento = redondear(base_total × % / 100)
base_total = base_total - descuento
totalAmount = base_total
```
- Los descuentos se aplican **secuencialmente** sobre el saldo restante
- El redondeo es a entero (.5 hacia arriba)
- El resultado debe coincidir con `totalAmount` del XML
## Formatos de salida
### JSON intermedio (`data/output/*.json`)
```json
{
"metadata": {
"archivo": "925485K200...xml",
"procesado": "2026-07-21T10:00:00"
},
"cabecera": {
"emisor": "925485K200",
"receptor": "7800AD0CL0001",
"numero_oc": "780790022025012792230",
"fecha_generacion": "2026-07-14T00:00:00",
"fecha_embarque": "2026-07-17",
"condiciones_pago": "NET 60 días desde fecha de factura",
"lugar_entrega": "7807900001686",
"dpto_ventas": "92",
"tipo_oc": "20",
"promocion": "POS REPLEN",
"numero_proveedor": "006205920",
"observaciones": "NO PRETICKET"
},
"detalle": [
{
"linea": "000001",
"cod_upc": "17730976822923",
"item": "000706071",
"cod_prov": "PONT2292",
"color_talla_um": "CAJ / 520GR",
"descripcion": "PURE DE TOMATE CJ",
"precio_original": 8148,
"moneda": "CLP",
"allowances": [],
"precio_efectivo": 8148,
"empaques": 510,
"unidad_empaque": "CSC",
"unid_empaque": 12,
"cantidad_unid": 6120,
"importe": 4155480
}
],
"totales": {
"importe_total": 31590312,
"moneda": "CLP"
}
}
```
### HTML visual (`data/html/*.html`)
Renderiza la misma información en una tabla plana auto-contenida, sin dependencias externas. Usa Jinja2 con la plantilla `template.html`.
### HTML para correo (`data/html/*_email.html`)
Generado por `email_reporter.generate_email_html()`. Incluye:
- **Cabecera**: N° OC, Fecha Entrega, Cliente (CardCode — CardName)
- **Tabla detalle**: cod_upc, empaques, color_talla_um, unid_empaque, cantidad_unid, Importe, precio_unidad, ItemCode, Precio SAP, Texto, descripción
- **Columna Texto**: indica si el ítem tiene catálogo, precio SAP, y si el precio XML difiere del SAP
### Payload SAP Draft (`data/sap_b1/*_sap.json`)
Generado por `sapb1_mapper.to_draft_payload()`. Se crea **siempre** (incluso si hay diferencias de precio). Solo se envía a SAP si todas las líneas pasan la validación de precios.
## Módulos SAP
| Módulo | Función |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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
Por cada línea del detalle se calcula `precio_xml = importe / cantidad_unid` y se compara con `Precio SAP` (desde ITM1):
- **Sin catálogo**: el UPC no tiene Substitute en OSCN
- **Sin precio SAP**: el ItemCode no tiene precio en ITM1
- **Precio difiere**: precio XML ≠ precio SAP
- **OK**: todo coincide
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–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
- El nombre del archivo XML no determina el contenido; todos los datos se extraen del XML.
- 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.