Metadata-Version: 2.4
Name: py-vucem
Version: 0.1.6
Summary: Cliente en Python para la Ventanilla Única de Comercio Exterior Mexicano (VUCEM)
Project-URL: Homepage, https://github.com/pesatto/py-vucem
Project-URL: Repository, https://github.com/pesatto/py-vucem
Project-URL: Issues, https://github.com/pesatto/py-vucem/issues
Author-email: Fernando Ruiz <fernando.ruiz@pesatto.com>
License: MIT
Keywords: aduanas,comercio exterior,manifestacion de valor,mexico,soap,vucem
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Natural Language :: Spanish
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Requires-Dist: cryptography>=42.0.0
Requires-Dist: lxml>=4.9.0
Requires-Dist: requests>=2.28.0
Requires-Dist: zeep>=4.2.1
Description-Content-Type: text/markdown

# py-vucem

Cliente Python para la **Ventanilla Única de Comercio Exterior Mexicano (VUCEM)**. Permite registrar Manifestaciones de Valor Electrónicas (MVE), digitalizar documentos, consultar pedimentos y generar el acuse de recepción en HTML, todo mediante los servicios SOAP oficiales de VUCEM firmados con e.firma (FIEL).

## Requisitos

- Python 3.9+
- e.firma (FIEL) vigente de la empresa importadora (archivos `.cer` y `.key`)
- Clave de acceso a servicios web de VUCEM (`claveWS`)
- Token de Banxico SIE (opcional, para tipo de cambio automático)

## Instalación

```bash
pip install py-vucem
```

O en modo desarrollo:

```bash
git clone https://github.com/pesatto/py_vucem
cd py_vucem
pip install -e .
```

---

## Configuración inicial

La e.firma puede cargarse de tres formas distintas. Usar base64 es recomendable cuando la FIEL se almacena en una base de datos (Odoo, Django, etc.) en lugar de archivos en disco.

**Desde archivos en disco:**

```python
from py_vucem import VucemClient

client = VucemClient(
    cer_path="ruta/a/fiel.cer",
    key_path="ruta/a/fiel.key",
    password="tu-password-fiel",
    clave_ws="tu-clave-ws-vucem",
    rfc="TU_RFC",
)
```

**Desde base64** (Odoo, Django, cualquier sistema con FIEL en BD):

```python
client = VucemClient(
    cer_b64=company.fiel_cer,       # campo Binary en Odoo u otro sistema
    key_b64=company.fiel_key,
    password=company.fiel_password,
    clave_ws=company.vucem_clave_ws,
    rfc=company.vat,
)
```

**Desde un `FielHandler` ya construido** (caso avanzado):

```python
from py_vucem.utils import FielHandler

fiel = FielHandler.desde_base64(cer_b64, key_b64, password)
client = VucemClient(fiel=fiel, clave_ws="...", rfc="TU_RFC")
```

> **Importante:** VUCEM requiere la **e.firma (FIEL)** de la empresa, no el CSD. Son archivos distintos — el CSD solo sirve para firmar CFDIs.

---

## Catálogos VUCEM

En lugar de escribir claves como strings, importa los catálogos para obtener autocompletado y evitar errores tipográficos:

```python
from py_vucem import FormaPago, Incoterm, MetodoValoracion, TipoFigura, Incrementable, Decrementable
```

Los valores son subclases de `str`, por lo que funcionan directamente como strings en cualquier función — no es necesario llamar a `.value`.

### `FormaPago`
| Valor | Descripción |
|-------|-------------|
| `FormaPago.TE` | Transferencia electrónica |
| `FormaPago.EF` | Efectivo |
| `FormaPago.CH` | Cheque |
| `FormaPago.LC` | Letra de cambio |
| `FormaPago.CC` | Carta de crédito |
| `FormaPago.OT` | Otro (requiere `especifique=` en el método) |

### `Incoterm`
| Valor | Descripción |
|-------|-------------|
| `Incoterm.FOB` | Franco a bordo |
| `Incoterm.CIF` | Coste, seguro y flete |
| `Incoterm.CFR` | Coste y flete |
| `Incoterm.EXW` | En fábrica |
| `Incoterm.FCA` | Franco transportista |
| `Incoterm.DAP` | Entregada en lugar |
| `Incoterm.DDP` | Entregada derechos pagados |
| `Incoterm.CPT` | Transporte pagado hasta |
| `Incoterm.CIP` | Transporte y seguro pagados hasta |
| `Incoterm.DPU` | Entregada y descargada en el lugar acordado |

