Están obligados a utilizar este complemento los contribuyentes que, conforme a la regla 2.6.1.1, fracción II, realicen la enajenación de gasolinas o diésel; en estos casos, deberán integrarlo en los CFDI que emitan por dichas operaciones.
Este complemento aplica específicamente cuando se facturan productos identificados con las siguientes claves de producto o servicio.
| Evento | Fecha |
|---|---|
| Publicación en el SAT | 25 de marzo de 2026 |
| Publicación en Ambiente DEMO | 27 de marzo de 2026 |
| Inicio de Vigencia y en Ambiente productivo | 24 de abril de 2026 |
| Número de permiso | RFC | Clave |
|---|---|---|
| PL/359/EXP/ES/2015 | CACX7605101P8 | 15101514 |
| PL/359/EXP/ES/2015 | CACX7605101P8 | 15101515 |
| PL/359/EXP/ES/2015 | CACX7605101P8 | 15101505 |
| PL/9998/EXP/ES/2015 | FUNK671228PH6 | 15101514 |
| PL/9998/EXP/ES/2015 | FUNK671228PH6 | 15101515 |
| PL/361/EXP/ES/2015 | IAÑL750210963 | 15101514 |
| H/36212/COM/2015 | MISC491214B86 | 15101505 |
| H/23421/COM/2020 | JUFA7608212V6 | 15101515 |
| PL/364/DIS/OM/2015 | EKU9003173C9 | 15101514 |
| PL/364/DIS/OM/2015 | EKU9003173C9 | 15101515 |
| PL/364/DIS/OM/2015 | EKU9003173C9 | 15101505 |
| PL/365/DIS/OM/2015 | EWE1709045U0 | 15101514 |
| PL/365/DIS/OM/2015 | EWE1709045U0 | 15101515 |
| PL/19935/DIS/OM/2017 | H&E951128469 | 15101514 |
| PL/367/EXP/ES/MM/2015 | XIA190128J61 | 15101505 |
| PL/1035/EXP/ES/MM/2015 | KIJ0906199R1 | 15101515 |
| PL/1055/EXP/ES/MM/2015 | OÑO120726RX3 | 15101515 |
| CNE/PL/1456/EXP/ES/2025 | KAHO641101B39 | 15101515 |
| CNE/H/207519/COM/2025 | KICR630120NX3 | 15101505 |
| CNE/PL/188367/DIS/OM/2025 | WERX631016S30 | 15101514 |
| CNE/PL/10352/EXP/ES/MM/2025 | XOJI740919U48 | 15101515 |
| CNE/PL/10352/EXP/ES/MM/2025 | XOJI740919U48 | 15101514 |
En el siguiente enlace encontrara la documentación referente al comprobante con complemento de Instituciones educativas privadas.
| Nombre | Documento |
|---|---|
| Estándar del complemento: | Descargar |
| Esquema XSD: | Descargar |
| Utilería XSLT: | Descargar |
| Catalagos: | Descargar |
<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/4" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:hidrocarburospetroliferos="http://www.sat.gob.mx/hidrocarburospetroliferos" xmlns:gasolinasdiesel="http://www.sat.gob.mx/" Certificado="" Exportacion="01" Fecha="2026-03-26T14:03:23" Folio="1432" FormaPago="02" LugarExpedicion="26000" MetodoPago="PUE" Moneda="MXN" NoCertificado="" Sello="" Serie="PREDIAL" SubTotal="100.00" TipoDeComprobante="I" Total="100.00" Version="4.0" 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/hidrocarburospetroliferos http://www.sat.gob.mx/sitio_internet/cfd/hidrocarburospetroliferos.xsd">
<cfdi:Emisor Nombre="ESCUELA KEMPER URGATE" RegimenFiscal="601" Rfc="EKU9003173C9"/>
<cfdi:Receptor DomicilioFiscalReceptor="86400" Nombre="FELIX MANUEL ANDRADE BALLADO" RegimenFiscalReceptor="616" Rfc="AABF800614HI0" UsoCFDI="S01"/>
<cfdi:Conceptos>
<cfdi:Concepto Cantidad="1" ClaveProdServ="15101514" ClaveUnidad="LTR" Descripcion="Gasolina regular menor a 91 octanos" Importe="100.00" ObjetoImp="01" Unidad="No aplica" ValorUnitario="100.00">
<cfdi:ComplementoConcepto>
<hidrocarburospetroliferos:HidroYPetro Version="1.0" TipoPermiso="PER01" NumeroPermiso="PL/359/EXP/ES/2015" ClaveHYP="15101514" SubProductoHYP="SP16"/>
</cfdi:ComplementoConcepto>
</cfdi:Concepto>
</cfdi:Conceptos>
</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_HYP = "http://www.sat.gob.mx/hidrocarburospetroliferos"
NS_XSI = "http://www.w3.org/2001/XMLSchema-instance"
DATOS = {
"Comprobante": {
"Version": "4.0",
"Serie": "PREDIAL",
"Folio": "1432",
"Fecha": "2026-03-26T14:03:23",
"SubTotal": "100.00",
"Moneda": "MXN",
"Total": "100.00",
"TipoDeComprobante": "I",
"Exportacion": "01",
"MetodoPago": "PUE",
"FormaPago": "02",
"LugarExpedicion": "26000",
},
"Emisor": {
"Rfc": "EKU9003173C9",
"Nombre": "ESCUELA KEMPER URGATE",
"RegimenFiscal": "601",
},
"Receptor": {
"Rfc": "AABF800614HI0",
"Nombre": "FELIX MANUEL ANDRADE BALLADO",
"DomicilioFiscalReceptor": "86400",
"RegimenFiscalReceptor": "616",
"UsoCFDI": "S01",
},
"Concepto": {
"ClaveProdServ": "15101505",
"ClaveUnidad": "LTR",
"Cantidad": "1",
"Unidad": "No aplica",
"Descripcion": "Diesel",
"ValorUnitario": "100.00",
"Importe": "100.00",
"ObjetoImp": "01",
},
"HidroYPetro": {
"Version": "1.0",
"TipoPermiso": "PER01",
"NumeroPermiso": "PL/364/DIS/OM/2015",
"ClaveHYP": "15101505",
"SubProductoHYP": "SP18",
},
}
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:
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_HYP} http://www.sat.gob.mx/sitio_internet/cfd/hidrocarburospetroliferos.xsd"
),
}
root = etree.Element(
f"{{{NS_CFDI}}}Comprobante",
root_attrs,
nsmap={
"cfdi": NS_CFDI,
"hidrocarburospetroliferos": NS_HYP,
"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"])
complemento_concepto = etree.SubElement(concepto, f"{{{NS_CFDI}}}ComplementoConcepto")
etree.SubElement(
complemento_concepto,
f"{{{NS_HYP}}}HidroYPetro",
**DATOS["HidroYPetro"],
)
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/CFDI40_HidrocarburosPetroliferos.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 Hidrocarburos y Petroliferos 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_HYP = 'http://www.sat.gob.mx/hidrocarburospetroliferos';
const NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance';
$DATA = [
'Comprobante' => [
'Version' => '4.0',
'Serie' => 'PREDIAL',
'Folio' => '1432',
'Fecha' => '2026-03-26T14:03:23',
'SubTotal' => '100.00',
'Moneda' => 'MXN',
'Total' => '100.00',
'TipoDeComprobante' => 'I',
'Exportacion' => '01',
'MetodoPago' => 'PUE',
'FormaPago' => '02',
'LugarExpedicion' => '26000',
],
'Emisor' => [
'Rfc' => 'EKU9003173C9',
'Nombre' => 'ESCUELA KEMPER URGATE',
'RegimenFiscal' => '601',
],
'Receptor' => [
'Rfc' => 'AABF800614HI0',
'Nombre' => 'FELIX MANUEL ANDRADE BALLADO',
'DomicilioFiscalReceptor' => '86400',
'RegimenFiscalReceptor' => '616',
'UsoCFDI' => 'S01',
],
'Concepto' => [
'ClaveProdServ' => '15101505',
'ClaveUnidad' => 'LTR',
'Cantidad' => '1',
'Unidad' => 'No aplica',
'Descripcion' => 'Diesel',
'ValorUnitario' => '100.00',
'Importe' => '100.00',
'ObjetoImp' => '01',
],
'HidroYPetro' => [
'Version' => '1.0',
'TipoPermiso' => 'PER01',
'NumeroPermiso' => 'PL/364/DIS/OM/2015',
'ClaveHYP' => '15101505',
'SubProductoHYP' => 'SP18',
],
];
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 generarXmlHidrocarburosPetroliferos(
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:hidrocarburospetroliferos', NS_HYP);
$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_HYP . ' http://www.sat.gob.mx/sitio_internet/cfd/hidrocarburospetroliferos.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');
$root->appendChild($conceptos);
$concepto = $doc->createElementNS(NS_CFDI, 'cfdi:Concepto');
setAttrs($concepto, $data['Concepto']);
$conceptos->appendChild($concepto);
$complementoConcepto = $doc->createElementNS(NS_CFDI, 'cfdi:ComplementoConcepto');
$concepto->appendChild($complementoConcepto);
$hidroYPetro = $doc->createElementNS(NS_HYP, 'hidrocarburospetroliferos:HidroYPetro');
setAttrs($hidroYPetro, $data['HidroYPetro']);
$complementoConcepto->appendChild($hidroYPetro);
$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 = dirname(__DIR__);
$rutaSalidaDefault = $base . '/xml/CFDI40__HidrocarburosPetroliferos.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 . '/xslt/cfdi/v40/cadenaoriginal.xslt';
try {
$archivo = generarXmlHidrocarburosPetroliferos($rutaSalida, $rutaCer, $rutaKey, $rutaXslt, $DATA);
echo "XML Hidrocarburos y Petroliferos 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);
}