Este recurso describe el estándar necesario para integrar la información de destrucción de vehículos en facturas electrónicas, conforme a las disposiciones que comparte la autoridad (SAT).
El Complemento de certificado de destrucción es un requisito indispensable al emitir facturas por el fin de la vida útil de un vehículo. Su propósito es vincular el comprobante fiscal con el certificado oficial de destrucción otorgado por las autoridades correspondientes.
Para el desarrollo y validación de este complemento, se ponen a disposición los siguientes archivos descargables.
| Nombre | Documento |
|---|---|
| Estándar del complemento: | 📥 Descargar |
| Esquema XSD: | 📥 Descargar |
| Utilería XSLT: | 📥 Descargar |
| Catálogo de Datos: | 📥 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" xmlns:destruccion="http://www.sat.gob.mx/certificadodestruccion" 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/certificadodestruccion http://www.sat.gob.mx/sitio_internet/cfd/certificadodestruccion/certificadodedestruccion.xsd" Version="4.0" Serie="MOTO" Folio="9090" Fecha="2026-01-27T10:20:00" FormaPago="99" NoCertificado="30001000000500003416" SubTotal="850.00" Moneda="MXN" Total="986.00" TipoDeComprobante="I" Exportacion="01" MetodoPago="PPD" LugarExpedicion="58000" 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="eECvtDO6mTUHF2+edWzyKK5E56CvW/hQ27a/wscTKw7i1SuD3mpA62nt3fdMThzZ5nBNTwHmruUWlRTwpcJPtnkAB/gT/mn93ELfLQrCXzvdfzvqRg/RBXt0nOUIEoTPoUa3+yvVTC2GNz2J+pABHyh+nOHHkovPGSE4/N57OQxBZNhp1M5K/jmYLT6QtS9dQK+D9Mhu1paH2fAdI9kQ5KlNpnyCDeuWJ0P1bdCJrEYdV/ZILCAgwZoKPwL4gK8U23RoniJP8lidN/Is8PS65Q5FgvvFk7zo+GTHbgcUMUzFOzHQvZjmIe2brZ5qLP2PAxqKvEWMdQnzy7tYuC3VwQ==">
<cfdi:Emisor Rfc="EKU9003173C9" Nombre="ESCUELA KEMPER URGATE" RegimenFiscal="601"/>
<cfdi:Receptor Rfc="AABF800614HI0" Nombre="FELIX MANUEL ANDRADE BALLADO" DomicilioFiscalReceptor="86400" RegimenFiscalReceptor="605" UsoCFDI="S01"/>
<cfdi:Conceptos>
<cfdi:Concepto ClaveProdServ="76122301" NoIdentificacion="DES-MOTO" Cantidad="1" ClaveUnidad="E48" Unidad="Unidad de servicio" Descripcion="DESTRUCCION DE MOTOCICLETA" ValorUnitario="850.00" Importe="850.00" ObjetoImp="02">
<cfdi:Impuestos>
<cfdi:Traslados>
<cfdi:Traslado Base="850.00" Impuesto="002" TipoFactor="Tasa" TasaOCuota="0.160000" Importe="136.00"/>
</cfdi:Traslados>
</cfdi:Impuestos>
</cfdi:Concepto>
</cfdi:Conceptos>
<cfdi:Impuestos TotalImpuestosTrasladados="136.00">
<cfdi:Traslados>
<cfdi:Traslado Base="850.00" Impuesto="002" TipoFactor="Tasa" TasaOCuota="0.160000" Importe="136.00"/>
</cfdi:Traslados>
</cfdi:Impuestos>
<cfdi:Complemento>
<destruccion:certificadodedestruccion Version="1.0" Serie="SERIE B" NumFolDesVeh="MOTO-IMP-2026-X">
<destruccion:VehiculoDestruido Marca="KAWASAKI" TipooClase="MOTOCICLETA DEPISTA" Año="2024" Modelo="NINJA 400" NIV="JKAKX450CCDA12335" NumSerie="JKAKX450CCDA12345" NumPlacas="X9-ZZ" NumMotor="EX400GE0000100" NumFolTarjCir="MOTO-TARJ-9988"/>
<destruccion:InformacionAduanera NumPedImp="24 47 3520 6009988 EXT" Fecha="2024-06-10" Aduana="AEROPUERTO INTERNACIONAL CDMX"/>
</destruccion:certificadodedestruccion>
</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_DESTRUCCION = "http://www.sat.gob.mx/certificadodestruccion"
NS_XSI = "http://www.w3.org/2001/XMLSchema-instance"
DATOS = {
"Comprobante": {
"Version": "4.0",
"Serie": "MOTO",
"Folio": "9090",
"FormaPago": "99",
"SubTotal": "850.00",
"Moneda": "MXN",
"Total": "986.00",
"TipoDeComprobante": "I",
"Exportacion": "01",
"MetodoPago": "PPD",
"LugarExpedicion": "58000",
},
"Emisor": {
"Rfc": "EKU9003173C9",
"Nombre": "ESCUELA KEMPER URGATE",
"RegimenFiscal": "601",
},
"Receptor": {
"Rfc": "AABF800614HI0",
"Nombre": "FELIX MANUEL ANDRADE BALLADO",
"DomicilioFiscalReceptor": "86400",
"RegimenFiscalReceptor": "605",
"UsoCFDI": "S01",
},
"Impuestos": {
"TotalImpuestosTrasladados": "136.00",
"Traslado": {
"Base": "850.00",
"Impuesto": "002",
"TipoFactor": "Tasa",
"TasaOCuota": "0.160000",
"Importe": "136.00",
},
},
"Complemento": {
"Version": "1.0",
"Serie": "SERIE B",
"NumFolDesVeh": "MOTO-IMP-2026-X",
"VehiculoDestruido": {
"Marca": "KAWASAKI",
"TipooClase": "MOTOCICLETA DEPISTA",
"A\u00f1o": "2024",
"Modelo": "NINJA 400",
"NIV": "JKAKX450CCDA12335",
"NumSerie": "JKAKX450CCDA12345",
"NumPlacas": "X9-ZZ",
"NumMotor": "EX400GE0000100",
"NumFolTarjCir": "MOTO-TARJ-9988",
},
"InformacionAduanera": {
"NumPedImp": "24 47 3520 6009988 EXT",
"Fecha": "2024-06-10",
"Aduana": "AEROPUERTO INTERNACIONAL CDMX",
},
},
}
CONCEPTO = {
"ClaveProdServ": "76122301",
"NoIdentificacion": "DES-MOTO",
"Cantidad": "1",
"ClaveUnidad": "E48",
"Unidad": "Unidad de servicio",
"Descripcion": "DESTRUCCION DE MOTOCICLETA",
"ValorUnitario": "850.00",
"Importe": "850.00",
"ObjetoImp": "02",
"Traslado": {
"Base": "850.00",
"Impuesto": "002",
"TipoFactor": "Tasa",
"TasaOCuota": "0.160000",
"Importe": "136.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("destruccion", NS_DESTRUCCION)
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_DESTRUCCION} "
"http://www.sat.gob.mx/sitio_internet/cfd/certificadodestruccion/certificadodedestruccion.xsd"
),
}
root = etree.Element(
f"{{{NS_CFDI}}}Comprobante",
root_attrs,
nsmap={
"cfdi": NS_CFDI,
"xsi": NS_XSI,
"destruccion": NS_DESTRUCCION,
},
)
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")
complemento_data = DATOS["Complemento"]
cert_destruccion = etree.SubElement(
complemento,
f"{{{NS_DESTRUCCION}}}certificadodedestruccion",
Version=complemento_data["Version"],
Serie=complemento_data["Serie"],
NumFolDesVeh=complemento_data["NumFolDesVeh"],
)
etree.SubElement(
cert_destruccion,
f"{{{NS_DESTRUCCION}}}VehiculoDestruido",
**complemento_data["VehiculoDestruido"],
)
etree.SubElement(
cert_destruccion,
f"{{{NS_DESTRUCCION}}}InformacionAduanera",
**complemento_data["InformacionAduanera"],
)
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/certificados_de_destrucción.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
date_default_timezone_set("America/Mexico_City");
error_reporting(E_ALL);
function normalizarCadenaOriginal(string $cadena): string {
$cadena = str_replace('<?xml version="1.0" encoding="UTF-8"?>', '', $cadena);
$cadena = str_replace(["\r", "\n"], '', $cadena);
$cadena = str_replace([""", "<", ">", "&"], ['"', '<', '>', '&'], $cadena);
return trim($cadena);
}
function obtenerNoCertificado(array $parsed): string {
if (empty($parsed['serialNumberHex'])) {
return '';
}
$hex = preg_replace('/\s+/', '', $parsed['serialNumberHex']);
$ascii = @hex2bin($hex);
if ($ascii !== false && preg_match('/^[0-9]+$/', $ascii)) {
return $ascii;
}
return strtoupper($hex);
}
// Rutas
$rutaCer = "/certificados/EKU9003173C9/EKU9003173C9.cer";
$rutaKey = "/certificados/EKU9003173C9/EKU9003173C9.pem";
$rutaXML = "/certificados.xml";
$rutaCadena = "/CadenaOriginal.txt";
$rutaXSLT = "/xslt/cfdi/v40/cadenaoriginal.xslt";
// Validaciones de archivos de sistema
if (!file_exists($rutaCer)) die("Error: No existe el CER en $rutaCer\n");
if (!file_exists($rutaKey)) die("Error: No existe el KEY PEM en $rutaKey\n");
if (!file_exists($rutaXSLT)) die("Error: No existe el archivo XSLT en $rutaXSLT\n");
if (!file_exists(dirname($rutaXML))) {
mkdir(dirname($rutaXML), 0777, true);
}
// === PREPARACIÓN DE CERTIFICADO ===
$certDer = file_get_contents($rutaCer);
$certificado = base64_encode($certDer);
// Obtener NoCertificado
$certPem = "-----BEGIN CERTIFICATE-----\n" .
chunk_split($certificado, 64, "\n") .
"-----END CERTIFICATE-----\n";
$parsed = @openssl_x509_parse($certPem);
$noCertificado = "";
if ($parsed !== false) {
$noCertificado = obtenerNoCertificado($parsed);
}
// === NAMESPACES ===
$ns = "http://www.sat.gob.mx/cfd/4";
$nsCert = "http://www.sat.gob.mx/certificadodestruccion";
// === CREAR ESTRUCTURA XML ===
$xml = new DOMDocument("1.0", "UTF-8");
$xml->formatOutput = true;
// Nodo Comprobante
$comprobante = $xml->createElementNS($ns, "cfdi:Comprobante");
$xml->appendChild($comprobante);
// Declaración de Namespaces adicionales
$comprobante->setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:destruccion", $nsCert);
$comprobante->setAttribute("xmlns:cfdi", $ns);
$comprobante->setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
$comprobante->setAttribute("xsi:schemaLocation",
"$ns http://www.sat.gob.mx/sitio_internet/cfd/4/cfdv40.xsd " .
"$nsCert http://www.sat.gob.mx/sitio_internet/cfd/certificadodestruccion/certificadodedestruccion.xsd"
);
// Atributos CFDI 4.0
$comprobante->setAttribute("Version", "4.0");
$comprobante->setAttribute("Serie", "A");
$comprobante->setAttribute("Folio", "167ABC");
$comprobante->setAttribute("Fecha", date('Y-m-d\TH:i:s'));
$comprobante->setAttribute("SubTotal", "1000.00");
$comprobante->setAttribute("Total", "1120.00");
$comprobante->setAttribute("Moneda", "MXN");
$comprobante->setAttribute("TipoCambio", "1");
$comprobante->setAttribute("TipoDeComprobante", "I");
$comprobante->setAttribute("LugarExpedicion", "58000");
$comprobante->setAttribute("FormaPago", "03");
$comprobante->setAttribute("MetodoPago", "PUE");
$comprobante->setAttribute("Exportacion", "01");
if (!empty($noCertificado)) {
$comprobante->setAttribute("NoCertificado", $noCertificado);
}
$comprobante->setAttribute("Certificado", $certificado);
// === EMISOR ===
$emisor = $xml->createElementNS($ns, "cfdi:Emisor");
$emisor->setAttribute("Rfc", "EKU9003173C9");
$emisor->setAttribute("Nombre", "ESCUELA KEMPER URGATE");
$emisor->setAttribute("RegimenFiscal", "601");
$comprobante->appendChild($emisor);
// === RECEPTOR ===
$receptor = $xml->createElementNS($ns, "cfdi:Receptor");
$receptor->setAttribute("Rfc", "ICV060329BY0");
$receptor->setAttribute("Nombre", "INMOBILIARIA CVA");
$receptor->setAttribute("UsoCFDI", "G03");
$receptor->setAttribute("RegimenFiscalReceptor", "601");
$receptor->setAttribute("DomicilioFiscalReceptor", "33826");
$comprobante->appendChild($receptor);
// === CONCEPTOS ===
$conceptos = $xml->createElementNS($ns, "cfdi:Conceptos");
$concepto = $xml->createElementNS($ns, "cfdi:Concepto");
$concepto->setAttribute("ClaveProdServ", "78101802");
$concepto->setAttribute("Cantidad", "1.00");
$concepto->setAttribute("ClaveUnidad", "E48");
$concepto->setAttribute("Descripcion", "Servicio de chatarrización y destrucción");
$concepto->setAttribute("ValorUnitario", "1000.00");
$concepto->setAttribute("Importe", "1000.00");
$concepto->setAttribute("ObjetoImp", "02");
// Impuestos del Concepto
$impC = $xml->createElementNS($ns, "cfdi:Impuestos");
$tras = $xml->createElementNS($ns, "cfdi:Traslados");
$t = $xml->createElementNS($ns, "cfdi:Traslado");
$t->setAttribute("Base", "1000.00");
$t->setAttribute("Impuesto", "002");
$t->setAttribute("TipoFactor", "Tasa");
$t->setAttribute("TasaOCuota", "0.160000");
$t->setAttribute("Importe", "160.00");
$tras->appendChild($t);
$retC = $xml->createElementNS($ns, "cfdi:Retenciones");
$r = $xml->createElementNS($ns, "cfdi:Retencion");
$r->setAttribute("Base", "1000.00");
$r->setAttribute("Impuesto", "002");
$r->setAttribute("TipoFactor", "Tasa");
$r->setAttribute("TasaOCuota", "0.040000");
$r->setAttribute("Importe", "40.00");
$retC->appendChild($r);
$impC->appendChild($tras);
$impC->appendChild($retC);
$concepto->appendChild($impC);
$conceptos->appendChild($concepto);
$comprobante->appendChild($conceptos);
// === IMPUESTOS TOTALES ===
$impTotal = $xml->createElementNS($ns, "cfdi:Impuestos");
$impTotal->setAttribute("TotalImpuestosRetenidos", "40.00");
$impTotal->setAttribute("TotalImpuestosTrasladados", "160.00");
$retencionesTotal = $xml->createElementNS($ns, "cfdi:Retenciones");
$retencionTotal = $xml->createElementNS($ns, "cfdi:Retencion");
$retencionTotal->setAttribute("Impuesto", "002");
$retencionTotal->setAttribute("Importe", "40.00");
$retencionesTotal->appendChild($retencionTotal);
$impTotal->appendChild($retencionesTotal);
$trasladosTotal = $xml->createElementNS($ns, "cfdi:Traslados");
$trasladoTotal = $xml->createElementNS($ns, "cfdi:Traslado");
$trasladoTotal->setAttribute("Base", "1000.00");
$trasladoTotal->setAttribute("Impuesto", "002");
$trasladoTotal->setAttribute("TipoFactor", "Tasa");
$trasladoTotal->setAttribute("TasaOCuota", "0.160000");
$trasladoTotal->setAttribute("Importe", "160.00");
$trasladosTotal->appendChild($trasladoTotal);
$impTotal->appendChild($trasladosTotal);
$comprobante->appendChild($impTotal);
// === COMPLEMENTO: CERTIFICADO DE DESTRUCCIÓN ===
$Complemento = $xml->createElementNS($ns, "cfdi:Complemento");
$comprobante->appendChild($Complemento);
$CertDest = $xml->createElementNS($nsCert, "destruccion:certificadodedestruccion");
$CertDest->setAttribute("Version", "1.0");
$CertDest->setAttribute("Serie", "SERIE A");
$CertDest->setAttribute("NumFolDesVeh", "01");
$Complemento->appendChild($CertDest);
// Nodo VehiculoDestruido
$Vehiculo = $xml->createElementNS($nsCert, "destruccion:VehiculoDestruido");
$Vehiculo->setAttribute("Año", "2000");
$Vehiculo->setAttribute("Marca", "BMW");
$Vehiculo->setAttribute("Modelo", "M140i");
$Vehiculo->setAttribute("NumFolTarjCir", "168DDGGTJUYK0532");
$Vehiculo->setAttribute("NumPlacas", "1RRT5679");
$Vehiculo->setAttribute("TipooClase", "Familiar");
$CertDest->appendChild($Vehiculo);
// Nodo InformacionAduanera (Opcional según el caso)
$InfoAduana = $xml->createElementNS($nsCert, "destruccion:InformacionAduanera");
$InfoAduana->setAttribute("Aduana", "02");
$InfoAduana->setAttribute("Fecha", "2017-04-22");
$InfoAduana->setAttribute("NumPedImp", "001204");
$CertDest->appendChild($InfoAduana);
// === GENERACIÓN DE CADENA ORIGINAL ===
$xsl = new DOMDocument();
$xsl->load($rutaXSLT);
$proc = new XSLTProcessor();
$proc->importStylesheet($xsl);
$cadenaOriginal = normalizarCadenaOriginal($proc->transformToXML($xml));
file_put_contents($rutaCadena, $cadenaOriginal);
// === SELLO DIGITAL ===
$privKeyRaw = file_get_contents($rutaKey);
$privKey = openssl_pkey_get_private($privKeyRaw);
if (!$privKey) {
die("ERROR: La llave privada no es válida o la ruta es incorrecta:\n" . openssl_error_string());
}
if (!openssl_sign($cadenaOriginal, $sello, $privKey, OPENSSL_ALGO_SHA256)) {
die("ERROR al generar el sello:\n" . openssl_error_string());
}
if (openssl_verify($cadenaOriginal, $sello, $certPem, OPENSSL_ALGO_SHA256) !== 1) {
die("ERROR: El sello generado no coincide con el certificado configurado.\n");
}
$selloBase64 = base64_encode($sello);
$comprobante->setAttribute("Sello", $selloBase64);
// === FINALIZAR Y GUARDAR ===
$xml->save($rutaXML);
echo "Archivo generado con éxito en: $rutaXML\n";
echo "Cadena Original guardada en: $rutaCadena\n";
?>