Files
CRM_AGENTES_CARGA/backend/api/v1/modules/fin/stamping/xslt
Jair Cedillo 6e208876f7 feat(fin): timbrado de CFDI 4.0 de ingreso con Comercio Digital
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>
2026-08-11 09:07:05 -05:00
..

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.xslt apunta 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

  1. Descarga el original del SAT (URLs de la tabla).
  2. Reescribe los href de los xsl:include: el de utilerias.xslt al archivo local, el resto a sin_complemento.xslt.
  3. 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.