El complemento facilita la incorporación de estos datos en la factura, tales como impuesto sobre erogaciones al trabajo personal, impuesto sobre adquisición de inmuebles y otras operaciones traslativas de dominio, impuesto sobre conjuntos urbanos, impuesto sobre anuncios publicitarios, impuestos locales (sobre alojamiento o servicios de hospedaje) retención de impuestos federales (IEPS y 5 al millar que dispone el artículo 191 de la Ley Federal de Derechos para facturas de obra pública) entre otros.
Enlace directo al sitio del SAT. liga
En el siguiente. liga encontrará la documentación necesaria para generar el comprobante con complemento de impuestos locales.
| Nombre | Documento |
|---|---|
| Estándar del complemento: | 📥 Descargar |
| Esquema XSD: | 📥 Descargar |
| Utilería XSLT: | 📥 Descargar |
| Nombre | Dirección |
|---|---|
| namespace: | http://www.sat.gob.mx/implocal |
| Location: | http://www.sat.gob.mx/sitio_internet/cfd/implocal/implocal.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/implocal http://www.sat.gob.mx/sitio_internet/cfd/implocal/implocal.xsd“ |
<?xml version="1.0" encoding="utf-8"?>
<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/4"
xmlns:implocal="http://www.sat.gob.mx/implocal"
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/implocal http://www.sat.gob.mx/sitio_internet/cfd/implocal/implocal.xsd" 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="
Version="4.0" Serie="Serie" Folio="Folio" Fecha="2026-03-24T00:00:00"
Sello="ZsVcQn2KntsESFEviBK9wG39a3n71zGB1P/bA6PS3l2Zw/vJaceTUG+lVGNpKZVkJi21/WGaapPxtSQfFrTfYrVaEGIIhAuIHG4aPxfJ4jImZGRsxqgVGuYxwpjjp9LH7f3K+9GSGJzWv3dwb1BV1sytBvtxG0B0RUiC1jlt1VAM8RJthDLA8hSeOimYe3F71DTz6w9JEcZFlEE0ksSOk34US3RXLhPVLljVTJNCA93LWE8hjun9OViJQQEEP+ns1cE+cqmlbkuu7BajegCfsLi4j8HQSD+vmNMWlfd2Lf874DOjw0b2xJ4xtNl9j3YDTNAuHRJXkokH3mkm1pnHqA=="
CondicionesDePago="CondicionesDePago" SubTotal="120.00" Moneda="MXN" TipoCambio="1"
Total="163.20" TipoDeComprobante="I" Exportacion="01" MetodoPago="PPD" FormaPago="99"
LugarExpedicion="20000">
<cfdi:Emisor Rfc="EKU9003173C9" Nombre="ESCUELA KEMPER URGATE" RegimenFiscal="601" />
<cfdi:Receptor Rfc="MASO451221PM4" Nombre="MARIA OLIVIA MARTINEZ SAGAZ" DomicilioFiscalReceptor="80290"
RegimenFiscalReceptor="616" UsoCFDI="S01" />
<cfdi:Conceptos>
<cfdi:Concepto ClaveProdServ="90111501" Cantidad="1" ClaveUnidad="E48"
Descripcion="Habitacion" ValorUnitario="120.00" Importe="120.00" ObjetoImp="02">
<cfdi:Impuestos>
<cfdi:Traslados>
<cfdi:Traslado Base="120.00" Importe="19.20" Impuesto="002"
TasaOCuota="0.160000" TipoFactor="Tasa" />
</cfdi:Traslados>
</cfdi:Impuestos>
</cfdi:Concepto>
</cfdi:Conceptos>
<cfdi:Impuestos TotalImpuestosTrasladados="19.20">
<cfdi:Traslados>
<cfdi:Traslado Base="120.00" Importe="19.20" Impuesto="002" TasaOCuota="0.160000"
TipoFactor="Tasa" />
</cfdi:Traslados>
</cfdi:Impuestos>
<cfdi:Complemento>
<implocal:ImpuestosLocales version="1.0" TotaldeRetenciones="0.00" TotaldeTraslados="24.00">
<implocal:TrasladosLocales ImpLocTrasladado="IMPUESTO DE HOSPEDAJE"
TasadeTraslado="2.00" Importe="24.00" />
</implocal:ImpuestosLocales>
</cfdi:Complemento>
</cfdi:Comprobante>
#!/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_IMPL = "http://www.sat.gob.mx/implocal"
NS_XSI = "http://www.w3.org/2001/XMLSchema-instance"
DATOS = {
"Comprobante": {
"Version": "4.0",
"Serie": "Serie",
"Folio": "Folio",
"Fecha": "2026-03-24T00:00:00",
"CondicionesDePago": "CondicionesDePago",
"SubTotal": "120.00",
"Moneda": "MXN",
"TipoCambio": "1",
"Total": "163.20",
"TipoDeComprobante": "I",
"Exportacion": "01",
"MetodoPago": "PPD",
"FormaPago": "99",
"LugarExpedicion": "20000",
},
"Emisor": {
"Rfc": "EKU9003173C9",
"Nombre": "ESCUELA KEMPER URGATE",
"RegimenFiscal": "601",
},
"Receptor": {
"Rfc": "MASO451221PM4",
"Nombre": "MARIA OLIVIA MARTINEZ SAGAZ",
"DomicilioFiscalReceptor": "80290",
"RegimenFiscalReceptor": "616",
"UsoCFDI": "S01",
},
"Concepto": {
"ClaveProdServ": "90111501",
"Cantidad": "1",
"ClaveUnidad": "E48",
"Descripcion": "Habitacion",
"ValorUnitario": "120.00",
"Importe": "120.00",
"ObjetoImp": "02",
},
"TrasladoConcepto": {
"Base": "120.00",
"Importe": "19.20",
"Impuesto": "002",
"TasaOCuota": "0.160000",
"TipoFactor": "Tasa",
},
"Impuestos": {
"TotalImpuestosTrasladados": "19.20",
},
"TrasladoGlobal": {
"Base": "120.00",
"Importe": "19.20",
"Impuesto": "002",
"TasaOCuota": "0.160000",
"TipoFactor": "Tasa",
},
"ImpuestosLocales": {
"version": "1.0",
"TotaldeRetenciones": "0.00",
"TotaldeTraslados": "24.00",
},
"TrasladosLocales": {
"ImpLocTrasladado": "IMPUESTO DE HOSPEDAJE",
"TasadeTraslado": "2.00",
"Importe": "24.00",
},
}
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("implocal", NS_IMPL)
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_IMPL} http://www.sat.gob.mx/sitio_internet/cfd/implocal/implocal.xsd"
),
}
root = etree.Element(
f"{{{NS_CFDI}}}Comprobante",
root_attrs,
nsmap={
"cfdi": NS_CFDI,
"implocal": NS_IMPL,
"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"])
imp_global = etree.SubElement(root, f"{{{NS_CFDI}}}Impuestos", **DATOS["Impuestos"])
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")
imp_locales = etree.SubElement(complemento, f"{{{NS_IMPL}}}ImpuestosLocales", **DATOS["ImpuestosLocales"])
etree.SubElement(imp_locales, f"{{{NS_IMPL}}}TrasladosLocales", **DATOS["TrasladosLocales"])
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)
etree.cleanup_namespaces(
tree.getroot(),
top_nsmap={"cfdi": NS_CFDI, "implocal": NS_IMPL, "xsi": NS_XSI},
)
tree.write(str(output_path), encoding="utf-8", pretty_print=True, xml_declaration=True)
# Evita redeclarar implocal en el nodo hijo cuando el serializer lo agrega.
xml_text = output_path.read_text(encoding="utf-8")
xml_text = xml_text.replace(
'<implocal:ImpuestosLocales xmlns:implocal="http://www.sat.gob.mx/implocal"',
'<implocal:ImpuestosLocales',
)
output_path.write_text(xml_text, encoding="utf-8")
return output_path
if __name__ == "__main__":
salida = Path(os.getenv("CFDI_OUTPUT_PATH", "xml/ImpuestosLocales10.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 Implocal 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_IMPL = 'http://www.sat.gob.mx/implocal';
const NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance';
$DATA = [
'Comprobante' => [
'Version' => '4.0',
'Serie' => 'Serie',
'Folio' => 'Folio',
'Fecha' => '2026-03-24T00:00:00',
'CondicionesDePago' => 'CondicionesDePago',
'SubTotal' => '120.00',
'Moneda' => 'MXN',
'TipoCambio' => '1',
'Total' => '163.20',
'TipoDeComprobante' => 'I',
'Exportacion' => '01',
'MetodoPago' => 'PPD',
'FormaPago' => '99',
'LugarExpedicion' => '20000',
],
'Emisor' => [
'Rfc' => 'EKU9003173C9',
'Nombre' => 'ESCUELA KEMPER URGATE',
'RegimenFiscal' => '601',
],
'Receptor' => [
'Rfc' => 'MASO451221PM4',
'Nombre' => 'MARIA OLIVIA MARTINEZ SAGAZ',
'DomicilioFiscalReceptor' => '80290',
'RegimenFiscalReceptor' => '616',
'UsoCFDI' => 'S01',
],
'Concepto' => [
'ClaveProdServ' => '90111501',
'Cantidad' => '1',
'ClaveUnidad' => 'E48',
'Descripcion' => 'Habitacion',
'ValorUnitario' => '120.00',
'Importe' => '120.00',
'ObjetoImp' => '02',
],
'TrasladoConcepto' => [
'Base' => '120.00',
'Importe' => '19.20',
'Impuesto' => '002',
'TasaOCuota' => '0.160000',
'TipoFactor' => 'Tasa',
],
'Impuestos' => [
'TotalImpuestosTrasladados' => '19.20',
],
'TrasladoGlobal' => [
'Base' => '120.00',
'Importe' => '19.20',
'Impuesto' => '002',
'TasaOCuota' => '0.160000',
'TipoFactor' => 'Tasa',
],
'ImpuestosLocales' => [
'version' => '1.0',
'TotaldeRetenciones' => '0.00',
'TotaldeTraslados' => '24.00',
],
'TrasladosLocales' => [
'ImpLocTrasladado' => 'IMPUESTO DE HOSPEDAJE',
'TasadeTraslado' => '2.00',
'Importe' => '24.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 generarXmlImplocal(
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:implocal', NS_IMPL);
$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_IMPL . ' http://www.sat.gob.mx/sitio_internet/cfd/implocal/implocal.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);
$concepto->appendChild($impConcepto);
$conceptos->appendChild($concepto);
$root->appendChild($conceptos);
$impGlobal = $doc->createElementNS(NS_CFDI, 'cfdi:Impuestos');
setAttrs($impGlobal, $data['Impuestos']);
$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');
$impLocales = $doc->createElementNS(NS_IMPL, 'implocal:ImpuestosLocales');
setAttrs($impLocales, $data['ImpuestosLocales']);
$trasladoLocal = $doc->createElementNS(NS_IMPL, 'implocal:TrasladosLocales');
setAttrs($trasladoLocal, $data['TrasladosLocales']);
$impLocales->appendChild($trasladoLocal);
$complemento->appendChild($impLocales);
$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/ImpuestosLocales10.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 = generarXmlImplocal($rutaSalida, $rutaCer, $rutaKey, $rutaXslt, $DATA);
// Normaliza namespaces: evita redeclarar implocal en nodos hijos.
$normalizado = new DOMDocument();
$normalizado->preserveWhiteSpace = false;
$normalizado->formatOutput = true;
$normalizado->load($archivo);
$xmlnsNs = 'http://www.w3.org/2000/xmlns/';
$hijos = $normalizado->getElementsByTagName('*');
foreach ($hijos as $nodo) {
if ($nodo->parentNode !== $normalizado->documentElement
&& $nodo->hasAttributeNS($xmlnsNs, 'implocal')) {
$nodo->removeAttributeNS($xmlnsNs, 'implocal');
}
}
$normalizado->save($archivo);
$xmlFinal = file_get_contents($archivo);
if ($xmlFinal === false) {
throw new RuntimeException("No se pudo leer XML generado para normalizar namespaces: {$archivo}");
}
$xmlFinal = str_replace(
'<implocal:ImpuestosLocales xmlns:implocal="http://www.sat.gob.mx/implocal"',
'<implocal:ImpuestosLocales',
$xmlFinal
);
if (file_put_contents($archivo, $xmlFinal) === false) {
throw new RuntimeException("No se pudo guardar XML normalizado: {$archivo}");
}
echo "XML Implocal 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);
}