Class: CFDI::Comprobante

Inherits:
Object
  • Object
show all
Defined in:
lib/comprobante.rb

Overview

La clase principal para crear Comprobantes

Constant Summary collapse

@@datosCadena =

los datos para la cadena original en el órden correcto

[:version, :fecha, :tipoDeComprobante, :formaDePago, :condicionesDePago, :subTotal, :TipoCambio, :moneda, :total, :metodoDePago, :lugarExpedicion, :NumCtaPago]
@@data =

Todos los datos del comprobante

@@datosCadena+[:emisor, :receptor, :conceptos, :serie, :folio, :sello, :noCertificado, :certificado, :conceptos, :complemento, :cancelada, :impuestos]
@@options =
{
  tasa: 0.16,
  defaults: {
    moneda: 'pesos',
    version: '3.3',
    subTotal: 0.0,
    TipoCambio: 1,
    conceptos: [],
    impuestos: Impuestos.new,
    tipoDeComprobante: 'ingreso'
  }
}

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data = {}, options = {}) ⇒ CFDI::Comprobante

Crear un comprobante nuevo

Parameters:

  • data (Hash) (defaults to: {})

    Los datos de un comprobante

  • options (Hash) (defaults to: {})

    Las opciones para este comprobante

Options Hash (data):

  • :version (String) — default: '3.2'

    La version del CFDI

  • :fecha (String) — default: ''

    La fecha del CFDI

  • :tipoDeComprobante (String) — default: 'ingreso'

    El tipo de Comprobante

  • :formaDePago (String) — default: ''

    La forma de pago (pago en una sóla exhibición?)

  • :condicionesDePago (String) — default: ''

    Las condiciones de pago (Efectos fiscales al pago?)

  • :TipoCambio (String) — default: 1

    El tipo de cambio para la moneda de este CFDI’

  • :moneda (String) — default: 'pesos'

    La moneda de pago

  • :metodoDePago (String) — default: ''

    El método de pago (depósito bancario? efectivo?)

  • :lugarExpedicion (String) — default: ''

    El lugar dónde se expide la factura (Nutopía, México?)

  • :NumCtaPago (String) — default: nil

    El número de cuenta para el pago

See Also:

  • Opciones


59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/comprobante.rb', line 59

def initialize data={}, options={}
  #hack porque dup se caga con instance variables
  opts = Marshal::load(Marshal.dump(@@options))
  data = opts[:defaults].merge data
  @opciones = opts.merge options
  data.each do |k,v|
    method = "#{k}="
    next if !self.respond_to?(method)
    self.send method, v
  end
  @impuestos ||= Impuestos.new
end

Class Method Details

.configure(options) ⇒ Hash

Configurar las opciones default de los comprobantes

Parameters:

options

Las opciones del comprobante: tasa (de impuestos), defaults: un Hash con la moneda (pesos), version (3.2), TipoCambio (1), y tipoDeComprobante (ingreso)

Returns:

  • (Hash)


36
37
38
39
# File 'lib/comprobante.rb', line 36

def self.configure options
  @@options = Comprobante.rmerge @@options, options
  @@options
end

.rmerge(defaults, other_hash) ⇒ Object



405
406
407
408
409
410
411
412
# File 'lib/comprobante.rb', line 405

def self.rmerge defaults, other_hash
  result = defaults.merge(other_hash) do |key, oldval, newval|
    oldval = oldval.to_hash if oldval.respond_to?(:to_hash)
    newval = newval.to_hash if newval.respond_to?(:to_hash)
    oldval.class.to_s == 'Hash' && newval.class.to_s == 'Hash' ? Comprobante.rmerge(oldval, newval) : newval
  end
  result
end

Instance Method Details

#addenda=(addenda) ⇒ Object



73
74
75
76
# File 'lib/comprobante.rb', line 73

def addenda= addenda
  addenda = Addenda.new addenda unless addenda.is_a? Addenda
  @addenda = addenda
