Cierra el ciclo de la factura: construcción del comprobante, sellado con el CSD de la empresa emisora y transmisión al PAC. - cfdi_builder: XML 4.0 de ingreso en el orden de atributos del XSD, del que depende la cadena original y con ella el sello. Todo el dinero con Decimal. - sealer: cadena original vía el XSLT oficial del SAT y firma con la llave del CSD. - pac_comercio_digital: cliente de timbrarV5. Conserva el código y el saldo de folios que el legado leía en una variable que descartaba (CFDI.cs:19324-19336). - csd_service y core/crypto: CSD por empresa, con la contraseña cifrada en la base. Antes el certificado había que dejarlo a mano en el almacenamiento y su contraseña era una variable de entorno global, lo que no funciona con varias empresas emisoras. - Cada intento —también los rechazados— guarda el XML que se transmitió y el que contestó el PAC: sin ese par no hay forma de reconstruir un rechazo cuando termina la petición. La declaración XML se escribe a mano con comillas dobles. lxml la emite con comillas simples, que es XML válido, pero Comercio Digital compara la cadena literal version="1.0" y responde 642 "la versión del XML no es 1.0". El modo (pruebas o producción) sale de invoices.stamping_mode y no se puede pasar por la API: es lo único que separa un timbre de prueba de un CFDI con validez fiscal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
XSLT de la cadena original — CFDI 4.0
La cadena original es la secuencia de datos que se firma para producir el sello del comprobante. Sólo se obtiene aplicando la transformación oficial del SAT: no se construye a mano.
| Archivo | Origen |
|---|---|
cadenaoriginal_4_0.xslt |
http://www.sat.gob.mx/sitio_internet/cfd/4/cadenaoriginal_4_0/cadenaoriginal_4_0.xslt |
utilerias.xslt |
http://www.sat.gob.mx/sitio_internet/cfd/2/cadenaoriginal_2_0/utilerias.xslt |
sin_complemento.xslt |
Nuestro. Marcador de posición, ver abajo. |
La única modificación: los href de los xsl:include
El archivo del SAT trae 33 xsl:include apuntando a URLs de sat.gob.mx. Se reescribieron los
href a rutas relativas locales; no se tocó ni una plantilla, ni un xsl:template, ni el orden
de los campos.
- El include de
utilerias.xsltapunta al archivo local del mismo nombre. - Los otros 32, todos de complementos (Carta Porte, Comercio Exterior, Nómina, Pagos…), apuntan a
sin_complemento.xslt, que es un stylesheet vacío.
Por qué es inocuo: cada XSLT de complemento sólo aporta plantillas que hacen match sobre nodos
de su propio complemento. Este módulo emite CFDI 4.0 tipo ingreso sin complementos, así que esas
plantillas nunca se invocan. Verificado: la cadena original que produce esta versión local es
idéntica, carácter a carácter, a la que produce el archivo del SAT resolviendo los includes por
red. Hay una prueba que lo fija en backend/tests/test_fin_stamping.py.
Por qué se hizo así, y no con un resolver
Porque libxslt sale a internet a resolver los xsl:include aunque el parser de lxml se cree con
no_network=True. Está comprobado: con el archivo original y sin resolver, la transformación
completa funciona, lo que sólo es posible si descargó utilerias.xslt de sat.gob.mx en ese
momento.
Eso es inaceptable aquí por tres razones: el timbrado dependería de que sat.gob.mx esté arriba y
responda rápido; la cadena original —el dato que se firma— vendría de una descarga no verificada en
tiempo de ejecución; y un cambio silencioso en el servidor del SAT cambiaría los sellos sin que
nadie lo note.
Se intentó primero con un etree.Resolver personalizado, que es lo que hace el sistema legado
(CFDI.cs, el bloque SafeXsltResolver comentado hacia la línea 8333). No funcionó: el resolver
también intercepta la resolución del documento principal, y devolver un stylesheet vacío ahí deja la
transformación sin plantillas y la cadena original sale vacía, sin ningún error. Un sello sobre
una cadena vacía es un CFDI que el SAT rechaza — o peor, un sello que parece válido y no lo es.
Con los href locales el problema desaparece de raíz: no hay nada que resolver fuera del directorio.
Cómo actualizar estos archivos
- Descarga el original del SAT (URLs de la tabla).
- Reescribe los
hrefde losxsl:include: el deutilerias.xsltal archivo local, el resto asin_complemento.xslt. - Corre
pytest tests/test_fin_stamping.py. La prueba de la cadena original tiene que seguir verde: si cambió el orden o el número de campos, el sello cambia y hay que revisarlo con Fiscal antes de subir nada.
Si algún día se soporta un complemento
Trae su XSLT oficial, déjalo en este directorio y apunta el href de ese include concreto al
archivo real, en lugar de a sin_complemento.xslt. No basta con añadir el nodo al XML: sin su
plantilla, el complemento no entra en la cadena original y el sello sale mal.