CFDI (cfdi v4.0.5)

Copy Markdown

Orquestador principal del CFDI 4.0.

Recibe un %Cfdi.Comprobante{} previamente armado con sus elementos (Emisor, Receptor, Conceptos, …), lo certifica, sella y serializa a XML, mapa o JSON.

Summary

Functions

Devuelve la cadena original guardada por sellar/3, o nil si todavía no fue sellado.

Asocia certificado y número de certificado al comprobante.

Invalida el sellado del documento: borra el Sello y el Timbre Fiscal Digital, dejándolo listo para volver a sellar.

Igual que from_xml/2 pero leyendo desde disco.

Igual que from_file/2 pero devuelve el %CFDI{} directo o levanta.

Reconstruye un %CFDI{} completo a partir de un XML.

Igual que from_xml/2 pero devuelve el %CFDI{} directo o levanta.

Genera la cadena original aplicando el XSLT al XML del CFDI.

Firma la cadena original con la llave privada cargada desde archivo.

¿El comprobante está sellado (tiene Sello)?

Firma la cadena original y escribe el atributo Sello usando una credencial ya cargada en config[:credential] y la cadena en config[:cadena].

Genera la cadena original, la firma con la llave privada del archivo dado, guarda la cadena en config[:cadena_original] y escribe el atributo Sello.

¿El comprobante trae Timbre Fiscal Digital?

Serializa el CFDI a JSON.

Proyecta el CFDI a un mapa de datos.

Serializa el CFDI a XML respetando el orden de elementos que exige el Anexo 20 del SAT (indispensable para que los XSLT de cadena original procesen el documento correctamente).

Folio fiscal (UUID) del timbre, o nil si el comprobante no está timbrado.

Asocia la ruta del XSLT que generará la cadena original.

Types

t()

@type t() :: %CFDI{comprobante: Cfdi.Comprobante.t() | nil, config: map()}

Functions

cadena_original(cfdi)

@spec cadena_original(t()) :: String.t() | nil

Devuelve la cadena original guardada por sellar/3, o nil si todavía no fue sellado.

Espejo del getter cadenaOriginal de Node.

certificar(c, cred)

@spec certificar(t(), Sat.Certificados.Credential.t()) ::
  {:ok, t()} | {:error, atom()}

Asocia certificado y número de certificado al comprobante.

desellar(cfdi)

@spec desellar(t()) :: t()

Invalida el sellado del documento: borra el Sello y el Timbre Fiscal Digital, dejándolo listo para volver a sellar.

Atajo de Cfdi.Comprobante.desellar/1 al nivel del %CFDI{}. Ver ahí cuándo hace falta llamarlo a mano.

from_file(path, opts \\ [])

@spec from_file(
  Path.t(),
  keyword()
) :: {:ok, t()} | {:error, Cfdi.Decoder.reason() | {:file_error, File.posix()}}

Igual que from_xml/2 pero leyendo desde disco.

Agrega {:error, {:file_error, posix}} a los errores posibles.

from_file!(path, opts \\ [])

@spec from_file!(
  Path.t(),
  keyword()
) :: t()

Igual que from_file/2 pero devuelve el %CFDI{} directo o levanta.

from_xml(xml, opts \\ [])

@spec from_xml(
  String.t(),
  keyword()
) :: {:ok, t()} | {:error, Cfdi.Decoder.reason()}

Reconstruye un %CFDI{} completo a partir de un XML.

Camino inverso de to_xml/2: devuelve el comprobante con todo lo que traía el documento —emisor, receptor, información global, conceptos con sus impuestos y complementos de concepto, impuestos globales, CFDI relacionados y complementos— como structs tipados, listo para inspeccionar o modificar.

{:ok, cfdi} = CFDI.from_xml(xml)
cfdi.comprobante."Total"
cfdi.comprobante |> Map.get(:"cfdi:Emisor") |> Map.get(:Rfc)

Los complementos se resuelven por la URI de su namespace contra Cfdi.Complementos.Registry. Uno desconocido no se descarta: cae al struct genérico Cfdi.Complementos.Complemento, preservando su carga útil.

Cuidado: un CFDI timbrado es inmutable

from_xml/2 preserva Sello, Certificado, NoCertificado y el TimbreFiscalDigital tal como venían — to_xml/2 los devuelve intactos.

Pero modificar un comprobante timbrado invalida su sello: la cadena original deja de coincidir. Fiscalmente eso no es "editar" una factura, es emitir otra: hay que volver a sellar (sellar/3) y re-timbrar, lo que produce un folio fiscal nuevo. Usá timbrado?/1 antes de tocar nada.

Errores

  • {:error, {:malformed_xml, reason}} — el XML no parsea
  • {:error, {:unexpected_root, name}} — la raíz no es cfdi:Comprobante

from_xml!(xml, opts \\ [])

@spec from_xml!(
  String.t(),
  keyword()
) :: t()

Igual que from_xml/2 pero devuelve el %CFDI{} directo o levanta.

generar_cadena_original(c, opts \\ [])

@spec generar_cadena_original(
  t(),
  keyword()
) :: {:ok, String.t()} | {:error, term()}

Genera la cadena original aplicando el XSLT al XML del CFDI.

Espejo de generarCadenaOriginal de Node. Toma el XSLT desde config[:xslt] (set vía xslt/2) o desde opts[:xslt].

generar_sello(cadena, keyfile, password \\ nil)