end

#cadena_originalString

La cadena original del CFDI

Returns:

  • (String)

    Separada por pipes, because fuck you that’s why



344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/comprobante.rb', line 344

def cadena_original
  params = []

  @@datosCadena.each {|key| params.push send(key) }
  params += @emisor.cadena_original
  params << @regimen
  params += @receptor.cadena_original

  @conceptos.each do |concepto|
    params += concepto.cadena_original
  end

  if @impuestos.count > 0
    @impuestos.traslados.each do |traslado|
      # tasa = traslado.tasa ? traslado.tasa.to_i : (@opciones[:tasa]*100).to_i
      tasa = (@opciones[:tasa]*100).to_i
      total = self.subTotal*@opciones[:tasa]
      params += [traslado.impuesto, tasa, total, total]
    end
  end

  params.select! { |i| i != nil && i != '' }
  params.map! do |elem|
    if elem.is_a? Float
      elem = sprintf('%.2f', elem)
    else
      elem = elem.to_s
    end
    elem
  end

  return "||#{params.join '|'}||"
end

#complemento=(complemento) ⇒ CFDI::Complemento

Asigna un complemento al comprobante

Parameters:

Returns:



183
184
185
186
187
# File 'lib/comprobante.rb', line 183

def complemento= complemento
  complemento = Complemento.new complemento unless complemento.is_a? Complemento
  @complemento = complemento
  complemento
end

#conceptos=(conceptos) ⇒ Array

Agrega uno o varios conceptos En caso de darle un Hash o un CFDI::Concepto, agrega este a los conceptos, de otro modo, sobreescribe los conceptos pre-existentes

Parameters:

Returns:

  • (Array)

    Los conceptos de este comprobante



163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/comprobante.rb', line 163

def conceptos= conceptos
  if conceptos.is_a? Array
    conceptos.map! do |concepto|
      concepto = Concepto.new concepto unless concepto.is_a? Concepto
    end
  elsif conceptos.is_a? Hash
    conceptos << Concepto.new(concepto)
  elsif conceptos.is_a? Concepto
    conceptos << conceptos
  end

  @conceptos = conceptos
  conceptos
end

#emisor=(emisor) ⇒ CFDI::Entidad

Asigna un emisor de tipo Entidad

Parameters:

Returns:



142
143
144
145
# File 'lib/comprobante.rb', line 142

def emisor= emisor
  emisor = Entidad.new emisor unless emisor.is_a? Entidad
  @emisor = emisor;
end

#fecha=(fecha) ⇒ String

Asigna una fecha al comprobante

Parameters:

  • fecha (Time, String)

    La fecha y hora (YYYY-MM-DDTHH:mm:SS) de la emisión

Returns:

  • (String)

    la fecha en formato ‘%FT%R:%S’



194
195
196
197
# File 'lib/comprobante.rb', line 194

def fecha= fecha
  fecha = fecha.strftime('%FT%R:%S') unless fecha.is_a? String
  @fecha = fecha
end

#impuestos=(value) ⇒ Object



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/comprobante.rb', line 115

def impuestos= value
  @impuestos = case @version
    when '3.2'
      return value if value.is_a? Impuestos
      raise 'v3.2 CFDI impuestos must be an array of hashes' unless value.is_a? Array

      traslados = value.map {|i|
        raise 'v3.2 CFDI impuestos must be an array of hashes' unless i.is_a? Hash

        tasa = i[:tasa] || @opciones[:tasa]

        {
          tasa: tasa,
          impuesto: i[:impuesto] || 'IVA',
          importe: tasa * self.subTotal
        }
      }

      Impuestos.new({ traslados: traslados })
    when '3.3' then value.is_a?(Impuestos) ? value : Impuestos.new(value)
  end
end

#metodoDePago_valueString

Regresa el método de pago como valor textual

