De acuerdo a la documentación del SAT, a partir del 1 de mayo de 2016 este complemento es utilizado por todos los contribuyentes que vendan, enajenen, arrienden o proporcionen bienes o servicios de manera onerosa a los Partidos Políticos y Asociaciones Civiles (tratándose de aspirantes y candidatos independientes), mismo que deberá incorporarse en los CFDI que emitan a favor de dichos partidos y asociaciones en los procesos de precampaña y campaña, cuando se trate de la adquisición o contratación de propaganda, incluyendo la utilitaria y publicidad, así como de espectáculos, cantantes y grupos musicales, bienes y servicios contratados para la realización de eventos de precampaña y campaña sin importar el monto, lo anterior de conformidad con el artículo 46 del Reglamento de Fiscalización del Instituto Nacional Electoral.
Enlace directo al sitio del SAT. liga
En el siguiente liga podrá encontrar toda la información oficial relacionada con el complemento de INE
| Nombre | Documento |
|---|---|
| Estándar del complemento: | 📥 Descargar |
| Esquema XSD: | 📥 Descargar |
| Utilería XSLT: | 📥 Descargar |
| Catálogo: | 📥 Descargar |
| Matriz de errores: | 📥 Descargar |
| Nombre | Dirección |
|---|---|
| namespace: | http://www.sat.gob.mx/ine |
| Location: | http://www.sat.gob.mx/sitio_internet/cfd/ine/ine11.xsd |
| Uso: | 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/ine http://www.sat.gob.mx/sitio_internet/cfd/ine/ine11.xsd“ |
<?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:ine="http://www.sat.gob.mx/ine" 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/ine http://www.sat.gob.mx/sitio_internet/cfd/ine/ine11.xsd" Version="4.0" Fecha="2026-03-24T00:00:43" Serie="Serie" Folio="23100" FormaPago="01" CondicionesDePago="condiciones" SubTotal="100.00" Moneda="MXN" Exportacion="01" TipoCambio="1" Total="114.00" TipoDeComprobante="I" MetodoPago="PUE" LugarExpedicion="45070" 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=" NoCertificado="30001000000500003416" Sello="kV2KrbwYvApv4MXXait5JkJ6ugtI8J1LBdSKoKz7TUs1SDXPsVT8i+CC5+RCioVouiECSY/2upT1ehtMiu+PEKt0expcXQA75+OQR1ADhv2wZK+s9P8aLSFLu+vmVkPcEQRkOvyKWmUXJGLUaHQPiuxk+EEGF10Vbe0Qmm+vwRnnXjIeo5Z1Yks1btiCowf31bx7wqZQHXJxXmPRVnDzOcHloAMqj3MnwF/DviiOOsND8ZPlUuXUq8pQZKu33VQUlcFr3RhxDAcqnWAI8+O78pklQa0r7jJCk9Up4uBaxgI+s16X5Qsw5LqCIgtS77lOc5TzixT57dunGsDcr5HDKQ==">
<cfdi:Emisor Rfc="EKU9003173C9" Nombre="ESCUELA KEMPER URGATE" RegimenFiscal="601" />
<cfdi:Receptor Rfc="AABF800614HI0" Nombre="FELIX MANUEL ANDRADE BALLADO" UsoCFDI="S01" DomicilioFiscalReceptor="86400" RegimenFiscalReceptor="616" />
<cfdi:Conceptos>
<cfdi:Concepto Cantidad="1.000000" Unidad="PZA" Descripcion="Test" ValorUnitario="100.00" Importe="100.00" ClaveProdServ="01010101" ClaveUnidad="EA" ObjetoImp="02">
<cfdi:Impuestos>
<cfdi:Traslados>
<cfdi:Traslado Base="100.00" Impuesto="002" TipoFactor="Tasa" TasaOCuota="0.160000" Importe="16.00" />
</cfdi:Traslados>
<cfdi:Retenciones>
<cfdi:Retencion Base="100.00" Impuesto="002" TipoFactor="Tasa" TasaOCuota="0.020000" Importe="2.00" />
</cfdi:Retenciones>
</cfdi:Impuestos>
</cfdi:Concepto>
</cfdi:Conceptos>
<cfdi:Impuestos TotalImpuestosRetenidos="2.00" TotalImpuestosTrasladados="16.00">
<cfdi:Retenciones>
<cfdi:Retencion Impuesto="002" Importe="2.00" />
</cfdi:Retenciones>
<cfdi:Traslados>
<cfdi:Traslado Base="100.00" Impuesto="002" Importe="16.00" TasaOCuota="0.160000" TipoFactor="Tasa" />
</cfdi:Traslados>
</cfdi:Impuestos>
<cfdi:Complemento>
<ine:INE Version="1.1" TipoProceso="Ordinario" TipoComite="Ejecutivo Nacional" />
</cfdi:Complemento>
</cfdi:Comprobante>
| Descripción | código |
|---|---|
| Si atributo TipoProceso = “Ordinario”, debe existir el atributo ine:TipoComite | 180 |
| Si atributo TipoProceso = “Precampaña o Campaña”, debe existir al menos un elemento Entidad:Ambito | 181 |
| Si atributo TipoProceso = “Precampaña o Campaña”, no debe existir el atributo ine:TipoComite | 182 |
| Si atributo TipoProceso = “Precampaña o Campaña”, no debe existir el atributo ine:TipoComite | 183 |
| Si atributo TipoComite = “Ejecutivo Nacional”, no debe existir ningún elemento ine:Entidad | 184 |
| Si atributo TipoComite = “Ejecutivo Estatal”, no debe existir ine:IdContabilidad | 185 |
| Si atributo TipoComite = “Ejecutivo Estatal”, debe existir al menos un elemento ine:Entidad y no debe existir ine:Entidad:Ambito | 186 |
| Elemento Entidad, no se debe repetir la combinación de ine:Entidad:ClaveEntidad con ine:Entidad:Ambito | 187 |
| Si atributo Ambito = “Local”, no se pueden seleccionar las claves NAC, CR1, CR2, CR3, CR4 y CR5 por que el Ambito es Local | 188 |
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import base64
import hashlib
import os
import subprocess
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_INE = "http://www.sat.gob.mx/ine"
NS_XSI = "http://www.w3.org/2001/XMLSchema-instance"
DATOS = {
"Comprobante": {
"Version": "4.0",
"Serie": "Serie",
"Folio": "23100",
"Fecha": "2026-03-24T00:00:43",
"FormaPago": "01",
"SubTotal": "100.00",
"Moneda": "MXN",
"TipoCambio": "1",
"Total": "114.00",
"TipoDeComprobante": "I",
"Exportacion": "01",
"MetodoPago": "PUE",
"LugarExpedicion": "45070",
"CondicionesDePago": "condiciones",
},
"Emisor": {
"Rfc": "EKU9003173C9",
"Nombre": "ESCUELA KEMPER URGATE",
"RegimenFiscal": "601",
},
"Receptor": {
"Rfc": "AABF800614HI0",
"Nombre": "FELIX MANUEL ANDRADE BALLADO",
"UsoCFDI": "S01",
"DomicilioFiscalReceptor": "86400",
"RegimenFiscalReceptor": "616",
},
"Concepto": {
"Cantidad": "1.000000",
"Unidad": "PZA",
"Descripcion": "Test",
"ValorUnitario": "100.00",
"Importe": "100.00",
"ClaveProdServ": "01010101",
"ClaveUnidad": "EA",
"ObjetoImp": "02",
},
"TrasladoConcepto": {
"Base": "100.00",
"Impuesto": "002",
"TipoFactor": "Tasa",
"TasaOCuota": "0.160000",
"Importe": "16.00",
},
"RetencionConcepto": {
"Base": "100.00",
"Impuesto": "002",
"TipoFactor": "Tasa",
"TasaOCuota": "0.020000",
"Importe": "2.00",
},
"Impuestos": {
"TotalImpuestosRetenidos": "2.00",
"TotalImpuestosTrasladados": "16.00",
},
"RetencionGlobal": {
"Impuesto": "002",
"Importe": "2.00",
},
"TrasladoGlobal": {
"Base": "100.00",
"Impuesto": "002",
"Importe": "16.00",
"TasaOCuota": "0.160000",
"TipoFactor": "Tasa",
},
"INE": {
"Version": "1.1",
"TipoProceso": "Ordinario",
"TipoComite": "Ejecutivo Nacional",
},
}
def _leer_certificado(cer_path: Path) -> tuple[str, str]:
cert_bytes = cer_path.read_bytes()
cert_b64 = base64.b64encode(cert_bytes).decode("utf-8").replace("\n", "").replace("\r", "")
serial_raw = subprocess.check_output(
["openssl", "x509", "-inform", "DER", "-in", str(cer_path), "-noout", "-serial"],
text=True,
).strip()
serial_hex = serial_raw.split("=", 1)[1].strip()
no_certificado = bytes.fromhex(serial_hex).decode("latin1").upper()
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("ine", NS_INE)
etree.register_namespace("xsi", NS_XSI)
cert_b64, no_certificado = _leer_certificado(cer_path)
root_attrs = {
**DATOS["Comprobante"],
"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_INE} http://www.sat.gob.mx/sitio_internet/cfd/ine/ine11.xsd"
),
}
root = etree.Element(
f"{{{NS_CFDI}}}Comprobante",
root_attrs,
nsmap={
"cfdi": NS_CFDI,
"ine": NS_INE,
"xsi": NS_XSI,
},
)
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 = etree.SubElement(conceptos, f"{{{NS_CFDI}}}Concepto", **DATOS["Concepto"])
imp_concepto = etree.SubElement(concepto, f"{{{NS_CFDI}}}Impuestos")
traslados_concepto = etree.SubElement(imp_concepto, f"{{{NS_CFDI}}}Traslados")
etree.SubElement(traslados_concepto, f"{{{NS_CFDI}}}Traslado", **DATOS["TrasladoConcepto"])
retenciones_concepto = etree.SubElement(imp_concepto, f"{{{NS_CFDI}}}Retenciones")
etree.SubElement(retenciones_concepto, f"{{{NS_CFDI}}}Retencion", **DATOS["RetencionConcepto"])
imp_global = etree.SubElement(root, f"{{{NS_CFDI}}}Impuestos", **DATOS["Impuestos"])
retenciones_global = etree.SubElement(imp_global, f"{{{NS_CFDI}}}Retenciones")
etree.SubElement(retenciones_global, f"{{{NS_CFDI}}}Retencion", **DATOS["RetencionGlobal"])
traslados_global = etree.SubElement(imp_global, f"{{{NS_CFDI}}}Traslados")
etree.SubElement(traslados_global, f"{{{NS_CFDI}}}Traslado", **DATOS["TrasladoGlobal"])
complemento = etree.SubElement(root, f"{{{NS_CFDI}}}Complemento")
etree.SubElement(complemento, f"{{{NS_INE}}}INE", **DATOS["INE"])
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(os.getenv("CFDI_OUTPUT_PATH", "xml/INE11.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 INE generado: {archivo}")
if os.getenv("SYNC_EDI_CONTENT", "").strip() == "1":
destino = Path("xml/edi_content.xml")
destino.write_bytes(archivo.read_bytes())
print(f"XML sincronizado para timbrado en: {destino}")
<?php
declare(strict_types=1);
date_default_timezone_set('America/Mexico_City');
const NS_CFDI = 'http://www.sat.gob.mx/cfd/4';
const NS_INE = 'http://www.sat.gob.mx/ine';
const NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance';
$DATA = [
'Comprobante' => [
'Version' => '4.0',
'Serie' => 'Serie',
'Folio' => '23100',
'Fecha' => '2026-03-24T00:00:43',
'FormaPago' => '01',
'SubTotal' => '100.00',
'Moneda' => 'MXN',
'TipoCambio' => '1',
'Total' => '114.00',
'TipoDeComprobante' => 'I',
'Exportacion' => '01',
'MetodoPago' => 'PUE',
'LugarExpedicion' => '45070',
'CondicionesDePago' => 'condiciones',
],
'Emisor' => [
'Rfc' => 'EKU9003173C9',
'Nombre' => 'ESCUELA KEMPER URGATE',
'RegimenFiscal' => '601',
],
'Receptor' => [
'Rfc' => 'AABF800614HI0',
'Nombre' => 'FELIX MANUEL ANDRADE BALLADO',
'UsoCFDI' => 'S01',
'DomicilioFiscalReceptor' => '86400',
'RegimenFiscalReceptor' => '616',
],
'Concepto' => [
'Cantidad' => '1.000000',
'Unidad' => 'PZA',
'Descripcion' => 'Test',
'ValorUnitario' => '100.00',
'Importe' => '100.00',
'ClaveProdServ' => '01010101',
'ClaveUnidad' => 'EA',
'ObjetoImp' => '02',
],
'TrasladoConcepto' => [
'Base' => '100.00',
'Impuesto' => '002',
'TipoFactor' => 'Tasa',
'TasaOCuota' => '0.160000',
'Importe' => '16.00',
],
'RetencionConcepto' => [
'Base' => '100.00',
'Impuesto' => '002',
'TipoFactor' => 'Tasa',
'TasaOCuota' => '0.020000',
'Importe' => '2.00',
],
'Impuestos' => [
'TotalImpuestosRetenidos' => '2.00',
'TotalImpuestosTrasladados' => '16.00',
],
'RetencionGlobal' => [
'Impuesto' => '002',
'Importe' => '2.00',
],
'TrasladoGlobal' => [
'Base' => '100.00',
'Impuesto' => '002',
'Importe' => '16.00',
'TasaOCuota' => '0.160000',
'TipoFactor' => 'Tasa',
],
'INE' => [
'Version' => '1.1',
'TipoProceso' => 'Ordinario',
'TipoComite' => 'Ejecutivo Nacional',
],
];
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 generarXmlINE(
string $outputPath,
string $cerPath,
string $keyPath,
string $xsltPath,
array $data
): string {
[$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:ine', NS_INE);
$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_INE . ' http://www.sat.gob.mx/sitio_internet/cfd/ine/ine11.xsd'
);
setAttrs($root, $data['Comprobante']);
$root->setAttribute('NoCertificado', $noCertificado);
$root->setAttribute('Certificado', $certB64);
$root->setAttribute('Sello', '');
$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');
$concepto = $doc->createElementNS(NS_CFDI, 'cfdi:Concepto');
setAttrs($concepto, $data['Concepto']);
$impConcepto = $doc->createElementNS(NS_CFDI, 'cfdi:Impuestos');
$trasladosConcepto = $doc->createElementNS(NS_CFDI, 'cfdi:Traslados');
$trasladoConcepto = $doc->createElementNS(NS_CFDI, 'cfdi:Traslado');
setAttrs($trasladoConcepto, $data['TrasladoConcepto']);
$trasladosConcepto->appendChild($trasladoConcepto);
$impConcepto->appendChild($trasladosConcepto);
$retencionesConcepto = $doc->createElementNS(NS_CFDI, 'cfdi:Retenciones');
$retencionConcepto = $doc->createElementNS(NS_CFDI, 'cfdi:Retencion');
setAttrs($retencionConcepto, $data['RetencionConcepto']);
$retencionesConcepto->appendChild($retencionConcepto);
$impConcepto->appendChild($retencionesConcepto);
$concepto->appendChild($impConcepto);
$conceptos->appendChild($concepto);
$root->appendChild($conceptos);
$impGlobal = $doc->createElementNS(NS_CFDI, 'cfdi:Impuestos');
setAttrs($impGlobal, $data['Impuestos']);
$retencionesGlobal = $doc->createElementNS(NS_CFDI, 'cfdi:Retenciones');
$retencionGlobal = $doc->createElementNS(NS_CFDI, 'cfdi:Retencion');
setAttrs($retencionGlobal, $data['RetencionGlobal']);
$retencionesGlobal->appendChild($retencionGlobal);
$impGlobal->appendChild($retencionesGlobal);
$trasladosGlobal = $doc->createElementNS(NS_CFDI, 'cfdi:Traslados');
$trasladoGlobal = $doc->createElementNS(NS_CFDI, 'cfdi:Traslado');
setAttrs($trasladoGlobal, $data['TrasladoGlobal']);
$trasladosGlobal->appendChild($trasladoGlobal);
$impGlobal->appendChild($trasladosGlobal);
$root->appendChild($impGlobal);
$complemento = $doc->createElementNS(NS_CFDI, 'cfdi:Complemento');
$ine = $doc->createElementNS(NS_INE, 'ine:INE');
setAttrs($ine, $data['INE']);
$complemento->appendChild($ine);
$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);
$sello = crearSello($cadena, $keyPath);
$sellado = new DOMDocument();
$sellado->formatOutput = true;
$sellado->load($outputPath);
$sellado->documentElement->setAttribute('Sello', $sello);
$sellado->save($outputPath);
return $outputPath;
}
$base = __DIR__;
$rutaSalidaDefault = $base . '/xml/INE11.xml';
$rutaSalidaEnv = getenv('CFDI_OUTPUT_PATH');
$rutaSalida = ($rutaSalidaEnv !== false && trim($rutaSalidaEnv) !== '')
? trim($rutaSalidaEnv)
: $rutaSalidaDefault;
$rutaCer = $base . '/certificados/EKU9003173C9/EKU9003173C9.cer';
$rutaKey = $base . '/certificados/EKU9003173C9/EKU9003173C9.pem';
$rutaXslt = $base . '/cadenaoriginal_4_0.xslt';
try {
$archivo = generarXmlINE($rutaSalida, $rutaCer, $rutaKey, $rutaXslt, $DATA);
echo "XML INE generado: {$archivo}\n";
$syncEdiContent = getenv('SYNC_EDI_CONTENT');
if ($syncEdiContent !== false && trim($syncEdiContent) === '1') {
$rutaEdiContent = $base . '/xml/edi_content.xml';
if (!copy($archivo, $rutaEdiContent)) {
throw new RuntimeException("No se pudo sincronizar XML a: {$rutaEdiContent}");
}
echo "XML sincronizado para timbrado en: {$rutaEdiContent}\n";
}
} catch (Throwable $e) {
fwrite(STDERR, "ERROR: " . $e->getMessage() . "\n");
exit(1);
}