@spec generar_sello(String.t(), Path.t(), String.t() | nil) ::
  {:ok, String.t()} | {:error, term()}

Firma la cadena original con la llave privada cargada desde archivo.

Espejo de generarSello de Node.

new(comprobante)

@spec new(Cfdi.Comprobante.t()) :: t()

sellado?(cfdi)

@spec sellado?(t()) :: boolean()

¿El comprobante está sellado (tiene Sello)?

Sellado y timbrado son etapas distintas: primero el emisor sella con su CSD, después el PAC timbra. Un CFDI recién sellado está sellado? pero todavía no timbrado?.

sellar(c)

@spec sellar(t()) :: {:ok, t()} | {:error, atom()}

Firma la cadena original y escribe el atributo Sello usando una credencial ya cargada en config[:credential] y la cadena en config[:cadena].

Para el flujo de alto nivel desde archivos, usar sellar/3.

sellar(c, credential)

sellar(c, keyfile, password)

@spec sellar(t(), Path.t(), String.t() | nil) :: {:ok, t()} | {:error, term()}

Genera la cadena original, la firma con la llave privada del archivo dado, guarda la cadena en config[:cadena_original] y escribe el atributo Sello.

Espejo de sellar(keyfile, password) de Node:

public async sellar(keyfile: string, password: string): Promise<void> {
  const cadena = await this.generarCadenaOriginal();
  const sello = await this.generarSello(cadena, keyfile, password);
  this._cadenaOriginal = cadena;
  this.setSello(sello);
}

timbrado?(cfdi)

@spec timbrado?(t()) :: boolean()

¿El comprobante trae Timbre Fiscal Digital?

Un CFDI timbrado ya fue certificado por un PAC: su Sello y su UUID sólo son válidos para el contenido exacto que se timbró. Modificar el contenido lo invalida — los setters de contenido borran el Sello y el timbre. Ver from_xml/2 y desellar/1.

to_json(cfdi)

@spec to_json(t()) :: String.t()

Serializa el CFDI a JSON.

Opciones:

  • :nstrue (default) incluye prefijos cfdi:; false los omite.
  • :prettytrue indenta el JSON; false (default) compacto.

to_json(cfdi, opts)

@spec to_json(
  t(),
  keyword()
) :: String.t()

to_map(cfdi)

@spec to_map(t()) :: map()

Proyecta el CFDI a un mapa de datos.

Las declaraciones de namespace (xmlns:*) y el xsi:schemaLocation NO aparecen: son plomería para reconstruir el XML, no datos del comprobante, y viven sólo en el camino de to_xml/2. El mapa trae emisor, receptor, conceptos, impuestos, sello, complementos… sin ruido de XML. Lo mismo aplica al JSON de to_json/2, que se arma sobre este mapa.

Opciones:

  • :nstrue (default) incluye el prefijo cfdi: en los nombres de elementos y mantiene los atributos como átomos (:Rfc, :Nombre) para distinguirlos de los elementos hijos; false los omite y uniforma TODAS las llaves al tipo elegido en :keys.

  • :keys — controla el tipo de las llaves cuando ns: false. Sin efecto con ns: true (la convención manda). Valores:

    • :string (default) — todas las llaves son strings. Siempre seguro.
    • :existing — llaves se convierten a átomo si ya existe en la tabla global de átomos del VM; si no, quedan como string. Seguro ante XML/llaves arbitrarias.
    • :atom — todas las llaves se convierten a átomos vía String.to_atom/1. Peligroso con XML externo: la tabla de átomos no tiene GC y puede agotarse (atom_table_full). Usar solo cuando se controla la fuente del XML.
  • :case — controla la capitalización de las llaves cuando ns: false. Sin efecto con ns: true. Valores:

    • :as_is (default) — preserva la capitalización original (PascalCase como en el XSD oficial: NoCertificado, RegimenFiscal).
    • :camel — pasa la primera letra a minúscula para producir camelCase idiomático (noCertificado, regimenFiscal). El resto del nombre queda intacto (UsoCFDIusoCFDI, preservando el acrónimo final). Útil al exportar a JSON o sistemas JS que esperan camelCase.

Convenciones de llaves cuando ns: true:

  • strings ("cfdi:Emisor") son elementos XML.
  • átomos (:Rfc, :Nombre) son atributos XML.

Cuando ns: false, las llaves se uniforman para una vista plana — útil para inspección, serialización a JSON o consumo desde sistemas que no distinguen entre atributos y elementos.

to_map(cfdi, opts)

@spec to_map(
  t(),
  keyword()
) :: map()

to_xml(cfdi)

@spec to_xml(t()) :: String.t()

Serializa el CFDI a XML respetando el orden de elementos que exige el Anexo 20 del SAT (indispensable para que los XSLT de cadena original procesen el documento correctamente).

Opciones:

  • :prettytrue indenta el XML para lectura humana; false (default) produce XML compacto en una sola línea.

to_xml(cfdi, opts)

@spec to_xml(
  t(),
  keyword()
) :: String.t()

uuid(cfdi)

@spec uuid(t()) :: String.t() | nil

Folio fiscal (UUID) del timbre, o nil si el comprobante no está timbrado.

xslt(c, path)

@spec xslt(t(), String.t()) :: t()

Asocia la ruta del XSLT que generará la cadena original.

Espejo de options.xslt en el constructor de CFDI de Node.