Files

14 KiB
Raw Permalink Blame History

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)

{
  "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.