desarrollo instalado en el cliente globe italia
This commit is contained in:
commit
d7c4d532b5
17 files changed
+1946
No files matched your search
+265
@@ -0,0 +1,265 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,133 @@
|
||||
# Especificación de Logging — ComercioNet
|
||||
|
||||
## Objetivo
|
||||
|
||||
Implementar un sistema de logging jerárquico con formato visual de árbol (`├──`, `└──`, `│`) que permita entender rápidamente el flujo de procesamiento de cada archivo y sus sub-operaciones (SAP B1, SQL, etc.).
|
||||
|
||||
## API
|
||||
|
||||
El módulo `logger_manager.py` expone una función única:
|
||||
|
||||
```python
|
||||
from logger_manager import get_logger
|
||||
|
||||
log = get_logger() # devuelve una instancia de TaskLogger
|
||||
```
|
||||
|
||||
### Métodos de `TaskLogger`
|
||||
|
||||
| Método | Descripción | Prefijo visual |
|
||||
|---|---|---|
|
||||
| `info(msg, _last=False)` | Mensaje informativo | `├──` o `└──` |
|
||||
| `warning(msg, _last=False)` | Advertencia | `├──` o `└──` |
|
||||
| `error(msg, _last=False)` | Error | `├──` o `└──` |
|
||||
| `task(name, emoji="")` | Context manager — abre un bloque anidado (push depth) | `├── + emoji + name` |
|
||||
| `last_task(name, emoji="")` | Context manager — último bloque del padre (push depth) | `└── + emoji + name` |
|
||||
|
||||
### Parámetro `_last`
|
||||
|
||||
En `info()`, `warning()`, `error()`, el parámetro `_last=True` cambia el prefijo de `├──` a `└──`, indicando que es el último mensaje en ese nivel.
|
||||
|
||||
```python
|
||||
log.info("Primer paso") # ├── Primer paso
|
||||
log.info("Paso intermedio") # ├── Paso intermedio
|
||||
log.info("Último paso", _last=True) # └── Último paso
|
||||
```
|
||||
|
||||
## Estructura jerárquica con `task()` / `last_task()`
|
||||
|
||||
```python
|
||||
with log.task("📦 Archivo X.xml"):
|
||||
log.info("XML parseado")
|
||||
with log.task("🔐 SAP B1"):
|
||||
log.info("Sesión iniciada")
|
||||
log.info("Catálogo OK")
|
||||
log.last_task("📁 Movido a processed")
|
||||
```
|
||||
|
||||
### Salida generada
|
||||
|
||||
```
|
||||
2026-07-31 10:15:03 ─── 📦 Archivo X.xml
|
||||
├── XML parseado
|
||||
├── 🔐 SAP B1
|
||||
│ ├── Sesión iniciada
|
||||
│ └── Catálogo OK
|
||||
└── 📁 Movido a processed
|
||||
```
|
||||
|
||||
## Formato de cada línea
|
||||
|
||||
```
|
||||
{timestamp} ─── {prefijo_árbol}{mensaje}
|
||||
```
|
||||
|
||||
Ejemplo:
|
||||
```
|
||||
2026-07-31 10:15:03 ─── │ ├── XML parseado
|
||||
```
|
||||
|
||||
- `timestamp`: `YYYY-MM-DD HH:mm:ss` (precisión de segundos)
|
||||
- `───`: separador fijo
|
||||
- `prefijo_árbol`: caracteres de indentación y árbol según profundidad
|
||||
- `mensaje`: texto del log
|
||||
|
||||
## Formato en archivo de log
|
||||
|
||||
El archivo `logs/comercio-net.log` usa el mismo formato con caracteres de árbol. No se incluyen niveles (INFO, WARNING, ERROR) en el texto — la diferenciación visual se logra con emojis y el contexto jerárquico.
|
||||
|
||||
## Emojis por etapa
|
||||
|
||||
| Etapa | Emoji |
|
||||
|---|---|
|
||||
| Procesar archivo XML | 📦 |
|
||||
| Parseo XML / datos extraídos | 📄 |
|
||||
| Guardar JSON / HTML | 💾 |
|
||||
| SAP B1 (sesión, catálogo, draft) | 🔐 |
|
||||
| Consultas HTTP (GET / POST) | 🔄 |
|
||||
| Éxito / Draft creado | ✅ |
|
||||
| Advertencia (precio difiere) | ⚠️ |
|
||||
| Error | ❌ |
|
||||
| Archivo movido | 📁 |
|
||||
| Inicio / fin de servicio | 🚀 / 🛑 |
|
||||
|
||||
## Configuración vía entorno
|
||||
|
||||
Variable `LOG_LEVEL` en `.env` (valores: `DEBUG`, `INFO`, `WARNING`, `ERROR`). Por defecto `INFO`.
|
||||
|
||||
```
|
||||
LOG_LEVEL=DEBUG
|
||||
```
|
||||
|
||||
## Thread-safety
|
||||
|
||||
Se usa `contextvars` para mantener el stack de profundidad por cada hilo / contexto asíncrono, permitiendo ejecución concurrente sin mezclar indentaciones.
|
||||
|
||||
## Archivos modificados
|
||||
|
||||
| Archivo | Cambio |
|
||||
|---|---|
|
||||
| `logger_manager.py` | Nuevas clases `TaskLogger`, `TreeFormatter`, `_TaskContext`. `get_logger()` devuelve `TaskLogger`. |
|
||||
| `service.py` | Cambiar `logging.getLogger()` por `get_logger()`. Envolver `process_file()` y bloques SAP con `task()`. |
|
||||
| `sapb1_manager.py` | Reemplazar `print()` por `log.info()/warning()/error()`. |
|
||||
| `sql_manager.py` | Reemplazar `print()` por `log.error()`. |
|
||||
| `main.py` | Ajustes menores para usar `task()` en el ciclo principal. |
|
||||
| (nuevo) `.gitignore` | Excluir `logs/`, `__pycache__/`, `*.pyc`, `.env`, `.DS_Store`. |
|
||||
|
||||
## Ejemplo completo de salida
|
||||
|
||||
```
|
||||
2026-07-31 10:15:03 ─── 🚀 Servicio ComercioNet - Procesador OC
|
||||
2026-07-31 10:15:03 ─── 📦 OC98765.xml
|
||||
├── 📄 XML parseado (orden E001-2439)
|
||||
├── 💾 JSON guardado en data/output/OC98765.json
|
||||
├── 💾 HTML generado
|
||||
├── 🔐 SAP B1
|
||||
│ ├── Sesión iniciada (B1SESSION: a634d0ec...)
|
||||
│ ├── 🔄 [GET] CardCode por GLN
|
||||
│ ├── Catálogo: 1520 items
|
||||
│ ├── Precios: 980 registros
|
||||
│ ├── ✅ Draft creado → DocEntry 2022
|
||||
│ └── 💾 Payload guardado
|
||||
└── 📁 XML movido a processed/
|
||||
```
|
||||
@@ -0,0 +1,189 @@
|
||||
# Especificación de Parseo — Órdenes de Compra CommerceNet Chile
|
||||
|
||||
## Formato del archivo
|
||||
|
||||
- XML con esquema EAN.UCC (GS1) basado en `As2Envelope.xsd`
|
||||
- Codificación: ISO-8859-1
|
||||
- Namespace: `http://www.uc-council.org/smp/schemas/eanucc`
|
||||
|
||||
## Estructura del XML
|
||||
|
||||
```
|
||||
envelope
|
||||
├── messageHeader
|
||||
│ ├── creationDate
|
||||
│ ├── userId
|
||||
│ ├── messageIdentifier
|
||||
│ ├── to/gln
|
||||
│ ├── from/gln
|
||||
│ └── representingParty/gln
|
||||
└── body
|
||||
└── transaction
|
||||
└── command
|
||||
└── documentCommand
|
||||
├── documentCommandHeader (@type="ADD")
|
||||
└── documentCommandOperand
|
||||
└── order
|
||||
├── @creationDate
|
||||
├── @documentStatus
|
||||
├── paymentTerms
|
||||
│ ├── @type
|
||||
│ ├── @eventType
|
||||
│ └── netPayment/timePeriodDue (@type, value)
|
||||
├── movementDate
|
||||
├── movementDateType
|
||||
├── typedEntityIdentification (@entityType="ORDER")
|
||||
│ └── entityIdentification
|
||||
│ ├── uniqueCreatorIdentification ← N° OC
|
||||
│ └── contentOwner/gln
|
||||
├── specialHandling
|
||||
├── salesDepartamentNumber
|
||||
├── orderType
|
||||
├── promotionDealNumber
|
||||
├── internalVendorNumber
|
||||
├── buyer/gln
|
||||
├── seller/gln
|
||||
├── shipParty (@identificationType="SHIP_TO")/gln
|
||||
├── shipFrom/name
|
||||
├── currency
|
||||
├── mainCarrierTransport
|
||||
│ ├── mode
|
||||
│ ├── means
|
||||
│ └── carrierName
|
||||
└── lineItem (@number)
|
||||
├── netPrice/amount (@currencyISOcode)
|
||||
├── PriceType
|
||||
├── requestedQuantity (@UnitType)
|
||||
├── containedUnits (@UnitType)
|
||||
├── allowanceCharge (0..N)
|
||||
│ ├── @allowanceOrChargeType ("ALLOWANCE" | "CHARGE")
|
||||
│ └── monetaryAmountOrPercentage
|
||||
│ ├── percentage
|
||||
│ └── amount (@currencyISOcode)
|
||||
├── itemIdentification
|
||||
│ ├── gtin
|
||||
│ ├── buyerItemNumber (opcional)
|
||||
│ └── vendorItemNumber (opcional)
|
||||
├── itemColor (opcional)
|
||||
├── itemSize (opcional)
|
||||
├── itemDescription (@language)/text
|
||||
└── totalAmount/amount (@currencyISOcode)
|
||||
```
|
||||
|
||||
## Reglas generales
|
||||
|
||||
- Todos los valores provienen **exclusivamente** del contenido XML
|
||||
- El nombre del archivo es solo referencial
|
||||
- No inventar campos que no existan en el XML
|
||||
- No recortar ni modificar valores
|
||||
|
||||
## Mapeo Cabecera
|
||||
|
||||
| Columna | Ruta XPath / Campo | Notas |
|
||||
|---|---|---|
|
||||
| Emisor | `envelope/messageHeader/from/gln` | |
|
||||
| Receptor | `envelope/messageHeader/to/gln` | |
|
||||
| N° Orden de Compra | `envelope/body/transaction/command/documentCommand/documentCommandOperand/order/typedEntityIdentification[@entityType='ORDER']/entityIdentification/uniqueCreatorIdentification` | Valor completo, sin recortes |
|
||||
| Fecha generación mensaje | `envelope/body/transaction/command/documentCommand/documentCommandOperand/order/@creationDate` | Atributo creationDate del elemento order |
|
||||
| Fecha de embarque | `envelope/body/transaction/command/documentCommand/documentCommandOperand/order/movementDate` | movementDate con movementDateType REQUESTED_DELIVERY |
|
||||
| Fecha de cancelación | — | No existe en el XML, se deja vacío |
|
||||
| Condiciones de pago | `paymentTerms/netPayment/timePeriodDue` | Formato: "NET {días} días desde fecha de factura" |
|
||||
| Lugar de entrega | `shipParty[@identificationType='SHIP_TO']/gln` | |
|
||||
| Dpto. ventas | `salesDepartamentNumber` | |
|
||||
| Tipo OC | `orderType` | |
|
||||
| Promoción | `promotionDealNumber` | |
|
||||
| N° Proveedor | `internalVendorNumber` | |
|
||||
| Observaciones | `specialHandling` | |
|
||||
|
||||
## Mapeo Detalle (por línea)
|
||||
|
||||
Orden de columnas:
|
||||
|
||||
| # | Columna | Ruta XPath | Notas |
|
||||
|---|---|---|---|
|
||||
| 1 | Línea | `lineItem/@number` | |
|
||||
| 2 | Cód. UPC (GTIN) | `lineItem/itemIdentification/gtin` | |
|
||||
| 3 | Item (buyer) | `lineItem/itemIdentification/buyerItemNumber` | |
|
||||
| 4 | Cód. Prov. | `lineItem/itemIdentification/vendorItemNumber` | |
|
||||
| 5 | Color/Talla/UM | `itemColor` + `itemSize` | Concatenar como "Color / Talla" según lo que traiga cada producto |
|
||||
| 6 | Descripción | `itemDescription[@language='SP']/text` | Primer elemento |
|
||||
| 7 | Precio Unit. | `netPrice/amount` | Moneda del atributo @currencyISOcode |
|
||||
| 8 | Empaques | `requestedQuantity` | Cantidad solicitada en unidades de empaque (CSC = cajas) |
|
||||
| 9 | Unid/Empaque | `containedUnits` | Unidades contenidas por empaque (SIN) |
|
||||
| 10 | Cantidad (unid) | — | **Derivado**: Empaques × Unid/Empaque |
|
||||
| 11 | Importe | `totalAmount/amount` | Moneda del atributo @currencyISOcode |
|
||||
|
||||
### Notas sobre UM
|
||||
|
||||
- `requestedQuantity/@UnitType` = CSC o CS (Case) → Empaques
|
||||
- `containedUnits/@UnitType` = SIN (Unit) → Unid/Empaque
|
||||
- Los campos `itemColor` e `itemSize` son **opcionales** según el emisor. Si no existen, la columna "Color/Talla/UM" se deja vacía.
|
||||
|
||||
### Algoritmo de Descuentos (allowanceCharge)
|
||||
|
||||
Cada línea puede tener 0 o más bloques `allowanceCharge`. Solo se procesan los de tipo `ALLOWANCE` con `percentage > 0`.
|
||||
|
||||
```
|
||||
base_total = netPrice × requestedQuantity
|
||||
|
||||
para cada allowanceCharge con allowanceOrChargeType="ALLOWANCE" y percentage > 0:
|
||||
descuento = round(base_total × percentage / 100) ← round a entero, .5 hacia arriba
|
||||
base_total = base_total - descuento
|
||||
|
||||
totalAmount (calculado) = base_total
|
||||
```
|
||||
|
||||
**Reglas:**
|
||||
- Los descuentos se aplican **secuencialmente** sobre el saldo restante (no todos sobre el original)
|
||||
- El percentage y amount en el XML son por unidad, informativos. El cálculo real se hace sobre el total de la línea
|
||||
- El resultado final debe coincidir con `lineItem/totalAmount/amount`
|
||||
- Si hay múltiples ALLOWANCE, se encadenan: primero desc % sobre el total original, luego otro % sobre el resultado anterior
|
||||
|
||||
**Ejemplos validados contra archivos reales:**
|
||||
|
||||
| Lín | Precio | CS | Base | Allowances | Cálculo | totalAmount XML |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | 32.230 | 2 | 64.460 | 25% → round(16.115)=16.115; 30% → round(48.345×30%)=14.504 | 64.460−16.115−14.504 = **33.841** | 33.841 ✅ |
|
||||
| 2 | 35.939 | 38 | 1.365.682 | 25% → round(341.420,5)=341.421; 15% → round(1.024.261×15%)=153.639 | 1.365.682−341.421−153.639 = **870.622** | 870.622 ✅ |
|
||||
| 9 | 39.097 | 1 | 39.097 | 25% → round(9.774,25)=9.774 | 39.097−9.774 = **29.323** | 29.323 ✅ |
|
||||
| 10 | 39.097 | 2 | 78.194 | 25% → round(19.548,5)=19.549 | 78.194−19.549 = **58.645** | 58.645 ✅ |
|
||||
| 20 | 170.000 | 4 | 680.000 | 25% → 170.000; 20% → round(510.000×20%)=102.000 | 680.000−170.000−102.000 = **408.000** | 408.000 ✅ |
|
||||
|
||||
## Mapeo Totales Finales
|
||||
|
||||
| Columna | Ruta XPath / Derivación | Notas |
|
||||
|---|---|---|
|
||||
| Importe total | Suma de `lineItem/totalAmount/amount` | O también `order/totalAmount` si existiera |
|
||||
| Etapa de transporte | `mainCarrierTransport/mode` | |
|
||||
| Quien transporta | `mainCarrierTransport/carrierName` | |
|
||||
| Divisa de referencia | `currency` | |
|
||||
| Condiciones de entrega | — | No existe en el XML |
|
||||
|
||||
## Diferencias entre emisores
|
||||
|
||||
| Elemento | Walmart (925485K200) | Otros emisores |
|
||||
|---|---|---|
|
||||
| Namespace | Sin prefijo (`xmlns=`) | Con prefijo `eanucc:` |
|
||||
| `requestedQuantity/@UnitType` | CSC | CS |
|
||||
| `itemColor`, `itemSize` | Presente | Ausente |
|
||||
| `buyerItemNumber`, `vendorItemNumber` | Presente | Ausente |
|
||||
| `salesDepartamentNumber`, `orderType` | Presente | Ausente |
|
||||
| `promotionDealNumber` | POS REPLEN | Ausente |
|
||||
| `internalVendorNumber` | Presente | Ausente |
|
||||
| `specialHandling` | NO PRETICKET | Ausente |
|
||||
| `mainCarrierTransport`, `currency` | Presente | Ausente |
|
||||
| `allowanceCharge` | Ausente | Presente (con descuentos reales o en 0) |
|
||||
|
||||
## Diferencia entre tipos de OC
|
||||
|
||||
- `orderType=20`: Tipo estándar
|
||||
- `orderType=33`: Tipo distinto (ej. reposición directa)
|
||||
- Ambos con `promotionDealNumber="POS REPLEN"`
|
||||
|
||||
## Manejo de Namespace
|
||||
|
||||
Los archivos pueden venir con o sin prefijo:
|
||||
- **Con prefijo**: `<eanucc:envelope>`, `<eanucc:order>` — usar el namespace `eanucc:` para las consultas XPath
|
||||
- **Sin prefijo**: `<envelope>`, `<order>` — namespace por default
|
||||
|
||||
Solución recomendada: registrar ambos namespaces y usar `*` o registrar el namespace URI `http://www.uc-council.org/smp/schemas/eanucc` con un alias en el parser.
|
||||
Reference in new issue
Block a user