# 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 `` | 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 ``, 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.