En la versión 3.3 del CFDI el SAT decidió salir con sus pendejadas y cambiar la manera de almacenar el método de pago como una clave porque les salió lo contador old school. Claro se guarda como una cadena de texto porque 0.1 + 0.2 ~= 0.30000000000000004 y para que sus archivos de excel estén centrados. Pensamientos positivos, Roberto, respira profundo.

Returns:

  • (String)

    El metodo de pago textual, por ejemplo “Dinero electrónico”



87
88
89
90
91
92
93
# File 'lib/comprobante.rb', line 87

def metodoDePago_value
  if @version.to_f >= 3.3
    CFDI.metodos_de_pago @metodoDePago
  else
    @metodoDePago
  end
end

#receptor=(receptor) ⇒ CFDI::Entidad

Asigna un receptor

Parameters:

Returns:



151
152
153
154
155
# File 'lib/comprobante.rb', line 151

def receptor= receptor
  receptor = Entidad.new receptor unless receptor.is_a? Entidad
  @receptor = receptor;
  receptor
end

#subTotalFloat

Regresa el subtotal de este comprobante, tomando el importe de cada concepto

Returns:

  • (Float)

    El subtotal del comprobante



98
99
100
101
102
103
104
# File 'lib/comprobante.rb', line 98

def subTotal
  ret = 0
  @conceptos.each do |c|
    ret += c.importe
  end
  ret
end

#timbre_valido?(cert = nil) ⇒ Boolean

Revisa que el timbre de un comprobante sea válido

Parameters:

  • El (String)

    certificado del PAC

Returns:

  • (Boolean)

    El resultado de la validación



383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/comprobante.rb', line 383

def timbre_valido? cert=nil
  return false unless complemento && complemento.selloSAT

  unless cert
    require 'open-uri'
    comps = complemento.noCertificadoSAT.scan(/(\d{6})(\d{6})(\d{2})(\d{2})(\d{2})?/)
    base_url = 'ftp://ftp2.sat.gob.mx/Certificados/FEA'
    url = "#{base_url}/#{comps.join('/')}/#{cert}.cer"
    begin
      cert = open(url).read
    rescue Exception => e
      raise "No pude descargar el certificado <#{url}>: #{e}"
    end
  end

  cert = OpenSSL::X509::Certificate.new cert
  selloBytes = Base64::decode64(complemento.selloSAT)
  cert.public_key.verify(OpenSSL::Digest::SHA1.new, selloBytes, complemento.cadena)
end

#to_hHash

Un hash con todos los datos del comprobante, listo para Hash.to_json

Returns:

  • (Hash)

    El comprobante como Hash



330
331
332
333
334
335
336
337
338
# File 'lib/comprobante.rb', line 330

def to_h
  hash = {}
  @@data.each do |key|
    data = deep_to_h send(key)
    hash[key] = data
  end

  return hash
end

#to_xmlString

El comprobante como XML

Returns:

  • (String)

    El comprobante namespaceado en versión 3.2 (porque soy un huevón)



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/comprobante.rb', line 203