### `MetodoValoracion`
| Valor | Descripción |
|-------|-------------|
| `MetodoValoracion.VTM` | Valor de transacción (método principal) |
| `MetodoValoracion.VMI` | Mercancías idénticas |
| `MetodoValoracion.VMS` | Mercancías similares |
| `MetodoValoracion.VPU` | Precio unitario de venta |
| `MetodoValoracion.VR`  | Valor reconstruido |
| `MetodoValoracion.A78` | Art. 78 LA (método residual) |

### `TipoFigura`
| Valor | Descripción |
|-------|-------------|
| `TipoFigura.AGE` | Agente aduanal |
| `TipoFigura.AAD` | Agencia aduanal |
| `TipoFigura.REP` | Representante legal |
| `TipoFigura.IMP` | Importador |
| `TipoFigura.OTR` | Otro |

### `Incrementable`
| Valor | Descripción |
|-------|-------------|
| `Incrementable.GS` | Gastos de transporte, seguros y conexos |
| `Incrementable.CG` | Comisiones y gastos de corretaje |
| `Incrementable.CE` | Costo de envases o embalajes |
| `Incrementable.GT` | Gastos de embalaje |
| `Incrementable.MP` | Materiales, piezas y partes incorporados |
| `Incrementable.HM` | Herramientas, matrices y moldes |
| `Incrementable.TI` | Trabajos de ingeniería y diseño |
| `Incrementable.RD` | Regalías y derechos de licencia |
| `Incrementable.VC` | Valor revertido al vendedor |
| `Incrementable.MC` | Materiales consumidos en la producción |

### `Decrementable`
| Valor | Descripción |
|-------|-------------|
| `Decrementable.GR` | Gastos realizados por cuenta del importador |
| `Decrementable.RP` | Gastos posteriores a la importación |
| `Decrementable.GT` | Transporte posterior al punto de internación |
| `Decrementable.RC` | Contribuciones y cuotas compensatorias |
| `Decrementable.PI` | Pagos por dividendos al vendedor |
| `Decrementable.ID` | Descuentos especiales aplicados |

---

## Servicios disponibles

### 1. Manifestación de Valor Electrónica (MVE)

La MVE documenta el valor declarado de las mercancías importadas (Art. 59-A Ley Aduanera).

#### Construir y enviar una MVE

```python
from py_vucem import VucemClient, FormaPago, Incoterm, MetodoValoracion, TipoFigura, Incrementable
from py_vucem.models.mv_builder import ManifestacionValor
from py_vucem.utils.banxico import BanxicoClient

banxico = BanxicoClient("tu-token-banxico")

client = VucemClient(
    cer_path="fiel.cer",
    key_path="fiel.key",
    password="password",
    clave_ws="clave_ws",
    rfc="PME241011C34",
)

# Construir el modelo
mv = ManifestacionValor("PME241011C34", banxico=banxico)

# Personas autorizadas a consultar la MV
mv.agregar_consulta("LWO041215F90", TipoFigura.AGE)   # agente aduanal
mv.agregar_consulta("RUHF981104PY2", TipoFigura.REP)  # representante legal

# eDocuments relacionados (COVEs digitalizados en VUCEM)
mv.agregar_documento("03602601109K2")

# Factura / COVE — el pedimento va a nivel de cada COVE
factura = mv.nueva_factura(
    cove="COVE2680TJDY6",
    incoterm=Incoterm.FOB,
    vinculacion=False,
    metodo=MetodoValoracion.VTM,
)
factura.agregar_pedimento(pedimento="6003095", patente="3977", aduana="160")

# Pagos (TC se consulta automáticamente de Banxico por fecha y moneda)
factura.agregar_pago(fecha="2026-02-03", total=21328.00, forma=FormaPago.TE, moneda="USD")
factura.agregar_pago(fecha="2026-03-11", total=47613.00, forma=FormaPago.TE, moneda="USD")

# Incrementables al valor en aduana
factura.agregar_incrementable(tipo=Incrementable.GS, fecha="2026-03-21",
                               total=1800.00, moneda="USD", cargo_importador=True)

# Enviar a VUCEM
resultado = client.registrar_manifestacion_desde_modelo(mv.to_dict())
print(resultado["numeroOperacion"])  # ej. 530502

# El XML firmado queda disponible en el resultado
from pathlib import Path
Path("mv_firmado.xml").write_text(resultado["_xml"], encoding="utf-8")
```

#### MVE con varios COVEs y pedimentos distintos

