Files
comercio-net/docs/flujo.md
T

266 lines
11 KiB
Markdown
Raw 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–Vie 08:00–18:00 → cada 10 min │
│ Lun–Vie 18:00–23:00 → cada 1 hora │
│ Fuera de horario / finde → 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 /Drafts │
│ └── Diferencias → log + skip Draft │
│ 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 rotativo diario
├── service.py ← Lógica: parseo, descuentos, JSON, HTML, SAP
├── sapb1_manager.py ← Conexión SAP B1 Service Layer (login, Draft)
├── sapb1_mapper.py ← Transforma JSON intermedio → payload Draft SAP
├── sql_manager.py ← Conexión SQL Server (catálogo OSCN, precios ITM1, CardName)
├── email_reporter.py ← Genera HTML para correo con validación de precios
├── template.html ← Plantilla HTML flat (Jinja2)
├── .env.dev ← Credenciales SAP B1 y SQL Server
├── 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)
│
├── logs/ ← Logs rotativos diarios (30 días de respaldo)
│ └── comercio-net.log
│
└── docs/
├── parser_spec.md ← Especificación detallada del parseo XML
└── 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 hábil? (lun–vie)
│ │ └── No → dormir hasta próximo día hábil
│ ├── ¿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()
├── 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_cardcode_by_gln(emisor, lugar) → CardCode
│ │ │ ├── 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_draft(payload)
│ │ │ │ └── Diferencias → log + omitir Draft
│ │ │ └── (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)
├── _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 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 |
| `email_reporter.py` | `generate_email_html()` — genera HTML para correo con tabla de validación |
## 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, el Draft **no** se crea en SAP (pero el payload y el email HTML se generan igual).
## 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 |
## 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.