265 lines
11 KiB
Markdown
265 lines
11 KiB
Markdown
# 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.
|