Cada `Factura` define su propio pedimento. Una misma MV puede tener COVEs apuntando a pedimentos diferentes, y un COVE puede no tener pedimento (ej. garantías por compensación):

```python
mv = ManifestacionValor("PME241011C34", banxico=banxico)
mv.agregar_documento("0360260175JM7")

# COVE 1 — compensación de garantía (sin precio pagado, sin pedimento)
fa1 = mv.nueva_factura("COVE2685WHIA3", incoterm=Incoterm.FOB,
                        vinculacion=False, metodo=MetodoValoracion.A78)
fa1.agregar_compenso(
    motivo="Garantía — proveedor envía piezas para reparación",
    mercancia="Compresor, bandas, flechas",
    especifique="Reemplazo de piezas por el proveedor",
    forma=FormaPago.OT,
    fecha="2026-07-02",
)
fa1.agregar_incrementable(tipo=Incrementable.GS, total=40.49, moneda="USD", fecha="2026-06-24")

# COVE 2 — compra normal, pedimento propio
fa2 = mv.nueva_factura("COVE2685CKRC6", incoterm=Incoterm.FOB,
                        vinculacion=False, metodo=MetodoValoracion.VTM)
fa2.agregar_pedimento(pedimento="5006726", patente="3977", aduana="160")
fa2.agregar_pago(fecha="2026-02-10", total=15500.00, forma=FormaPago.TE, moneda="USD")
fa2.agregar_pago(fecha="2026-04-01", total=22015.00, forma=FormaPago.TE, moneda="USD")
fa2.agregar_incrementable(tipo=Incrementable.GS, total=6833.63, moneda="USD", fecha="2026-06-24")
```

#### Tipos de pago disponibles

| Método | Descripción |
|--------|-------------|
| `fa.agregar_pago()` | Precio ya pagado al proveedor |
| `fa.agregar_por_pagar()` | Precio pendiente de pago (crédito) |
| `fa.agregar_compenso()` | Pago por compensación (intercambio de bienes/servicios) |
| `fa.agregar_incrementable()` | Fletes, seguros, cargos que incrementan el valor en aduana |
| `fa.agregar_decrementable()` | Descuentos, devoluciones, conceptos que disminuyen el valor |

> `agregar_compenso()` con `forma=FormaPago.OT` requiere el parámetro `especifique=` para describir la forma de pago.

#### Tipos de cambio

Si no configuras `BanxicoClient`, debes proporcionar `tc=` en cada operación con moneda extranjera:

```python
mv = ManifestacionValor("PME241011C34")   # sin banxico

factura.agregar_pago(
    fecha="2026-02-03", total=21328.00,
    forma=FormaPago.TE, moneda="USD",
    tc=20.1234,   # obligatorio sin BanxicoClient
)
```

Con `BanxicoClient` el TC FIX oficial se consulta automáticamente por fecha y moneda. Si proporcionas `tc=` junto con `BanxicoClient`, valida que tu TC no difiera más del 1% del oficial.

```python
from py_vucem.utils.banxico import BanxicoClient

banxico = BanxicoClient("tu-token")
tc = banxico.tc("USD", "2026-02-03")   # Decimal('17.2532')
```

Token gratuito en: `https://www.banxico.org.mx/SieAPIRest/service/v1/token`

#### Obtener el XML

```python
# Antes de enviar — para revisar o guardar
xml_firmado = client.generar_xml_mv(mv.to_dict(), con_firma=True)
Path("mv_firmado.xml").write_text(xml_firmado, encoding="utf-8")

# Validar contra el XSD de VUCEM
errores = client.validar_xml_mv(xml_firmado)
for e in errores:
    print("XSD:", e)

# Al registrar — el XML queda en resultado["_xml"]
resultado = client.registrar_manifestacion_desde_xml(xml_firmado)
print(resultado["numeroOperacion"])   # ej. 530502
Path("mv_enviada.xml").write_text(resultado["_xml"], encoding="utf-8")
```

> Guarda siempre `resultado["_xml"]` junto con el `numeroOperacion` como respaldo de auditoría.

#### Consultar una MVE registrada

```python
# Por número de operación
data = client.consultar_manifestacion(numero_operacion=530502)

# Por número de MV (MNVA...)
data = client.consultar_manifestacion(edocument="MNVA2600GAOE4")

print(data["estatus"])       # "Aceptado"
print(data["eDocument"])     # "MNVA2600GAOE4"
print(data["fechaRegistro"]) # "2026-06-29T..."
```

---

### 2. Acuse de recepción de MVE