def to_xml
  ns = {
    'xmlns:cfdi' => "http://www.sat.gob.mx/cfd/3",
    'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance",
    'xsi:schemaLocation' => "http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv#{@version.gsub(/\D/, '')}.xsd",
    version: @version,
    folio: @folio,
    fecha: @fecha,
    formaDePago: @formaDePago,
    condicionesDePago: @condicionesDePago,
    subTotal: sprintf('%.2f', self.subTotal),
    Moneda: @moneda,
    total: sprintf('%.2f', self.total),
    metodoDePago: @metodoDePago,
    tipoDeComprobante: @tipoDeComprobante,
    LugarExpedicion: @lugarExpedicion,
  }
  ns[:serie] = @serie if @serie
  ns[:TipoCambio] = @TipoCambio if @TipoCambio
  ns[:NumCtaPago] = @NumCtaPago if @NumCtaPago && @NumCtaPago!=''

  if (@addenda)
    # Si tenemos addenda, entonces creamos el campo "xmlns:ElNombre" y agregamos sus definiciones al SchemaLocation
    ns["xmlns:#{@addenda.nombre}"] = @addenda.namespace
    ns['xsi:schemaLocation'] += ' '+[@addenda.namespace, @addenda.xsd].join(' ')
  end

  if @noCertificado
    ns[:noCertificado] = @noCertificado
    ns[:certificado] = @certificado
  end

  if @sello
    ns[:sello] = @sello
  end

  @builder = Nokogiri::XML::Builder.new do |xml|
    xml.Comprobante(ns) do
      ins = xml.doc.root.add_namespace_definition('cfdi', 'http://www.sat.gob.mx/cfd/3')
      xml.doc.root.namespace = ins

      xml.Emisor(@emisor.ns)  {
        xml.DomicilioFiscal(@emisor.domicilioFiscal.to_h.reject {|k,v| v == nil})
        xml.ExpedidoEn(@emisor.expedidoEn.to_h.reject {|k,v| v == nil || v == ''})
        xml.RegimenFiscal({Regimen: @emisor.regimenFiscal})
      }
      xml.Receptor(@receptor.ns) {
        xml.Domicilio(@receptor.domicilioFiscal.to_h.reject {|k,v| v == nil || v == ''})
      }
      xml.Conceptos {
        @conceptos.each do |concepto|
          # select porque luego se caga el xml si incluyo noIdentificacion y es empty

          cc = concepto.to_h.select {|k,v| v!=nil && v != ''}

          cc = cc.map {|k,v|
            v = sprintf('%.2f', v) if v.is_a? Float
            [k,v]
          }.to_h

          xml.Concepto(cc) {
            xml.ComplementoConcepto
          }
        end
      }

      impuestos_options = {}
      impuestos_options = {totalImpuestosTrasladados: sprintf('%.2f', self.subTotal*@opciones[:tasa])} if @impuestos.count > 0
      xml.Impuestos(impuestos_options) {
        if @impuestos.traslados.count > 0
          xml.Traslados {
            @impuestos.traslados.each do |traslado|
              xml.Traslado({
                impuesto: traslado.impuesto,
                tasa:(@opciones[:tasa]*100).to_i,
                importe: sprintf('%.2f', self.subTotal*@opciones[:tasa])})
            end
          }
        end
        if @impuestos.retenciones.count > 0
          xml.Retenciones {
            @impuestos.retenciones.each do |retencion|
              xml.Retencion({
                impuesto: retencion.impuesto,
                tasa:(@opciones[:tasa]*100).to_i,
                importe: sprintf('%.2f', self.subTotal*@opciones[:tasa])})
            end
          }
        end
      }

      xml.Complemento {
        if @complemento
          nsTFD = {
            'xsi:schemaLocation' => 'http://www.sat.gob.mx/TimbreFiscalDigital http://www.sat.gob.mx/TimbreFiscalDigital/TimbreFiscalDigital.xsd',
            'xmlns:tfd' => 'http://www.sat.gob.mx/TimbreFiscalDigital',
            'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'
          }
          xml['tfd'].TimbreFiscalDigital(@complemento.to_h.merge nsTFD) {
          }

        end
      }

      if (@addenda)
        xml.Addenda {
          @addenda.data.each do |k,v|
            if v.is_a? Hash
              xml[@addenda.nombre].send(k, v)
            elsif v.is_a? Array
              xml[@addenda.nombre].send(k, v)
            else
              xml[@addenda.nombre].send(k, v)
            end
          end
        }
      end

    end
  end
  @builder.to_xml
end

#totalFloat

Regresa el total

Returns:

  • (Float)

    El subtotal multiplicado por la tasa



109
110
111
112
113
# File 'lib/comprobante.rb', line 109

def total
  iva = 0.0
  iva = (self.subTotal*@opciones[:tasa]) if @impuestos.count > 0
  self.subTotal + @impuestos.total
end