Class: Cfdi40::Comprobante

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

Overview

root node

Instance Attribute Summary collapse

Attributes inherited from Node

#children_nodes, #element_name, #parent_node, #readonly, #xml_document, #xml_parent

Instance Method Summary collapse

Methods inherited from Node

#add_attributes_to, #add_child_node, #add_children_to, #add_namespaces_to, #attibute_is_null?, attributes, #clean_cached_xml, #create_xml_node, #current_namespace, default_values, define_attribute, define_element_name, define_namespace, define_reader, define_writer, #delete_child, element_name, #expanded_element_name, formats, #formatted_value, #load_from_ng_node, #lock, namespaces, #set_defaults, verify_class_variables

Constructor Details

#initializeComprobante



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/cfdi40/comprobante.rb', line 38

def initialize
  super
  @errors = []
  @conceptos = Conceptos.new
  @conceptos.parent_node = self
  @emisor = Emisor.new
  @emisor.parent_node = self
  @receptor = Receptor.new
  @receptor.parent_node = self
  @sat_csd = SatCsd.new
  @fecha ||= Time.now
  @children_nodes = [@emisor, @receptor, @conceptos]
  @namespace_pagos_on_root = false
  set_defaults
end

Instance Attribute Details

#cadena_originalObject (readonly)

Returns the value of attribute cadena_original.



34
35
36
# File 'lib/cfdi40/comprobante.rb', line 34

def cadena_original
  @cadena_original
end

#conceptosObject (readonly)

Returns the value of attribute conceptos.



34
35
36
# File 'lib/cfdi40/comprobante.rb', line 34

def conceptos
  @conceptos
end

#emisorObject (readonly)

Returns the value of attribute emisor.



34
35
36
# File 'lib/cfdi40/comprobante.rb', line 34

def emisor
  @emisor
end

#errorsObject (readonly)

Returns the value of attribute errors.



34
35
36
# File 'lib/cfdi40/comprobante.rb', line 34

def errors
  @errors
end

#key_data=(value) ⇒ Object (writeonly)

Sets the attribute key_data



35
36
37
# File 'lib/cfdi40/comprobante.rb', line 35

def key_data=(value)
  @key_data = value
end

#key_pass=(value) ⇒ Object (writeonly)

Sets the attribute key_pass



35
36
37
# File 'lib/cfdi40/comprobante.rb', line 35

def key_pass=(value)
  @key_pass = value
end

#loaded_xmlObject

Returns the value of attribute loaded_xml.



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

def loaded_xml
  @loaded_xml
end

#namespace_pagos_on_root=(value) ⇒ Object (writeonly)

Sets the attribute namespace_pagos_on_root



35
36
37
# File 'lib/cfdi40/comprobante.rb', line 35

def namespace_pagos_on_root=(value)
  @namespace_pagos_on_root = value
end

#private_keyObject (readonly)

Returns the value of attribute private_key.



34
35
36
# File 'lib/cfdi40/comprobante.rb', line 34

def private_key
  @private_key
end

#receptorObject (readonly)

Returns the value of attribute receptor.



34
35
36
# File 'lib/cfdi40/comprobante.rb', line 34

def receptor
  @receptor
end

#sat_csdObject (readonly)

Returns the value of attribute sat_csd.



34
35
36
# File 'lib/cfdi40/comprobante.rb', line 34

def sat_csd
  @sat_csd
end

Instance Method Details

#add_concepto(attributes = {}) ⇒ Object

## Required attributes

clave_prod_serv

From SAT catalogue

clave_unidad

From SAT catalogue

cantidad

Must be greather than 0

descripcion

Product or service description

### Price and Taxes attributes

tasa_iva

Decimal between 0 and 1. Nil means exempt. Default value is 0.16

tasa_ieps

Decimal between 0 and 1. Nil means exempt. Default value is null

precio_bruto

Price before apply taxes or gross price. All quantities are calculated based on this price and taxes rate.

precio_neto

Precio after taxes or net price. All quantities are calculated from this prices. When both, precio_neto and precio_bruto exist, precio_neto is used

The most common usage requires only the net price (precio_neto).

## Optional attributes:

no_identificacion
unidad
descuento

PENDING

## Special attributes

### IEDU attributes

IEDU node (path: cfdi:Comprobante/cfdi:Conceptos/cfdi:Concepto/cfdi:ComplementoConcepto/iedu:instEducativas) is generated when one of iedu_nombre_alumno, iedu_curp, iedu_nivel_educativo exist.

iedu_nombre_alumno
iedu_curp
iedu_nivel_educativo
iedu_aut_rvoe
iedu_rfc_pago

Raises:



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/cfdi40/comprobante.rb', line 138