VUCEM no genera un acuse PDF automáticamente para las MVEs registradas por API — el documento debe generarse del lado del cliente con los datos del trámite.

Todas las funciones de acuse devuelven el documento como **string base64** del HTML, lo que permite almacenarlo directamente en bases de datos (Odoo `ir.attachment`, Django `FileField`, etc.) o decodificarlo para guardarlo en disco.

#### Pre-acuse (borrador antes de enviar)

Genera el HTML del acuse con los datos del XML **sin conectarse a VUCEM**. Útil para verificar que incoterms, montos, pedimentos y compensos estén correctos antes de registrar.

```python
import base64
from pathlib import Path

xml = client.generar_xml_mv(mv.to_dict(), con_firma=True)

b64 = client.generar_preacuse(xml, nombre_importador="PESATTO MEXICO, SA DE CV")

# Guardar en disco
Path("borrador_mv.html").write_bytes(base64.b64decode(b64))

# En Odoo
attachment.datas = b64
attachment.mimetype = "text/html"
```

Los campos que dependen de VUCEM (eDocument, sellos, cadena original) aparecen como marcadores con la etiqueta **BORRADOR**.

#### Acuse oficial (después de registrar en VUCEM)

Solo necesitas el número de operación. El cliente realiza internamente dos consultas a VUCEM:
1. Por `numero_operacion` → obtiene eDocument + sellos digitales + cadena original
2. Por eDocument → obtiene `datosManifestacionValor` (facturas, pagos, TC, incrementables)

Si el estatus no es `"Aceptado"`, lanza `ValueError`.

```python
import base64
from pathlib import Path

b64 = client.generar_acuse(
    numero_operacion=530502,
    nombre_importador="PESATTO MEXICO, SA DE CV",
)

# Guardar en disco
Path("acuse_mv.html").write_bytes(base64.b64decode(b64))

# En Odoo
attachment.datas = b64
attachment.mimetype = "text/html"
```

#### Obtener PDF

El HTML incluye CSS `@media print` optimizado — el encabezado se repite en cada página y los bloques no se cortan. Para obtener PDF:

1. Abre el `.html` en Chrome o Edge
2. `Ctrl+P` → Destino: **Guardar como PDF**
3. Sin márgenes, orientación vertical, papel Carta

#### Múltiples COVEs

Si la MVE tiene más de un COVE, el acuse genera **una página por COVE**, cada una con su información completa (pagos, incrementables, valor en aduana, sellos digitales).

Cuando un COVE tiene `compensoPago`, el acuse muestra una tabla detallada con fecha, motivo, mercancía y forma de pago — no solo un contador de conceptos.

---

### 3. Digitalización de documentos

Convierte documentos PDF en eDocuments registrados en VUCEM (facturas, listas de empaque, certificados, etc.).

```python
# Ver catálogo de tipos de documento disponibles
tipos = client.consultar_tipos_documento()

# Digitalizar un PDF
with open("factura.pdf", "rb") as f:
    pdf_bytes = f.read()

resultado = client.digitalizar_documento(
    correo="contacto@empresa.com",
    id_tipo_documento="1",            # 1 = Factura comercial
    nombre_documento="Factura ST2601016",
    archivo_bytes=pdf_bytes,
    rfc_consulta="PME241011C34",      # RFC autorizado a consultar
)

print(resultado["eDocument"])         # ej. "03602601109K2"
print(resultado["numeroOperacion"])

# Consultar el estatus de la digitalización
estatus = client.consultar_estatus_digitalizacion(resultado["numeroOperacion"])
```

> El PDF debe pesar menos de 3 MB.

---

### 4. Consulta de COVEs

```python
cove = client.consultar_cove("COVE2680TJDY6")
print(cove)
```

---

### 5. Consulta de pedimentos

```python
# Listar pedimentos por aduana y fecha
pedimentos = client.listar_pedimentos(
    aduana="160",
    patente=3977,
    fecha_inicio=date(2026, 1, 1),
    fecha_fin=date(2026, 6, 30),
)

# Estado de un pedimento
estado = client.consultar_estado_pedimentos(
    aduana=160, patente=3977,
    pedimento=6003095,
    numero_operacion=530502,
)

# Detalle completo
detalle = client.consultar_pedimento_completo(
    aduana="160", patente=3977, pedimento=6003095,
)

# Partida específica
partida = client.consultar_partida(
    aduana="160", patente=3977, pedimento=6003095,
    numero_operacion=530502, numero_partida=1,
)

# Remesas
remesas = client.consultar_remesas(
    aduana=160, patente=3977,
    pedimento=6003095, numero_operacion=530502,
)
```

