En el siguiente enlace encontrará información referente a comercio exterior 2.0 además podrá consultar el estándar en donde se encuentra el apartado “Validaciones adicionales a realizar por el Proveedor” para la implementación del complemento de acuerdo a las especificaciones del SAT.
Enlace directo del sitio del SAT. Comercio exterior
| Nombre | Documento |
|---|---|
| Esquema XSD: | 📥 Descargar |
| Utilerias XSLT: | 📥 Descargar |
| Catalogos: | 📥 Descargar |
| Matriz de errores: | 📥 Descargar |
<?xml version="1.0" encoding="utf-8"?>
<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/4"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sat.gob.mx/cfd/4 http://www.sat.gob.mx/sitio_internet/cfd/4/cfdv40.xsd http://www.sat.gob.mx/ComercioExterior20 http://www.sat.gob.mx/sitio_internet/cfd/ComercioExterior20/ComercioExterior20.xsd" xmlns:cce20="http://www.sat.gob.mx/ComercioExterior20"
Version="4.0" Serie="Serie" Folio="Folio1" Fecha="2026-03-25T00:00:00" FormaPago="99" NoCertificado="30001000000500003416"
Certificado="MIIFsDCCA5igAwIBAgIUMzAwMDEwMDAwMDA1MDAwMDM0MTYwDQYJKoZIhvcNAQELBQAwggErMQ8wDQYDVQQDDAZBQyBVQVQxLjAsBgNVBAoMJVNFUlZJQ0lPIERFIEFETUlOSVNUUkFDSU9OIFRSSUJVVEFSSUExGjAYBgNVBAsMEVNBVC1JRVMgQXV0aG9yaXR5MSgwJgYJKoZIhvcNAQkBFhlvc2Nhci5tYXJ0aW5lekBzYXQuZ29iLm14MR0wGwYDVQQJDBQzcmEgY2VycmFkYSBkZSBjYWxpejEOMAwGA1UEEQwFMDYzNzAxCzAJBgNVBAYTAk1YMRkwFwYDVQQIDBBDSVVEQUQgREUgTUVYSUNPMREwDwYDVQQHDAhDT1lPQUNBTjERMA8GA1UELRMIMi41LjQuNDUxJTAjBgkqhkiG9w0BCQITFnJlc3BvbnNhYmxlOiBBQ0RNQS1TQVQwHhcNMjMwNTE4MTE0MzUxWhcNMjcwNTE4MTE0MzUxWjCB1zEnMCUGA1UEAxMeRVNDVUVMQSBLRU1QRVIgVVJHQVRFIFNBIERFIENWMScwJQYDVQQpEx5FU0NVRUxBIEtFTVBFUiBVUkdBVEUgU0EgREUgQ1YxJzAlBgNVBAoTHkVTQ1VFTEEgS0VNUEVSIFVSR0FURSBTQSBERSBDVjElMCMGA1UELRMcRUtVOTAwMzE3M0M5IC8gVkFEQTgwMDkyN0RKMzEeMBwGA1UEBRMVIC8gVkFEQTgwMDkyN0hTUlNSTDA1MRMwEQYDVQQLEwpTdWN1cnNhbCAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtmecO6n2GS0zL025gbHGQVxznPDICoXzR2uUngz4DqxVUC/w9cE6FxSiXm2ap8Gcjg7wmcZfm85EBaxCx/0J2u5CqnhzIoGCdhBPuhWQnIh5TLgj/X6uNquwZkKChbNe9aeFirU/JbyN7Egia9oKH9KZUsodiM/pWAH00PCtoKJ9OBcSHMq8Rqa3KKoBcfkg1ZrgueffwRLws9yOcRWLb02sDOPzGIm/jEFicVYt2Hw1qdRE5xmTZ7AGG0UHs+unkGjpCVeJ+BEBn0JPLWVvDKHZAQMj6s5Bku35+d/MyATkpOPsGT/VTnsouxekDfikJD1f7A1ZpJbqDpkJnss3vQIDAQABox0wGzAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIGwDANBgkqhkiG9w0BAQsFAAOCAgEAFaUgj5PqgvJigNMgtrdXZnbPfVBbukAbW4OGnUhNrA7SRAAfv2BSGk16PI0nBOr7qF2mItmBnjgEwk+DTv8Zr7w5qp7vleC6dIsZFNJoa6ZndrE/f7KO1CYruLXr5gwEkIyGfJ9NwyIagvHHMszzyHiSZIA850fWtbqtythpAliJ2jF35M5pNS+YTkRB+T6L/c6m00ymN3q9lT1rB03YywxrLreRSFZOSrbwWfg34EJbHfbFXpCSVYdJRfiVdvHnewN0r5fUlPtR9stQHyuqewzdkyb5jTTw02D2cUfL57vlPStBj7SEi3uOWvLrsiDnnCIxRMYJ2UA2ktDKHk+zWnsDmaeleSzonv2CHW42yXYPCvWi88oE1DJNYLNkIjua7MxAnkNZbScNw01A6zbLsZ3y8G6eEYnxSTRfwjd8EP4kdiHNJftm7Z4iRU7HOVh79/lRWB+gd171s3d/mI9kte3MRy6V8MMEMCAnMboGpaooYwgAmwclI2XZCczNWXfhaWe0ZS5PmytD/GDpXzkX0oEgY9K/uYo5V77NdZbGAjmyi8cE2B2ogvyaN2XfIInrZPgEffJ4AB7kFA2mwesdLOCh0BLD9itmCve3A1FGR4+stO2ANUoiI3w3Tv2yQSg4bjeDlJ08lXaaFCLW2peEXMXjQUk7fmpb5MNuOUTW6BE=" CondicionesDePago="Condiciones" SubTotal="800" Moneda="USD" TipoCambio="17.8520" Total="928.00" TipoDeComprobante="I" Exportacion="02" MetodoPago="PPD" LugarExpedicion="20000" Sello="U54UYs4vFwDEPWWHCelJqtujv6l1zU+/ojajIYFE/Cha97J0XRxNqs1z+bPbHKx1EmviZJ7byWA1oi4E4vAd9CUKy0Aok+iW4oQ2yAREXbMuuMrMkHrz42xuzKjRpBIb2/WFAq8bkVeXplk17Fy+CgX6N6RdGQ/gcVkmyp7hplpkl+JhyRZuhwRplQIRiXQRpX6ZQC9rQHLcA7U8+60QuxOrgc6NTQS56FPjebJUGkiAgtmRdc8QeNQ6YISQsHBo49wTE+gm+RR265RQj3IIdSW776tPAUjrc6xzmIWfP7JhH6GU83H1EJ6jfAWksCc0vx5Qn8hyF7xcHktVhbOTPw==">
<cfdi:Emisor Rfc="EKU9003173C9" Nombre="ESCUELA KEMPER URGATE" RegimenFiscal="601" />
<cfdi:Receptor Rfc="CUSC850516316" Nombre="CESAR OSBALDO CRUZ SOLORZANO" DomicilioFiscalReceptor="45638" RegimenFiscalReceptor="616" UsoCFDI="S01" />
<cfdi:Conceptos>
<cfdi:Concepto ClaveProdServ="50211503" NoIdentificacion="131494-1055" Cantidad="2" ClaveUnidad="H87" Unidad="Pieza" Descripcion="Cigarros" ValorUnitario="400.00" Importe="800.00" ObjetoImp="02">
<cfdi:Impuestos>
<cfdi:Traslados>
<cfdi:Traslado Base="800.00" Impuesto="002" TipoFactor="Tasa" TasaOCuota="0.160000" Importe="128.00" />
</cfdi:Traslados>
</cfdi:Impuestos>
</cfdi:Concepto>
</cfdi:Conceptos>
<cfdi:Impuestos TotalImpuestosTrasladados="128.00">
<cfdi:Traslados>
<cfdi:Traslado Base="800.00" Impuesto="002" TipoFactor="Tasa" TasaOCuota="0.160000" Importe="128.00" />
</cfdi:Traslados>
</cfdi:Impuestos>
<cfdi:Complemento>
<cce20:ComercioExterior Version="2.0" ClaveDePedimento="A1" CertificadoOrigen="0" Incoterm="FOB" TipoCambioUSD="17.7780" TotalUSD="400.00">
<cce20:Emisor>
<cce20:Domicilio Calle="CALLE" Colonia="0214" Localidad="01" Municipio="014" Estado="QUE" Pais="MEX" CodigoPostal="76199" />
</cce20:Emisor>
<cce20:Receptor>
<cce20:Domicilio Calle="CALLE" Colonia="0214" Localidad="01" Municipio="014" Estado="QUE" Pais="MEX" CodigoPostal="76199" />
</cce20:Receptor>
<cce20:Mercancias>
<cce20:Mercancia NoIdentificacion="131494-1055" FraccionArancelaria="2402200100" CantidadAduana="117.64" UnidadAduana="01" ValorUnitarioAduana="3.40" ValorDolares="400.00" />
</cce20:Mercancias>
</cce20:ComercioExterior>
</cfdi:Complemento>
</cfdi:Comprobante>
<?xml version="1.0" encoding="utf-8"?>
<cfdi:Comprobante xsi:schemaLocation="http://www.sat.gob.mx/cfd/4 http://www.sat.gob.mx/sitio_internet/cfd/4/cfdv40.xsd http://www.sat.gob.mx/ComercioExterior20 http://www.sat.gob.mx/sitio_internet/cfd/ComercioExterior20/ComercioExterior20.xsd"
xmlns:cce20="http://www.sat.gob.mx/ComercioExterior20" Version="4.0" Fecha="2026-03-25T00:00:00" SubTotal="0.00" Moneda="USD" TipoCambio="17.1995" Total="0.00" TipoDeComprobante="T" Exportacion="02" LugarExpedicion="48020" xmlns:cfdi="http://www.sat.gob.mx/cfd/4" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" NoCertificado="30001000000500003416" Certificado="MIIFsDCCA5igAwIBAgIUMzAwMDEwMDAwMDA1MDAwMDM0MTYwDQYJKoZIhvcNAQELBQAwggErMQ8wDQYDVQQDDAZBQyBVQVQxLjAsBgNVBAoMJVNFUlZJQ0lPIERFIEFETUlOSVNUUkFDSU9OIFRSSUJVVEFSSUExGjAYBgNVBAsMEVNBVC1JRVMgQXV0aG9yaXR5MSgwJgYJKoZIhvcNAQkBFhlvc2Nhci5tYXJ0aW5lekBzYXQuZ29iLm14MR0wGwYDVQQJDBQzcmEgY2VycmFkYSBkZSBjYWxpejEOMAwGA1UEEQwFMDYzNzAxCzAJBgNVBAYTAk1YMRkwFwYDVQQIDBBDSVVEQUQgREUgTUVYSUNPMREwDwYDVQQHDAhDT1lPQUNBTjERMA8GA1UELRMIMi41LjQuNDUxJTAjBgkqhkiG9w0BCQITFnJlc3BvbnNhYmxlOiBBQ0RNQS1TQVQwHhcNMjMwNTE4MTE0MzUxWhcNMjcwNTE4MTE0MzUxWjCB1zEnMCUGA1UEAxMeRVNDVUVMQSBLRU1QRVIgVVJHQVRFIFNBIERFIENWMScwJQYDVQQpEx5FU0NVRUxBIEtFTVBFUiBVUkdBVEUgU0EgREUgQ1YxJzAlBgNVBAoTHkVTQ1VFTEEgS0VNUEVSIFVSR0FURSBTQSBERSBDVjElMCMGA1UELRMcRUtVOTAwMzE3M0M5IC8gVkFEQTgwMDkyN0RKMzEeMBwGA1UEBRMVIC8gVkFEQTgwMDkyN0hTUlNSTDA1MRMwEQYDVQQLEwpTdWN1cnNhbCAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtmecO6n2GS0zL025gbHGQVxznPDICoXzR2uUngz4DqxVUC/w9cE6FxSiXm2ap8Gcjg7wmcZfm85EBaxCx/0J2u5CqnhzIoGCdhBPuhWQnIh5TLgj/X6uNquwZkKChbNe9aeFirU/JbyN7Egia9oKH9KZUsodiM/pWAH00PCtoKJ9OBcSHMq8Rqa3KKoBcfkg1ZrgueffwRLws9yOcRWLb02sDOPzGIm/jEFicVYt2Hw1qdRE5xmTZ7AGG0UHs+unkGjpCVeJ+BEBn0JPLWVvDKHZAQMj6s5Bku35+d/MyATkpOPsGT/VTnsouxekDfikJD1f7A1ZpJbqDpkJnss3vQIDAQABox0wGzAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIGwDANBgkqhkiG9w0BAQsFAAOCAgEAFaUgj5PqgvJigNMgtrdXZnbPfVBbukAbW4OGnUhNrA7SRAAfv2BSGk16PI0nBOr7qF2mItmBnjgEwk+DTv8Zr7w5qp7vleC6dIsZFNJoa6ZndrE/f7KO1CYruLXr5gwEkIyGfJ9NwyIagvHHMszzyHiSZIA850fWtbqtythpAliJ2jF35M5pNS+YTkRB+T6L/c6m00ymN3q9lT1rB03YywxrLreRSFZOSrbwWfg34EJbHfbFXpCSVYdJRfiVdvHnewN0r5fUlPtR9stQHyuqewzdkyb5jTTw02D2cUfL57vlPStBj7SEi3uOWvLrsiDnnCIxRMYJ2UA2ktDKHk+zWnsDmaeleSzonv2CHW42yXYPCvWi88oE1DJNYLNkIjua7MxAnkNZbScNw01A6zbLsZ3y8G6eEYnxSTRfwjd8EP4kdiHNJftm7Z4iRU7HOVh79/lRWB+gd171s3d/mI9kte3MRy6V8MMEMCAnMboGpaooYwgAmwclI2XZCczNWXfhaWe0ZS5PmytD/GDpXzkX0oEgY9K/uYo5V77NdZbGAjmyi8cE2B2ogvyaN2XfIInrZPgEffJ4AB7kFA2mwesdLOCh0BLD9itmCve3A1FGR4+stO2ANUoiI3w3Tv2yQSg4bjeDlJ08lXaaFCLW2peEXMXjQUk7fmpb5MNuOUTW6BE=" Sello="YNTOSKDRxt1UXZIG6eU5GXbjgBxdViozyH55O+mU/K8Tph5PudqlvrgZays3ItN4CzHcLZLBuuC3vUw6V4/5QFDmK3+wt/E2QsD5B6uQ1RipGqRDfquftEB+lPMK3EEIyJ0MiT/0o63sUnZboxlcxgljfmIGb800I/oav4aMp7C35hZ1urc23ye3Rsc6xTJzalyF1t5S46ITHjbY2/nYSoWkoeNQdOEUihHQM0cojKvfEDaqo0oZGo0yhTWTn1RJ+mRUQ6v0XdsSSwrPWJItrEYrIVgjeFKhw4glceZO7ZPinFn/sCSjbumJhG8jCALJuZTnG3cdadN+nGwS9uvF7g==">
<cfdi:Emisor Rfc="EKU9003173C9" Nombre="ESCUELA KEMPER URGATE" RegimenFiscal="601" />
<cfdi:Receptor Rfc="EKU9003173C9" Nombre="ESCUELA KEMPER URGATE" DomicilioFiscalReceptor="20928" RegimenFiscalReceptor="601" UsoCFDI="G01" />
<cfdi:Conceptos>
<cfdi:Concepto ClaveProdServ="50211503" NoIdentificacion="131494-1055" Cantidad="2" ClaveUnidad="H87" Unidad="Pieza" Descripcion="Vasos" ValorUnitario="10.00" Importe="20.00" ObjetoImp="01" />
</cfdi:Conceptos>
<cfdi:Complemento>
<cce20:ComercioExterior MotivoTraslado="99" Version="2.0" ClaveDePedimento="A1" CertificadoOrigen="0" Incoterm="FOB" TipoCambioUSD="17.7780" TotalUSD="400.00">
<cce20:Emisor>
<cce20:Domicilio Calle="CALLE" Colonia="0214" Localidad="01" Municipio="014" Estado="QUE" Pais="MEX" CodigoPostal="76199" />
</cce20:Emisor>
<cce20:Mercancias>
<cce20:Mercancia NoIdentificacion="131494-1055" FraccionArancelaria="2402200100" CantidadAduana="117.64" UnidadAduana="01" ValorUnitarioAduana="3.40" ValorDolares="400.00" />
</cce20:Mercancias>
</cce20:ComercioExterior>
</cfdi:Complemento>
</cfdi:Comprobante>
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import base64
import hashlib
import subprocess
from datetime import datetime
from pathlib import Path
from lxml import etree
from M2Crypto.RSA import load_key_string
NS_CFDI = "http://www.sat.gob.mx/cfd/4"
NS_CCE20 = "http://www.sat.gob.mx/ComercioExterior20"
NS_XSI = "http://www.w3.org/2001/XMLSchema-instance"
DATOS = {
"Comprobante": {
"Version": "4.0",
"Serie": "Serie",
"Folio": "Folio1",
"FormaPago": "99",
"CondicionesDePago": "Condiciones",
"SubTotal": "800.00",
"Moneda": "USD",
"TipoCambio": "17.8520",
"Total": "928.00",
"TipoDeComprobante": "I",
"Exportacion": "02",
"MetodoPago": "PPD",
"LugarExpedicion": "20000",
},
"Emisor": {
"Rfc": "EKU9003173C9",
"Nombre": "ESCUELA KEMPER URGATE",
"RegimenFiscal": "601",
},
"Receptor": {
"Rfc": "CUSC850516316",
"Nombre": "CESAR OSBALDO CRUZ SOLORZANO",
"DomicilioFiscalReceptor": "45638",
"RegimenFiscalReceptor": "616",
"UsoCFDI": "S01",
},
"Impuestos": {
"TotalImpuestosTrasladados": "128.00",
"Traslado": {
"Base": "800.00",
"Impuesto": "002",
"TipoFactor": "Tasa",
"TasaOCuota": "0.160000",
"Importe": "128.00",
},
},
"ComercioExterior": {
"Version": "2.0",
"ClaveDePedimento": "A1",
"CertificadoOrigen": "0",
"Incoterm": "FOB",
"TipoCambioUSD": "17.7780",
"TotalUSD": "400.00",
},
}
CONCEPTO = {
"ClaveProdServ": "50211503",
"NoIdentificacion": "131494-1055",
"Cantidad": "2",
"ClaveUnidad": "H87",
"Unidad": "Pieza",
"Descripcion": "Cigarros",
"ValorUnitario": "400.00",
"Importe": "800.00",
"ObjetoImp": "02",
"Traslado": {
"Base": "800.00",
"Impuesto": "002",
"TipoFactor": "Tasa",
"TasaOCuota": "0.160000",
"Importe": "128.00",
},
}
DOMICILIO_CCE = {
"Calle": "CALLE",
"Colonia": "0214",
"Localidad": "01",
"Municipio": "014",
"Estado": "QUE",
"Pais": "MEX",
"CodigoPostal": "76199",
}
MERCANCIA = {
"NoIdentificacion": "131494-1055",
"FraccionArancelaria": "2402200100",
"CantidadAduana": "117.64",
"UnidadAduana": "01",
"ValorUnitarioAduana": "3.40",
"ValorDolares": "400.00",
}
def _leer_certificado(cer_path: Path) -> tuple[str, str]:
cert_bytes = cer_path.read_bytes()
cert_b64 = base64.b64encode(cert_bytes).decode().replace("\n", "").replace("\r", "")
serial_raw = subprocess.check_output(
["openssl", "x509", "-inform", "DER", "-in", str(cer_path), "-noout", "-serial"],
text=True,
).strip()
no_certificado = serial_raw[::2][4:].strip()
return cert_b64, no_certificado
def _parser_xslt_complements(xslt_path: Path):
class LocalResolver(etree.Resolver):
def resolve(self, url, pubid, context):
if "sitio_internet/cfd" in url or "sat.gob.mx" in url:
filename = url.split("/")[-1]
local_path = xslt_path.parent / "complements" / filename
if local_path.exists():
return self.resolve_filename(str(local_path), context)
return None
parser = etree.XMLParser(resolve_entities=True, remove_blank_text=False)
parser.resolvers.add(LocalResolver())
return parser
def _cadena_original(xml_path: Path, xslt_path: Path) -> str:
xml_doc = etree.parse(str(xml_path))
xslt_doc = etree.parse(str(xslt_path), parser=_parser_xslt_complements(xslt_path))
transform = etree.XSLT(xslt_doc)
return str(transform(xml_doc)).replace('<?xml version="1.0" encoding="UTF-8"?>', "").replace("\n", "").strip()
def _sello(cadena: str, key_path: Path) -> str:
key_data = key_path.read_text(encoding="utf-8")
rsa = load_key_string(key_data.encode("utf-8"))
digest = hashlib.sha256(cadena.encode("utf-8")).digest()
firma = rsa.sign(digest, "sha256")
return base64.b64encode(firma).decode("utf-8")
def generar_xml(output_path: Path, cer_path: Path, key_path: Path, xslt_path: Path) -> Path:
etree.register_namespace("cfdi", NS_CFDI)
etree.register_namespace("cce20", NS_CCE20)
etree.register_namespace("xsi", NS_XSI)
cert_b64, no_certificado = _leer_certificado(cer_path)
root_attrs = {
**DATOS["Comprobante"],
"Fecha": datetime.now().replace(microsecond=0).isoformat(),
"NoCertificado": no_certificado,
"Certificado": cert_b64,
"Sello": "",
f"{{{NS_XSI}}}schemaLocation": (
f"{NS_CFDI} http://www.sat.gob.mx/sitio_internet/cfd/4/cfdv40.xsd "
f"{NS_CCE20} http://www.sat.gob.mx/sitio_internet/cfd/ComercioExterior20/ComercioExterior20.xsd"
),
}
root = etree.Element(
f"{{{NS_CFDI}}}Comprobante",
root_attrs,
nsmap={
"cfdi": NS_CFDI,
"xsi": NS_XSI,
"cce20": NS_CCE20,
},
)
etree.SubElement(root, f"{{{NS_CFDI}}}Emisor", **DATOS["Emisor"])
etree.SubElement(root, f"{{{NS_CFDI}}}Receptor", **DATOS["Receptor"])
conceptos = etree.SubElement(root, f"{{{NS_CFDI}}}Conceptos")
concepto_data = CONCEPTO.copy()
traslado_concepto = concepto_data.pop("Traslado")
concepto = etree.SubElement(conceptos, f"{{{NS_CFDI}}}Concepto", **concepto_data)
impuestos_concepto = etree.SubElement(concepto, f"{{{NS_CFDI}}}Impuestos")
traslados_concepto = etree.SubElement(impuestos_concepto, f"{{{NS_CFDI}}}Traslados")
etree.SubElement(traslados_concepto, f"{{{NS_CFDI}}}Traslado", **traslado_concepto)
impuestos = etree.SubElement(
root,
f"{{{NS_CFDI}}}Impuestos",
TotalImpuestosTrasladados=DATOS["Impuestos"]["TotalImpuestosTrasladados"],
)
traslados = etree.SubElement(impuestos, f"{{{NS_CFDI}}}Traslados")
etree.SubElement(traslados, f"{{{NS_CFDI}}}Traslado", **DATOS["Impuestos"]["Traslado"])
complemento = etree.SubElement(root, f"{{{NS_CFDI}}}Complemento")
cce = etree.SubElement(
complemento,
f"{{{NS_CCE20}}}ComercioExterior",
**DATOS["ComercioExterior"],
)
emisor_cce = etree.SubElement(cce, f"{{{NS_CCE20}}}Emisor")
etree.SubElement(emisor_cce, f"{{{NS_CCE20}}}Domicilio", **DOMICILIO_CCE)
receptor_cce = etree.SubElement(cce, f"{{{NS_CCE20}}}Receptor")
etree.SubElement(receptor_cce, f"{{{NS_CCE20}}}Domicilio", **DOMICILIO_CCE)
mercancias = etree.SubElement(cce, f"{{{NS_CCE20}}}Mercancias")
etree.SubElement(mercancias, f"{{{NS_CCE20}}}Mercancia", **MERCANCIA)
output_path.parent.mkdir(parents=True, exist_ok=True)
etree.ElementTree(root).write(
str(output_path),
encoding="utf-8",
pretty_print=True,
xml_declaration=True,
)
cadena = _cadena_original(output_path, xslt_path)
sello = _sello(cadena, key_path)
tree = etree.parse(str(output_path))
tree.getroot().set("Sello", sello)
tree.write(str(output_path), encoding="utf-8", pretty_print=True, xml_declaration=True)
return output_path
if __name__ == "__main__":
salida = Path("xml/comercio_exterior20.xml")
cer = Path("certificados/EKU9003173C9/EKU9003173C9.cer")
key = Path("certificados/EKU9003173C9/EKU9003173C9.pem")
xslt = Path("xslt/cfdi/v40/cadenaoriginal.xslt")
archivo = generar_xml(salida, cer, key, xslt)
print(f"XML generado: {archivo}")
<?php
declare(strict_types=1);
date_default_timezone_set('America/Mexico_City');
const NS_CFDI = 'http://www.sat.gob.mx/cfd/4';
const NS_CCE20 = 'http://www.sat.gob.mx/ComercioExterior20';
const NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance';
$DATA = [
'Comprobante' => [
'Version' => '4.0',
'Serie' => 'Serie',
'Folio' => 'Folio1',
'Fecha' => '2026-03-25T00:00:00',
'FormaPago' => '99',
'CondicionesDePago' => 'Condiciones',
'SubTotal' => '800.00',
'Moneda' => 'USD',
'TipoCambio' => '17.8520',
'Total' => '928.00',
'TipoDeComprobante' => 'I',
'Exportacion' => '02',
'MetodoPago' => 'PPD',
'LugarExpedicion' => '20000',
],
'Emisor' => [
'Rfc' => 'EKU9003173C9',
'Nombre' => 'ESCUELA KEMPER URGATE',
'RegimenFiscal' => '601',
],
'Receptor' => [
'Rfc' => 'CUSC850516316',
'Nombre' => 'CESAR OSBALDO CRUZ SOLORZANO',
'DomicilioFiscalReceptor' => '45638',
'RegimenFiscalReceptor' => '616',
'UsoCFDI' => 'S01',
],
'Concepto' => [
'ClaveProdServ' => '50211503',
'NoIdentificacion' => '131494-1055',
'Cantidad' => '2',
'ClaveUnidad' => 'H87',
'Unidad' => 'Pieza',
'Descripcion' => 'Cigarros',
'ValorUnitario' => '400.00',
'Importe' => '800.00',
'ObjetoImp' => '02',
'Traslado' => [
'Base' => '800.00',
'Impuesto' => '002',
'TipoFactor' => 'Tasa',
'TasaOCuota' => '0.160000',
'Importe' => '128.00',
],
],
'Impuestos' => [
'TotalImpuestosTrasladados' => '128.00',
'Traslado' => [
'Base' => '800.00',
'Impuesto' => '002',
'TipoFactor' => 'Tasa',
'TasaOCuota' => '0.160000',
'Importe' => '128.00',
],
],
'ComercioExterior' => [
'Version' => '2.0',
'ClaveDePedimento' => 'A1',
'CertificadoOrigen' => '0',
'Incoterm' => 'FOB',
'TipoCambioUSD' => '17.7780',
'TotalUSD' => '400.00',
],
'DomicilioCCE' => [
'Calle' => 'CALLE',
'Colonia' => '0214',
'Localidad' => '01',
'Municipio' => '014',
'Estado' => 'QUE',
'Pais' => 'MEX',
'CodigoPostal' => '76199',
],
'MercanciaCCE' => [
'NoIdentificacion' => '131494-1055',
'FraccionArancelaria' => '2402200100',
'CantidadAduana' => '117.64',
'UnidadAduana' => '01',
'ValorUnitarioAduana' => '3.40',
'ValorDolares' => '400.00',
],
];
function setAttrs(DOMElement $node, array $attrs): void
{
foreach ($attrs as $k => $v) {
$node->setAttribute((string)$k, (string)$v);
}
}
function leerCertificado(string $cerPath): array
{
$certBytes = file_get_contents($cerPath);
if ($certBytes === false) {
throw new RuntimeException("No se pudo leer el certificado: {$cerPath}");
}
$certB64 = str_replace(["\n", "\r"], '', base64_encode($certBytes));
$certPem = "-----BEGIN CERTIFICATE-----\n"
. chunk_split(base64_encode($certBytes), 64, "\n")
. "-----END CERTIFICATE-----\n";
$parsed = openssl_x509_parse($certPem);
if ($parsed === false || empty($parsed['serialNumberHex'])) {
throw new RuntimeException('No se pudo obtener serialNumberHex del certificado.');
}
$hex = preg_replace('/\s+/', '', (string)$parsed['serialNumberHex']);
$decoded = hex2bin($hex);
if ($decoded === false || $decoded === '') {
throw new RuntimeException('No se pudo decodificar el serial del certificado.');
}
return [$certB64, strtoupper($decoded)];
}
function cadenaOriginal(string $xmlPath, string $xsltPath): string
{
$xml = new DOMDocument();
if (!$xml->load($xmlPath)) {
throw new RuntimeException("No se pudo cargar XML: {$xmlPath}");
}
$xsl = new DOMDocument();
if (!$xsl->load($xsltPath)) {
throw new RuntimeException("No se pudo cargar XSLT: {$xsltPath}");
}
$proc = new XSLTProcessor();
$proc->importStylesheet($xsl);
$result = $proc->transformToXML($xml);
if ($result === false) {
throw new RuntimeException('No se pudo generar la cadena original con XSLT.');
}
return trim(str_replace(["\n", '<?xml version="1.0"?>'], '', $result));
}
function crearSello(string $cadena, string $keyPath): string
{
$pem = file_get_contents($keyPath);
if ($pem === false) {
throw new RuntimeException("No se pudo leer la llave privada: {$keyPath}");
}
$privateKey = openssl_pkey_get_private($pem);
if ($privateKey === false) {
throw new RuntimeException('No se pudo cargar la llave privada.');
}
$ok = openssl_sign($cadena, $firma, $privateKey, OPENSSL_ALGO_SHA256);
if (!$ok) {
throw new RuntimeException('No se pudo firmar la cadena original.');
}
return base64_encode($firma);
}
function guardarCadenaOriginalTxt(string $outputXmlPath, string $cadena): string
{
$dir = dirname($outputXmlPath);
$timestamp = (new DateTimeImmutable('now', new DateTimeZone('America/Mexico_City')))->format('Ymd_His');
$txtPath = $dir . '/cadena_original_comercio_exterior20_' . $timestamp . '.txt';
if (file_put_contents($txtPath, $cadena . PHP_EOL) === false) {
throw new RuntimeException("No se pudo guardar la cadena original en TXT: {$txtPath}");
}
return $txtPath;
}
function generarXmlComercioExterior20(
string $outputPath,
string $cerPath,
string $keyPath,
string $xsltPath,
array $data
): array {
[$certB64, $noCertificado] = leerCertificado($cerPath);
$doc = new DOMDocument('1.0', 'UTF-8');
$doc->formatOutput = true;
$root = $doc->createElementNS(NS_CFDI, 'cfdi:Comprobante');
$root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:cce20', NS_CCE20);
$root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xsi', NS_XSI);
$root->setAttributeNS(
NS_XSI,
'xsi:schemaLocation',
NS_CFDI . ' http://www.sat.gob.mx/sitio_internet/cfd/4/cfdv40.xsd '
. NS_CCE20 . ' http://www.sat.gob.mx/sitio_internet/cfd/ComercioExterior20/ComercioExterior20.xsd'
);
$comprobante = $data['Comprobante'];
setAttrs($root, $comprobante);
$root->setAttribute('NoCertificado', $noCertificado);
$root->setAttribute('Certificado', $certB64);
$doc->appendChild($root);
$emisor = $doc->createElementNS(NS_CFDI, 'cfdi:Emisor');
setAttrs($emisor, $data['Emisor']);
$root->appendChild($emisor);
$receptor = $doc->createElementNS(NS_CFDI, 'cfdi:Receptor');
setAttrs($receptor, $data['Receptor']);
$root->appendChild($receptor);
$conceptos = $doc->createElementNS(NS_CFDI, 'cfdi:Conceptos');
$conceptoData = $data['Concepto'];
$trasladoConcepto = $conceptoData['Traslado'];
unset($conceptoData['Traslado']);
$concepto = $doc->createElementNS(NS_CFDI, 'cfdi:Concepto');
setAttrs($concepto, $conceptoData);
$impuestosConcepto = $doc->createElementNS(NS_CFDI, 'cfdi:Impuestos');
$trasladosConcepto = $doc->createElementNS(NS_CFDI, 'cfdi:Traslados');
$trasladoNodoConcepto = $doc->createElementNS(NS_CFDI, 'cfdi:Traslado');
setAttrs($trasladoNodoConcepto, $trasladoConcepto);
$trasladosConcepto->appendChild($trasladoNodoConcepto);
$impuestosConcepto->appendChild($trasladosConcepto);
$concepto->appendChild($impuestosConcepto);
$conceptos->appendChild($concepto);
$root->appendChild($conceptos);
$impuestos = $doc->createElementNS(NS_CFDI, 'cfdi:Impuestos');
$impuestosData = $data['Impuestos'];
$trasladoGlobal = $impuestosData['Traslado'];
unset($impuestosData['Traslado']);
setAttrs($impuestos, $impuestosData);
$traslados = $doc->createElementNS(NS_CFDI, 'cfdi:Traslados');
$traslado = $doc->createElementNS(NS_CFDI, 'cfdi:Traslado');
setAttrs($traslado, $trasladoGlobal);
$traslados->appendChild($traslado);
$impuestos->appendChild($traslados);
$root->appendChild($impuestos);
$complemento = $doc->createElementNS(NS_CFDI, 'cfdi:Complemento');
$cce = $doc->createElementNS(NS_CCE20, 'cce20:ComercioExterior');
setAttrs($cce, $data['ComercioExterior']);
$emisorCce = $doc->createElementNS(NS_CCE20, 'cce20:Emisor');
$domEmisor = $doc->createElementNS(NS_CCE20, 'cce20:Domicilio');
setAttrs($domEmisor, $data['DomicilioCCE']);
$emisorCce->appendChild($domEmisor);
$receptorCce = $doc->createElementNS(NS_CCE20, 'cce20:Receptor');
$domReceptor = $doc->createElementNS(NS_CCE20, 'cce20:Domicilio');
setAttrs($domReceptor, $data['DomicilioCCE']);
$receptorCce->appendChild($domReceptor);
$mercancias = $doc->createElementNS(NS_CCE20, 'cce20:Mercancias');
$mercancia = $doc->createElementNS(NS_CCE20, 'cce20:Mercancia');
setAttrs($mercancia, $data['MercanciaCCE']);
$mercancias->appendChild($mercancia);
$cce->appendChild($emisorCce);
$cce->appendChild($receptorCce);
$cce->appendChild($mercancias);
$complemento->appendChild($cce);
$root->appendChild($complemento);
$dir = dirname($outputPath);
if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
throw new RuntimeException("No se pudo crear directorio de salida: {$dir}");
}
$doc->save($outputPath);
$cadena = cadenaOriginal($outputPath, $xsltPath);
$txtPath = guardarCadenaOriginalTxt($outputPath, $cadena);
$sello = crearSello($cadena, $keyPath);
$sellado = new DOMDocument();
$sellado->formatOutput = true;
$sellado->load($outputPath);
$sellado->documentElement->setAttribute('Sello', $sello);
$sellado->save($outputPath);
return [$outputPath, $txtPath];
}
$base = __DIR__;
$rutaSalida = $base . '/xml/comercio_exterior20_php.xml';
$rutaCer = $base . '/certificados/EKU9003173C9/EKU9003173C9.cer';
$rutaKey = $base . '/certificados/EKU9003173C9/EKU9003173C9.pem';
$rutaXslt = $base . '/cadenaoriginal_4_0.xslt';
try {
[$xmlPath, $txtPath] = generarXmlComercioExterior20($rutaSalida, $rutaCer, $rutaKey, $rutaXslt, $DATA);
echo "XML generado: {$xmlPath}\n";
echo "Cadena original TXT: {$txtPath}\n";
} catch (Throwable $e) {
fwrite(STDERR, "ERROR: " . $e->getMessage() . "\n");
exit(1);
}