Skip to content

Commit 3fb368e

Browse files
committed
v4.0.417 comercio exterior y manifiestos verificados end-to-end
Alinea la version con el API (4.0.417) y deja los dos endpoints validados contra el API real, no solo por construccion de modelos. Verificacion end-to-end: - Timbrados los 18 escenarios de comercio exterior (9 por valores y 9 por referencias) y firmada la carta manifiesto. Sin CCE122 ni CFDI40179: la escala decimal sobrevive intacta a la serializacion. Ejemplos de comercio exterior: - La fecha de emision se toma al vuelo con datetime.now(). Antes estaba fija en una fecha literal, lo que hacia que los ejemplos fallaran con CCE121 en cuanto pasaban 72 horas. El resto de los ejemplos del repo ya usaba datetime.now(); esto los alinea. - tipo_cambio documenta que debe ser el valor del DOF para la fecha de emision y que el propio error del PAC informa el esperado. - Normalizada la indentacion del bloque __main__. Person: - sat_cfdi_use usaba el alias "cfdiUse" y nunca se poblaba; el API expone el catalogo expandido como "satCfdiUse". - Agregados manifest_status_id y manifest_status, que reflejan el resultado de firmar la carta manifiesto. - capital_regime marcado como deprecado: el API no expone esa propiedad. - InvoiceRecipient.foreign_country_code marcado como deprecado en favor de country_id, que es el campo que el API acepta. Deuda tecnica: - Retirados los 49 json_encoders de los modelos. Estan deprecados en Pydantic v2 y eran redundantes: send_request serializa con model_dump(mode="json"), que ya rinde Decimal como cadena y datetime como ISO-8601. Comprobado que los payloads quedan byte a byte identicos en los 18 escenarios. - Purgadas de examples.py una api key y un tenant reales que estaban commiteados en bloques cURL comentados; quedan los placeholders. La llave sigue expuesta en el historial: hay que rotarla. Documentacion: - README con secciones de uso de comercio exterior y firma de manifiesto, ambas ejecutadas y timbradas antes de documentarlas, mas el indice completo de ejemplos. - CLAUDE.md documenta manifest_service.py, los modulos de modelos por complemento y el criterio de escala decimal que exige el SAT.
1 parent 3c26c50 commit 3fb368e

11 files changed

Lines changed: 259 additions & 142 deletions

‎CLAUDE.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ FiscalApiClient (Facade)
4848
├── catalog_service.py → SAT catalog searches
4949
├── stamp_service.py → Stamp/validation credit ledger transactions
5050
├── sat_validation_service.py → SAT validations (structure, seals, status, 69-B blacklists)
51+
├── manifest_service.py → Manifest signing with the taxpayer's FIEL
5152
└── download_*_service.py → Bulk download management
5253
```
5354

@@ -63,13 +64,17 @@ client.invoices.create(invoice)
6364
client.people.get_list(page_num, page_size)
6465
client.stamps.get_list(page_num, page_size)
6566
client.sat_validations.validate(request)
67+
client.manifests.sign(request)
6668
```
6769

6870
### Models (Pydantic v2)
6971

7072
Located in `fiscalapi/models/`:
7173
- **common_models.py** - Base DTOs: `ApiResponse[T]`, `PagedList[T]`, `ValidationFailure`, `FiscalApiSettings`
7274
- **fiscalapi_models.py** - Domain models: `Invoice`, `Person`, `Product`, `TaxFile`, payroll complements, stamp transactions and the ledger enums (`CreditType`, `StampTransactionType`, `StampTransactionStatus`)
75+
- **comercio_exterior_models.py** - Comercio Exterior complement, reached through `InvoiceComplement.comercio_exterior`
76+
- **carta_porte_models.py** - Carta Porte complement, reached through `InvoiceComplement.carta_porte`
77+
- **manifest_models.py** - `SignManifestRequest` / `SignManifestResponse`
7378
- **sat_validation_models.py** - SAT validation models and the `SatValidationTypeIds` / `SatValidationStatusIds` id enums
7479

7580
**Key Pattern - Field Aliasing:** Models use Pydantic `Field(alias="...")` for API JSON field mapping. When serializing, use `by_alias=True` and `exclude_none=True`.
@@ -162,6 +167,18 @@ pip install -r requirements.txt
162167
- Use `list[T]` and `dict[K,V]` (Python 3.9+ built-in generics) instead of `List[T]` and `Dict[K,V]`
163168
- Use `default_factory=list` for mutable defaults, never `default=[]`
164169
- All Field() calls should have explicit `default=...` for required fields
170+
- Do not use `json_encoders`: it is deprecated in Pydantic v2 and redundant here, because
171+
`BaseService.send_request` serializes with `model_dump(mode="json", ...)`, which already
172+
renders `Decimal` as a JSON string and `datetime` as ISO-8601
173+
174+
### Decimal Scale
175+
176+
Monetary and rate fields are `decimal.Decimal`, never `float`. The SAT validates the *scale*
177+
(number of decimal places) of several CFDI attributes and rejects comprobantes that do not match,
178+
so trailing zeros must survive serialization. Build values from string literals with the exact
179+
scale (`Decimal("0.160000")`, not `Decimal(0.16)`); `model_dump(mode="json")` preserves them.
180+
Known rejections caused by the wrong scale: `CFDI40179` on `taxRate` (6 decimals) and `CCE122`
181+
on `valorDolares` (feeds the computed `TotalUSD`, 2 decimals).
165182