---

### 6. Descarga de acuses PDF (eDocuments y COVEs)

```python
# Acuse de un eDocument digitalizado
resultado = client.descargar_acuse("03602601109K2")
if resultado.get("pdf"):
    with open("acuse.pdf", "wb") as f:
        f.write(resultado["pdf"])

# Acuse de un COVE
resultado = client.descargar_acuse("COVE2680TJDY6", es_cove=True)
```

> Este servicio soporta eDocuments numéricos y COVEs. Los números MVE (`MNVA...`) no son compatibles — para MVEs usa `client.generar_acuse(numero_operacion)`.

---

## Scripts de ejemplo

Los scripts en `ejemplos/` muestran flujos completos listos para ejecutar:

| Script | Descripción |
|--------|-------------|
| `enviar_mv_6003095.py` | Construye, valida y envía una MVE completa |
| `generar_preacuse_6003095.py` | Genera el borrador del acuse sin conectarse a VUCEM |
| `generar_acuse_6003095.py` | Genera el acuse oficial a partir del número de operación |
| `consultar_mv_530488.py` | Consulta el estatus de una MVE registrada |
| `cargar_documentos_6003095.py` | Digitaliza documentos PDF en VUCEM |
| `crear_pedimento_6003095.py` | Construye el modelo de MVE en JSON para revisión |

Flujo típico:

```bash
# 1. Revisar datos antes de enviar
python ejemplos/generar_preacuse_6003095.py

# 2. Enviar a VUCEM (guarda el XML y el numeroOperacion)
python ejemplos/enviar_mv_6003095.py

# 3. Generar el acuse oficial
python ejemplos/generar_acuse_6003095.py
```

---

## Notas técnicas

### Conectividad con VUCEM

Los servicios de VUCEM requieren configuración SSL especial (certificados legacy, TLS 1.0/1.1). La librería maneja esto internamente mediante `LegacyVucemAdapter` — no es necesaria ninguna configuración adicional.

### WSDL remoto vs local

Para servicios donde el XSD remoto devuelve 404 (como `ConsultaManifestacionService`), la librería usa WSDLs locales empaquetados solo para validación, mientras la comunicación sigue siendo contra el endpoint en producción.

### Cadena original y firma

La cadena original de la MVE usa el mismo formato numérico que el XML (máximo 3 decimales redondeados, sin ceros finales), garantizando que la firma electrónica sea válida. Este formato es equivalente al `formatVucemNumber()` de la implementación PHP de referencia de VUCEM.

### Prerequisito para acuses de MVE

El acuse de una MVE solo puede generarse cuando el COVE referenciado existe y está validado en VUCEM. El agente aduanal debe registrar el COVE **antes** de presentar la MVE.

---

## Estructura del proyecto

```
py_vucem/
├── src/py_vucem/
│   ├── __init__.py                # Exporta VucemClient, BanxicoClient y catálogos
│   ├── client.py                  # VucemClient — punto de entrada principal
│   ├── catalogos.py               # Enums: FormaPago, Incoterm, MetodoValoracion, etc.
│   ├── tipos_documento.py         # Catálogo de tipos de documento para digitalización
│   ├── models/
│   │   ├── mv_builder.py          # ManifestacionValor y Factura (API fluida)
│   │   ├── mv_mapper.py           # Convierte modelo a estructura interna VUCEM
│   │   ├── mv_xml.py              # Generador de XML SOAP firmado
│   │   ├── mv_xml_parser.py       # Parser inverso: XML VUCEM → modelo
│   │   └── mv_acuse_generator.py  # Generador de acuse HTML (retorna base64)
│   ├── services/
│   │   ├── manifestacion_service.py   # Registro, consulta y acuses de MVE
│   │   ├── digitalizar_documento.py   # Digitalización de PDFs
│   │   ├── pedimentos_service.py      # Consulta de pedimentos aduaneros
│   │   └── cove_consulta.py           # Consulta de COVEs
│   └── utils/
│       ├── __init__.py            # FielHandler (desde_archivo / desde_base64), LegacyVucemAdapter
│       └── banxico.py             # BanxicoClient — TC FIX oficial
├── ejemplos/                      # Scripts listos para ejecutar
├── tests/                         # Pruebas unitarias
│   └── fixtures/                  # Certificados de prueba
└── wsdl/                          # WSDLs locales de respaldo
```

---

## Licencia

MIT © Fernando Ruiz — [Pesatto México](https://pesatto.com)