def add_concepto(attributes = {})
  raise Error, "CFDi tipo pago no acepta conceptos" if tipo_de_comprobante == "P"

  concepto = Concepto.new
  concepto.parent_node = @conceptos
  attributes.each do |key, value|
    method_name = "#{key}=".to_sym
    raise Error, ":#{key} no se puede asignar al concepto" unless concepto.respond_to?(method_name)

    concepto.public_send(method_name, value)
  end
  concepto.calculate!
  @conceptos.children_nodes << concepto
  calculate!
  concepto
end

#add_namespace_pagos_to_rootObject

Some PACs require that the namespace pago20 be placed in root node



307
308
309
310
311
312
# File 'lib/cfdi40/comprobante.rb', line 307

def add_namespace_pagos_to_root
  self.class.define_namespace "pago20", "http://www.sat.gob.mx/Pagos20"
  @schema_location += " http://www.sat.gob.mx/Pagos20 " \
                       "http://www.sat.gob.mx/sitio_internet/cfd/Pagos/Pagos20.xsd"
  true
end

#add_pago(attributes = {}) ⇒ Object

TODO: Doc params add_pago monto uuid folio serie num_parcialidad fecha_pago forma_pago importe_saldo_anterior objeto_impuestos

Raises:



188
189
190
191
192
193
# File 'lib/cfdi40/comprobante.rb', line 188

def add_pago(attributes = {})
  raise Error, "CFDi debe ser tipo 'P'" unless tipo_de_comprobante == "P"

  add_node_concepto_actividad_pago
  complemento.add_pago(attributes)
end

#add_splitted_pago(attributes = {}) ⇒ Object

See test_adding_pago_with_n_docto_relacionados in file test/test_cfdi40_rep.rb

Raises:



203
204
205
206
207
208
# File 'lib/cfdi40/comprobante.rb', line 203

def add_splitted_pago(attributes = {})
  raise Error, "CFDi debe ser tipo 'P'" unless tipo_de_comprobante == "P"

  add_node_concepto_actividad_pago
  complemento.add_splitted_pago(attributes)
end

#calculate!Object



255
256
257
258
259
260
261
262
263
# File 'lib/cfdi40/comprobante.rb', line 255

def calculate!
  return false if readonly

  @docxml = nil
  @subtotal = @conceptos.children_nodes.map(&:importe).map(&:to_f).sum
  @total = @conceptos.children_nodes.map(&:importe_neto).map(&:to_f).sum
  add_traslados_summary_node
  true
end

#cert_der=(cert_data) ⇒ Object



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

def cert_der=(cert_data)
  @sat_csd ||= SatCsd.new
  @sat_csd.cert_der = cert_data
  emisor.rfc = @sat_csd.rfc
  emisor.nombre ||= @sat_csd.name
  @no_certificado = @sat_csd.no_certificado
  @certificado = @sat_csd.cert64
  true
end

#cert_path=(path) ⇒ Object

Accept a path to read the certificate. Certificate is a X509 file. SAT generates those files in DER format.



57
58
59
# File 'lib/cfdi40/comprobante.rb', line 57

def cert_path=(path)
  self.cert_der = File.read(path)
end

#concepto_nodesObject



265
266
267
# File 'lib/cfdi40/comprobante.rb', line 265

def concepto_nodes
  @conceptos.children_nodes
end

#key_path=(path) ⇒ Object



71
72
73
# File 'lib/cfdi40/comprobante.rb', line 71

def key_path=(path)
  @key_data = File.read(path)
end

#load_certObject

Load from attribute ‘Certificado’ when the CFDi is loaded from a string



77
78
79
80
81
82
83
84
85
86
# File 'lib/cfdi40/comprobante.rb', line 77

def load_cert
  return if @sat_csd&.cert64

  @sat_csd ||= SatCsd.new
  @sat_csd.cert_der = OpenSSL::X509::Certificate.new(Base64.decode64(certificado))
  true
rescue StandardError
  # puts "Waring; Unable to load certificate from XML string"
  false
end

#load_concepto(ng_node) ⇒ Object

Load node ‘Concepto’ from a Nokogiri::XML::Element



156
157
158
159
160
161
162
163
# File 'lib/cfdi40/comprobante.rb', line 156

def load_concepto(ng_node)
  concepto = Concepto.new
  concepto.parent_node = @conceptos
  concepto.load_from_ng_node(ng_node)
  concepto.precio_bruto = concepto.valor_unitario.to_f
  @conceptos.children_nodes << concepto
  concepto
end

#load_impuestos(ng_node) ⇒ Object

Load node cfdi:Comprobante/cfdi:Impuestos