166183
### Type Annotations
167184

‎README.md‎

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
- **Configuración de datos fiscales** (RFC, domicilio fiscal, régimen fiscal)
3939
- **Datos de empleado** (agrega/actualiza/elimina datos de empleado a una persona. CFDI Nómina)
4040
- **Datos de empleador** (agrega/actualiza/elimina datos de empleador a una persona. CFDI Nómina)
41+
- **Firma de carta manifiesto** (firma el manifiesto de una persona con su e.firma/FIEL)
4142

4243
## 🛍️ Gestión de Productos/Servicios
4344
- **Gestión de productos y servicios** con catálogo personalizable
@@ -399,6 +400,137 @@ params = StampTransactionParams(
399400
api_response = client.stamps.transfer_stamps(params)
400401
```
401402

403+
### 10. Factura de Comercio Exterior
404+
405+
El complemento se envía en `Invoice.complement.comercio_exterior`. Puede combinarse con carta porte en el mismo comprobante.
406+
407+
> **Escala decimal:** el SAT valida la cantidad de decimales de varios campos. Construye siempre los montos con `Decimal("...")` a partir de una cadena con la escala exacta (`Decimal("0.160000")`, no `Decimal(0.16)`): el SDK preserva los ceros finales al serializar. Una escala incorrecta produce rechazos `CFDI40179` (en `taxRate`) o `CCE122` (en `valorDolares`).
408+
409+
> **Tipo de cambio:** `tipo_cambio_usd` debe ser el publicado en el DOF para la fecha de emisión, y la fecha del comprobante no puede exceder 72 horas al momento del timbrado. Si no coinciden, el PAC responde `CCE121` e indica el valor esperado.
410+
411+
```python
412+
from datetime import datetime
413+
from decimal import Decimal
414+
415+
from fiscalapi.models import (
416+
Invoice, InvoiceIssuer, InvoiceRecipient, InvoiceItem, ItemTax,
417+
InvoiceComplement, TaxCredential,
418+
ComercioExteriorComplement, ComercioExteriorEmisor, ComercioExteriorEmisorDomicilio,
419+
ComercioExteriorReceptor, ComercioExteriorReceptorDomicilio, ComercioExteriorMercancia,
420+
)
421+
422+
invoice = Invoice(
423+
version_code="4.0",
424+
payment_form_code="99",
425+
payment_method_code="PPD",
426+
currency_code="USD",
427+
type_code="I",
428+
expedition_zip_code="42501",
429+
series="CCE",
430+
date=datetime.now().replace(microsecond=0),
431+
export_code="02",
432+
issuer=InvoiceIssuer(
433+
tin="EKU9003173C9",
434+
legal_name="ESCUELA KEMPER URGATE",
435+
tax_regime_code="601",
436+
tax_credentials=[
437+
TaxCredential(base64_file="<CER_BASE64>", file_type=0, password="<CSD_PASSWORD>"),
438+
TaxCredential(base64_file="<KEY_BASE64>", file_type=1, password="<CSD_PASSWORD>"),
439+
],
440+
),
441+
# Receptor extranjero: country_id (ResidenciaFiscal) es obligatorio si se envía foreign_tin.
442+
recipient=InvoiceRecipient(
443+
tin="XEXX010101000",
444+
legal_name="U.S. 0026 SW",
445+
zip_code="42501",
446+
tax_regime_code="616",
447+
cfdi_use_code="S01",
448+
country_id="USA",
449+
foreign_tin="123456789",
450+
),
451+
items=[
452+
InvoiceItem(
453+
item_code="50211503",
454+
item_sku="131494-1055", # debe coincidir con mercancia.no_identificacion
455+
quantity=Decimal("2"),
456+
unit_of_measurement_code="H87",
457+
description="Cigarros",
458+
unit_price=Decimal("200.00"),
459+
discount=Decimal("0"),
460+
tax_object_code="02",
461+
item_taxes=[
462+
ItemTax(tax_code="002", tax_type_code="Tasa", tax_rate=Decimal("0.160000"), tax_flag_code="T"),
463+
],
464+
),
465+
],
466+
complement=InvoiceComplement(
467+
comercio_exterior=ComercioExteriorComplement(
468+
clave_de_pedimento_id="A1",
469+
certificado_origen=0, # 0 = no funge como certificado de origen, 1 = sí
470+
incoterm_id="FOB",
471+
tipo_cambio_usd=Decimal("16.9722"),
472+
# Domicilio del emisor: campos de catálogo SAT, con sufijo Id.
473+
emisor=ComercioExteriorEmisor(
474+
domicilio=ComercioExteriorEmisorDomicilio(
475+
calle="CALLE DEL PAPEL",
476+
colonia_id="0214",
477+
localidad_id="01",
478+
municipio_id="014",
479+
estado_id="QUE",
480+
pais_id="MEX",
481+
codigo_postal_id="76199",
482+
),
483+
),
484+
# Domicilio del receptor: texto libre, sin sufijo Id (salvo pais_id).
485+
receptor=ComercioExteriorReceptor(
486+
num_reg_id_trib="123456789",
487+
domicilio=ComercioExteriorReceptorDomicilio(
488+
calle="ST. A",
489+
estado="TX",
490+
pais_id="USA",
491+
codigo_postal="00000",
492+
),
493+
),
494+
mercancias=[
495+
ComercioExteriorMercancia(
496+
no_identificacion="131494-1055",
497+
fraccion_arancelaria_id="2402200100",
498+
cantidad_aduana=Decimal("117.64"),
499+
unidad_aduana_id="01",
500+
valor_unitario_aduana=Decimal("3.40"),
501+
valor_dolares=Decimal("400.00"),
502+
),
503+
],
504+
),
505+
),
506+
)
507+
508+
api_response = client.invoices.create(invoice)
509+
```
510+
511+
### 11. Firmar Carta Manifiesto
512+
513+
Firma la carta manifiesto de una persona y devuelve el PDF resultante en base64.
514+
515+
Requiere la **e.firma (FIEL)** del contribuyente, no el CSD de timbrado, y que exista una persona con ese RFC en el tenant.
516+
517+
```python
518+
from fiscalapi.models import SignManifestRequest
519+
520+
api_response = client.manifests.sign(SignManifestRequest(
521+
base64_cer="<FIEL_CER_BASE64>",
522+
base64_key="<FIEL_KEY_BASE64>",
523+
password="<FIEL_PASSWORD>",
524+
))
525+
526+
if api_response.succeeded:
527+
print(api_response.data.file_name) # EKU9003173C9.pdf
528+
print(api_response.data.file_extension) # .pdf
529+
print(api_response.data.base64_file) # PDF firmado en base64
530+
```
531+
532+
Tras una firma exitosa, `client.people.get_by_id(...)` refleja el nuevo estado en `manifest_status_id`.
533+
402534
## 📋 Operaciones Principales
403535

404536
- **Facturas (CFDI)**
@@ -411,6 +543,8 @@ api_response = client.stamps.transfer_stamps(params)
411543
Listar transacciones, transferir y retirar timbres o créditos de validación entre personas.
412544
- **Validaciones SAT**
413545
Validar estructura, certificado, sellos, estatus en el SAT y listas negras 69-B de un CFDI o un RFC.
546+
- **Carta manifiesto**
547+
Firmar la carta manifiesto de una persona con su e.firma (FIEL) y obtener el PDF resultante.
414548

415549
## 📂 Más Ejemplos
416550

@@ -420,6 +554,11 @@ api_response = client.stamps.transfer_stamps(params)
420554
- [Facturas de Nómina](examples/ejemplos-facturas-de-nomina.py)
421555
- [Impuestos Locales (Por Valores)](examples/ejemplos-factura-impuestos-locales-valores.py)
422556
- [Impuestos Locales (Por Referencias)](examples/ejemplos-factura-impuestos-locales-referencias.py)
557+
- [Comercio Exterior (Por Valores)](examples/ejemplos-factura-comercio-exterior-valores.py)
558+
- [Comercio Exterior (Por Referencias)](examples/ejemplos-factura-comercio-exterior-referencias.py)
559+
- [Carta Porte (Por Valores)](examples/ejemplos-factura-carta-porte-valores.py)
560+
- [Carta Porte (Por Referencias)](examples/ejemplos-factura-carta-porte-referencias.py)
561+
- [Firma de Cartas Manifiesto](examples/ejemplos-firma-manifiestos.py)
423562

424563
## 🤝 Contribuir
425564

‎examples/ejemplos-factura-carta-porte-valores.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@
4343

4444
settings = FiscalApiSettings(
4545
api_url="https://test.fiscalapi.com",
46-
api_key="API_KEY",
47-
tenant="TENANT_ID"
46+
api_key="<API_KEY>",
47+
tenant="<TENANT_KEY>"
4848
)
4949

5050
client = FiscalApiClient(settings=settings)

‎examples/ejemplos-factura-comercio-exterior-referencias.py‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,20 @@
5555

5656
settings = FiscalApiSettings(
5757
api_url="https://test.fiscalapi.com",
58-
api_key="API_KEY",
59-
tenant="TENANT_ID"
58+
api_key="<API_KEY>",
59+
tenant="<TENANT_KEY>"
6060
)
6161

6262
client = FiscalApiClient(settings=settings)
6363

6464
# Valores centralizados para todos los ejemplos (cambia aqui una sola vez)
65-
current_date = datetime.fromisoformat("2026-05-19T08:56:40")
66-
tipo_cambio = Decimal("17.3477")
65+
# Fecha de emision: el SAT rechaza con CCE121 un CFDI cuya fecha exceda 72 horas
66+
# respecto al momento del timbrado, por eso se toma la fecha actual.
67+
current_date = datetime.now().replace(microsecond=0)
68+
# Tipo de cambio USD publicado en el DOF para la fecha de emision. El PAC lo
69+
# valida contra el DOF y, cuando no coincide, el error CCE121 informa el valor
70+
# esperado; actualiza esta constante con ese valor.
71+
tipo_cambio = Decimal("16.9722")
6772
issuer_id = "2e7b988f-3a2a-4f67-86e9-3f931dd48581" # ESCUELA KEMPER URGATE
6873
recipient_id = "109f4d94-63ea-4a21-ab15-20c8b87d8ee9" # KARLA FUENTES
6974

@@ -1171,7 +1176,7 @@ def factura_ce_unidades_de_medida_no_equivalentes_por_referencias():
11711176
# MAIN
11721177
# ============================================================================
11731178
if __name__ == "__main__":
1174-
factura_ce_ingreso_con_carta_porte_31_por_referencias()
1179+
factura_ce_ingreso_con_carta_porte_31_por_referencias()
11751180
# factura_ce_ingreso_diferentes_monedas_por_referencias()
11761181
# factura_ce_kit_parte_por_referencias()
11771182
# factura_ce_receptor_extranjero_por_referencias()

‎examples/ejemplos-factura-comercio-exterior-valores.py‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@
3939

4040
settings = FiscalApiSettings(
4141
api_url="https://test.fiscalapi.com",
42-
api_key="API_KEY",
43-
tenant="TENANT_ID"
42+
api_key="<API_KEY>",
43+
tenant="<TENANT_KEY>"
4444
)
4545

4646
client = FiscalApiClient(settings=settings)
@@ -51,8 +51,13 @@
5151
password = "12345678a"
5252

5353
# Valores centralizados para todos los ejemplos (cambia aqui una sola vez)
54-
current_date = datetime.fromisoformat("2026-05-19T08:56:40")
55-
tipo_cambio = Decimal("17.3477")
54+
# Fecha de emision: el SAT rechaza con CCE121 un CFDI cuya fecha exceda 72 horas
55+
# respecto al momento del timbrado, por eso se toma la fecha actual.
56+
current_date = datetime.now().replace(microsecond=0)
57+
# Tipo de cambio USD publicado en el DOF para la fecha de emision. El PAC lo
58+
# valida contra el DOF y, cuando no coincide, el error CCE121 informa el valor
59+
# esperado; actualiza esta constante con ese valor.
60+
tipo_cambio = Decimal("16.9722")
5661
issuer_id = "2e7b988f-3a2a-4f67-86e9-3f931dd48581" #ESCUELA KEMPER URGATE
5762
recipient_id = "109f4d94-63ea-4a21-ab15-20c8b87d8ee9" #KARLA FUENTES
5863

@@ -1130,4 +1135,4 @@ def factura_ce_unidades_de_medida_no_equivalentes_por_valores():
11301135
# factura_ce_traslado_con_carta_porte_31_por_valores()
11311136
# factura_ce_traslado_traslado_mercancia_propia_por_valores()
11321137
# factura_ce_traslado_traslado_por_valores()
1133-
factura_ce_unidades_de_medida_no_equivalentes_por_valores()
1138+
factura_ce_unidades_de_medida_no_equivalentes_por_valores()

‎examples/ejemplos-firma-manifiestos.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
# Configuración de FiscalAPI
1616
settings = FiscalApiSettings(
1717
api_url="https://test.fiscalapi.com",
18-
api_key="API_KEY",
19-
tenant="TENANT_ID"
18+
api_key="<API_KEY>",
19+
tenant="<TENANT_KEY>"
2020
)
2121

2222
# Credenciales FIEL de prueba (ESCUELA KEMPER URGATE)

0 commit comments

Comments
 (0)