14 KiB
14 KiB
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
totalAmountdel XML
Formatos de salida
JSON intermedio (data/output/*.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_DIRpuede ser ruta absoluta (ej:C:\Comercionet\INen producción) o relativa (data/inputpor defecto).- El payload SAP incluye
AddressExtensionconBillToCityyBillToCountyobtenidos de CRD1.