Normally this node is calculated but must be read from the XML when a CFDi is loaded



169
170
171
172
173
174
175
176
# File 'lib/cfdi40/comprobante.rb', line 169

def load_impuestos(ng_node)
  impuestos.load_from_ng_node(ng_node)
  ng_iva_node = ng_node.xpath("cfdi:Traslados/cfdi:Traslado[@Impuesto='002']").first
  return true if ng_iva_node.nil?

  impuestos.traslado_iva.load_from_ng_node(ng_iva_node)
  true
end

#load_pagos(pagos_node) ⇒ Object



296
297
298
# File 'lib/cfdi40/comprobante.rb', line 296

def load_pagos(pagos_node)
  complemento.load_pagos(pagos_node)
end

#load_tfd(tfd_node) ⇒ Object



288
289
290
291
292
293
294
# File 'lib/cfdi40/comprobante.rb', line 288

def load_tfd(tfd_node)
  timbre = Cfdi40::Timbre.new
  timbre.load_from_ng_node(tfd_node)
  timbre.parent_node = complemento
  complemento.children_nodes << timbre
  timbre
end

#original_contentObject



242
243
244
245
246
# File 'lib/cfdi40/comprobante.rb', line 242

def original_content
  xml_string = loaded_xml.nil? ? docxml.to_s : loaded_xml

  Cfdi40::OriginalContent.generate(xml_string)
end

#pago_nodesObject



269
270
271
272
273
# File 'lib/cfdi40/comprobante.rb', line 269

def pago_nodes
  return [] unless defined?(@complemento)

  complemento.pago_nodes
end

#remove_pago(index) ⇒ Object



195
196
197
198
199
200
# File 'lib/cfdi40/comprobante.rb', line 195

def remove_pago(index)
  return unless defined?(@complemento)
  return if complemento.pagos.pago_nodes.empty?

  complemento.pagos.remove_pago(index.to_i)
end

#signObject

Raises:



88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/cfdi40/comprobante.rb', line 88

def sign
  @sat_csd ||= SatCsd.new
  load_private_key if @sat_csd.private_key.nil?
  return unless @sat_csd.private_key

  raise Error, "Key and certificate not match" unless @sat_csd.valid_pair?

  @cadena_original = original_content
  digest = @sat_csd.private_key.sign(OpenSSL::Digest.new("SHA256"), @cadena_original)
  @sello = Base64.strict_encode64 digest
  lock
  @docxml = nil
end

#signed?Boolean



238
239
240
# File 'lib/cfdi40/comprobante.rb', line 238

def signed?
  !docxml.root.attributes["Sello"].nil?
end

#timbreObject



300
301
302
303
304
# File 'lib/cfdi40/comprobante.rb', line 300

def timbre
  return nil unless defined?(@complemento)

  complemento.timbre
end

#to_sObject



210
211
212
# File 'lib/cfdi40/comprobante.rb', line 210

def to_s
  to_xml
end

#to_xmlObject



214
215
216
217
218
219
220
221
# File 'lib/cfdi40/comprobante.rb', line 214

def to_xml
  return loaded_xml if !loaded_xml.nil? && signed?

  sign unless signed?
  return xml_string_ns_pagos_on_root if @namespace_pagos_on_root && pago_nodes.count > 0

  docxml.to_xml
end

#total_impuestos_trasladadosObject

Shortcut to attribute TotalImpuestosTrasladados of impuestos node



249
250
251
252
253
# File 'lib/cfdi40/comprobante.rb', line 249

def total_impuestos_trasladados
  return nil unless impuestos_node

  impuestos_node.total_impuestos_trasladados
end

#total_ivaObject



282
283
284
285
286
# File 'lib/cfdi40/comprobante.rb', line 282

def total_iva
  return 0 unless traslados

  traslados.traslados_iva.map(&:importe).map(&:to_f).sum.round(2)
end

#total_iva_nodeObject



275
276
277
278
279
280
# File 'lib/cfdi40/comprobante.rb', line 275

def total_iva_node
  # TODO: Puede haber más de un nodo, cuando hay varias tasas de iva
  return nil unless impuestos_node

  impuestos_node.traslado_iva
end

#valid?Boolean



223
224
225
226
227
228
229
# File 'lib/cfdi40/comprobante.rb', line 223

def valid?
  schema_validator = SchemaValidator.new(to_s)
  return true if schema_validator.valid?

  @errors = schema_validator.errors
  @errors.empty?
end

#valid_signature?Boolean



231
232
233
234
235
236
# File 'lib/cfdi40/comprobante.rb', line 231

def valid_signature?
  return false unless signed?

  signature_validator = SignatureValidator.new(to_xml)
  signature_validator.valid?
end