cfdi40 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f610f343a775755d3ec5e603588f8693ac502c576c56a32bf6dda422a7eebebe
4
- data.tar.gz: 0e72a90973ca6f28b42b0c793951400a0d5ae8532bbb7c2d750c917cbae41b38
3
+ metadata.gz: cad6896abeef260eaf8d4f9e659da6b6572fd693e51713261c982addf854373a
4
+ data.tar.gz: c68c19bb783b5864c81103899d5ab46b1281d6eccaecbd43efee35cf927777d6
5
5
  SHA512:
6
- metadata.gz: 1cfcc729870cb04928ad9534068843df9ea61a9a6f9b0e994500f0b94b16f01659427984a05e05cc991900055bd4ac5bf8456d2ddabe0ffd7570f8e229693042
7
- data.tar.gz: cfd0ca0717d596c1347e42d6ff680c39c13c57c0cf83c5ad9005ca96c9efcc5f9e1bd55ed706a086b99657bba0ff244b5eba8abcfabe50980f1481651429a87d
6
+ metadata.gz: 0ffc2e71680140e910f44dc35c5201ad28747454d4167290c57f52836bbd75dcae1aaa6768735d7435b2510cd96b6d9be6d271dddddce59e598c86ae71a502f1
7
+ data.tar.gz: 8649c8ade0dbbbee0caf4e9a31692d30874eb86ee5712ae3144501cd41614e6739c3d7d90b9e2584d561d7856345ad6b37063921671d50daa76c60b95d907d62
@@ -2,6 +2,7 @@
2
2
 
3
3
  # Create and Read XML documents
4
4
  module Cfdi40
5
+ # root node
5
6
  class Comprobante < Node
6
7
  define_namespace "xsi", "http://www.w3.org/2001/XMLSchema-instance"
7
8
  define_namespace "cfdi", "http://www.sat.gob.mx/cfd/4"
@@ -13,7 +14,7 @@ module Cfdi40
13
14
  define_attribute :version, xml_attribute: "Version", readonly: true, default: "4.0"
14
15
  define_attribute :serie, xml_attribute: "Serie"
15
16
  define_attribute :folio, xml_attribute: "Folio"
16
- define_attribute :fecha, xml_attribute: "Fecha"
17
+ define_attribute :fecha, xml_attribute: "Fecha", format: :t_FechaH
17
18
  define_attribute :sello, xml_attribute: "Sello", readonly: true
18
19
  define_attribute :forma_pago, xml_attribute: "FormaPago"
19
20
  define_attribute :no_certificado, xml_attribute: "NoCertificado"
@@ -32,6 +33,7 @@ module Cfdi40
32
33
 
33
34
  attr_reader :emisor, :receptor, :conceptos, :private_key, :sat_csd, :errors
34
35
  attr_writer :key_data, :key_pass
36
+ attr_accessor :loaded_xml
35
37
 
36
38
  def initialize
37
39
  super
@@ -43,7 +45,7 @@ module Cfdi40
43
45
  @receptor = Receptor.new
44
46
  @receptor.parent_node = self
45
47
  @sat_csd = SatCsd.new
46
- @fecha ||= Time.now.strftime("%Y-%m-%dT%H:%M:%S")
48
+ @fecha ||= Time.now
47
49
  @children_nodes = [@emisor, @receptor, @conceptos]
48
50
  set_defaults
49
51
  end
@@ -91,6 +93,7 @@ module Cfdi40
91
93
 
92
94
  digest = @sat_csd.private_key.sign(OpenSSL::Digest.new("SHA256"), original_content)
93
95
  @sello = Base64.strict_encode64 digest
96
+ lock
94
97
  @docxml = nil
95
98
  end
96
99
 
@@ -192,7 +195,9 @@ module Cfdi40
192
195
  end
193
196
 
194
197
  def to_xml
195
- sign
198
+ return loaded_xml if !loaded_xml.nil? && signed?
199
+
200
+ sign unless signed?
196
201
  docxml.to_xml
197
202
  end
198
203
 
@@ -204,17 +209,25 @@ module Cfdi40
204
209
  @errors.empty?
205
210
  end
206
211
 
212
+ def valid_signature?
213
+ return false unless signed?
214
+
215
+ signature_validator = SignatureValidator.new(to_xml)
216
+ signature_validator.valid?
217
+ end
218
+
219
+ def signed?
220
+ !docxml.root.attributes["Sello"].nil?
221
+ end
222
+
207
223
  def cadena_original
208
224
  original_content
209
225
  end
210
226
 
211
227
  def original_content
212
- xslt_path = File.join(File.dirname(__FILE__), "..", "..", "lib/xslt/cadenaoriginal_local.xslt")
213
- xslt = Nokogiri::XSLT(File.open(xslt_path))
214
- transformed = xslt.transform(docxml)
215
- # The ampersand (&) char must be used in original content
216
- # even though the documentation indicates otherwise
217
- transformed.children.to_s.gsub("&amp;", "&").strip
228
+ xml_string = loaded_xml.nil? ? docxml.to_s : loaded_xml
229
+
230
+ Cfdi40::OriginalContent.generate(xml_string)
218
231
  end
219
232
 
220
233
  # Shortcut to attribute TotalImpuestosTrasladados of impuestos node
@@ -225,10 +238,13 @@ module Cfdi40
225
238
  end
226
239
 
227
240
  def calculate!
241
+ return false if readonly
242
+
228
243
  @docxml = nil
229
244
  @subtotal = @conceptos.children_nodes.map(&:importe).map(&:to_f).sum
230
245
  @total = @conceptos.children_nodes.map(&:importe_neto).map(&:to_f).sum
231
246
  add_traslados_summary_node
247
+ true
232
248
  end
233
249
 
234
250
  def concepto_nodes
@@ -9,7 +9,7 @@ module Cfdi40
9
9
  class Concepto < Node
10
10
  define_attribute :clave_prod_serv, xml_attribute: "ClaveProdServ"
11
11
  define_attribute :no_identificacion, xml_attribute: "NoIdentificacion"
12
- define_attribute :cantidad, xml_attribute: "Cantidad", default: 1
12
+ define_attribute :cantidad, xml_attribute: "Cantidad", default: 1, format: :decimal
13
13
  define_attribute :clave_unidad, xml_attribute: "ClaveUnidad"
14
14
  define_attribute :unidad, xml_attribute: "Unidad"
15
15
  define_attribute :descripcion, xml_attribute: "Descripcion"
@@ -75,7 +75,7 @@ module Cfdi40
75
75
  end
76
76
 
77
77
  def traslado_iva_node
78
- return nil unless impuestos_node.is_a?(Impuestos)
78
+ return nil unless impuestos_node.is_a?(ConceptoImpuestos)
79
79
 
80
80
  impuestos_node.traslado_iva
81
81
  end
@@ -140,10 +140,10 @@ module Cfdi40
140
140
  return @impuestos_node if defined?(@impuestos_node)
141
141
  return nil unless objeto_impuestos?
142
142
 
143
- @impuestos_node = children_nodes.select { |child| child.is_a?(Impuestos) }.first
143
+ @impuestos_node = children_nodes.select { |child| child.is_a?(ConceptoImpuestos) }.first
144
144
  return if @impuestos_node
145
145
 
146
- @impuestos_node = Impuestos.new
146
+ @impuestos_node = ConceptoImpuestos.new
147
147
  @impuestos_node.parent_node = self
148
148
  children_nodes << @impuestos_node
149
149
  @impuestos_node
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # /cfdi:Comprobante/cfdi:Conceptos/cfdiConcepto/cfdi:Impuestos
4
+ module Cfdi40
5
+ class ConceptoImpuestos < Node
6
+ define_element_name "Impuestos"
7
+
8
+ def traslados
9
+ return @traslados if defined?(@traslados)
10
+
11
+ @traslados = Traslados.new
12
+ @traslados.parent_node = self
13
+ children_nodes << @traslados
14
+ @traslados
15
+ end
16
+
17
+ def traslados_node
18
+ children_nodes.select { |n| n.is_a?(Traslados) }.first
19
+ end
20
+
21
+ def traslado_nodes
22
+ return [] if traslados_node.nil?
23
+
24
+ traslados_node.traslado_nodes
25
+ end
26
+
27
+ def traslado_iva
28
+ traslados.traslado_iva
29
+ end
30
+ end
31
+ end
@@ -1,17 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cfdi40
4
- class Totales < Node
5
- define_attribute :ret_iva, xml_attribute: "TotalRetencionesIVA"
6
- define_attribute :ret_isr, xml_attribute: "TotalRetencionesISR"
7
- define_attribute :ret_ieps, xml_attribute: "TotalRetencionesIEPS"
8
- define_attribute :base_iva16, xml_attribute: "TotalTrasladosBaseIVA16"
9
- define_attribute :importe_iva16, xml_attribute: "TotalTrasladosImpuestoIVA16"
10
- define_attribute :base_iva8, xml_attribute: "TotalTrasladosBaseIVA8"
11
- define_attribute :importe_iva8, xml_attribute: "TotalTrasladosImpuestoIVA8"
12
- define_attribute :base_iva0, xml_attribute: "TotalTrasladosBaseIVA0"
13
- define_attribute :importe_iva0, xml_attribute: "TotalTrasladosImpuestoIVA0"
14
- define_attribute :base_iva_excento, xml_attribute: "TotalTrasladosBaseIVAExento"
15
- define_attribute :monto_total, xml_attribute: "MontoTotalPagos"
4
+ class CpTotales < Node
5
+ define_element_name 'Totales'
6
+ define_attribute :ret_iva, xml_attribute: "TotalRetencionesIVA", format: :t_ImporteMXN
7
+ define_attribute :ret_isr, xml_attribute: "TotalRetencionesISR", format: :t_ImporteMXN
8
+ define_attribute :ret_ieps, xml_attribute: "TotalRetencionesIEPS", format: :t_ImporteMXN
9
+ define_attribute :base_iva16, xml_attribute: "TotalTrasladosBaseIVA16", format: :t_ImporteMXN
10
+ define_attribute :importe_iva16, xml_attribute: "TotalTrasladosImpuestoIVA16", format: :t_ImporteMXN
11
+ define_attribute :base_iva8, xml_attribute: "TotalTrasladosBaseIVA8", format: :t_ImporteMXN
12
+ define_attribute :importe_iva8, xml_attribute: "TotalTrasladosImpuestoIVA8", format: :t_ImporteMXN
13
+ define_attribute :base_iva0, xml_attribute: "TotalTrasladosBaseIVA0", format: :t_ImporteMXN
14
+ define_attribute :importe_iva0, xml_attribute: "TotalTrasladosImpuestoIVA0", format: :t_ImporteMXN
15
+ define_attribute :base_iva_excento, xml_attribute: "TotalTrasladosBaseIVAExento", format: :t_ImporteMXN
16
+ define_attribute :monto_total, xml_attribute: "MontoTotalPagos", format: :t_ImporteMXN
16
17
  end
17
18
  end
@@ -6,7 +6,7 @@ module Cfdi40
6
6
  define_attribute :serie, xml_attribute: "Serie"
7
7
  define_attribute :folio, xml_attribute: "Folio"
8
8
  define_attribute :moneda_dr, xml_attribute: "MonedaDR", default: "MXN"
9
- define_attribute :equivalencia_dr, xml_attribute: "EquivalenciaDR", default: "1"
9
+ define_attribute :equivalencia_dr, xml_attribute: "EquivalenciaDR", default: "1", format: :decimal
10
10
  define_attribute :num_parcialidad, xml_attribute: "NumParcialidad"
11
11
  define_attribute :imp_saldo_ant, xml_attribute: "ImpSaldoAnt", format: :t_ImporteMXN
12
12
  define_attribute :imp_pagado, xml_attribute: "ImpPagado", format: :t_ImporteMXN
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # /cfdi:Comprobante/cfdi:Impuestos
3
4
  module Cfdi40
4
5
  class Impuestos < Node
5
- define_attribute :total_impuestos_retenidos, xml_attribute: "TotalImpuestosRetenidos", format: :t_ImporteMXN
6
- define_attribute :total_impuestos_trasladados, xml_attribute: "TotalImpuestosTrasladados", format: :t_ImporteMXN
6
+ define_attribute :total_impuestos_retenidos, xml_attribute: "TotalImpuestosRetenidos", format: :t_Importe
7
+ define_attribute :total_impuestos_trasladados, xml_attribute: "TotalImpuestosTrasladados", format: :t_Importe
7
8
 
8
9
  def traslados
9
10
  return @traslados if defined?(@traslados)
data/lib/cfdi40/node.rb CHANGED
@@ -1,13 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cfdi40
4
+ # Main class for build CFDi.
5
+ #
6
+ # Keeps definitions (names, accessors and formats) and
7
+ # relations (parent & children) between nodes
4
8
  class Node
5
9
  # Nokigiri XML Document for the xml_node
6
10
  attr_accessor :xml_document, :xml_parent, :children_nodes, :parent_node
7
11
  attr_writer :element_name
12
+ attr_reader :readonly
8
13
 
9
14
  def initialize
10
15
  self.class.verify_class_variables
16
+ @readonly = readonly
11
17
  @children_nodes = []
12
18
  set_defaults
13
19
  end
@@ -29,16 +35,78 @@ module Cfdi40
29
35
 
30
36
  def self.define_attribute(accessor, xml_attribute:, default: nil, format: nil, readonly: false)
31
37
  verify_class_variables
32
- if readonly
33
- attr_reader accessor.to_sym
34
- else
35
- attr_accessor accessor.to_sym
36
- end
38
+ define_reader(accessor, format)
39
+ define_writer(accessor, readonly, format)
40
+
37
41
  @@attributes[name][accessor.to_sym] = xml_attribute
38
42
  @@default_values[name][accessor.to_sym] = default if default
39
43
  return unless format
40
44
 
41
- @@formats[name][accessor.to_sym] = format
45
+ @@formats[name][accessor.to_sym] = format.to_sym
46
+ end
47
+
48
+ def self.define_reader(accessor, format)
49
+ case format.to_s
50
+ when 't_Importe', 'decimal'
51
+ define_method("#{accessor}".to_sym) do
52
+ value = instance_variable_defined?("@#{accessor}".to_sym) ? instance_variable_get("@#{accessor}".to_sym) : 0
53
+ value.to_f.round(6)
54
+ end
55
+ when 't_ImporteMXN'
56
+ define_method("#{accessor}".to_sym) do
57
+ value = instance_variable_defined?("@#{accessor}".to_sym) ? instance_variable_get("@#{accessor}".to_sym) : 0
58
+ value.to_f.round(2)
59
+ end
60
+ when 't_FechaH', 't_FechaHora'
61
+ define_method("#{accessor}".to_sym) do
62
+ value = instance_variable_defined?("@#{accessor}".to_sym) ? instance_variable_get("@#{accessor}".to_sym) : nil
63
+ return nil unless value.is_a?(Time)
64
+
65
+ value
66
+ end
67
+ else
68
+ define_method("#{accessor}".to_sym) do
69
+ value = instance_variable_defined?("@#{accessor}".to_sym) ? instance_variable_get("@#{accessor}".to_sym) : nil
70
+ return nil if value.nil?
71
+
72
+ value.to_s
73
+ end
74
+ end
75
+ end
76
+
77
+ def self.define_writer(accessor, readonly_attribute, format)
78
+ if readonly_attribute
79
+ define_method "#{accessor}=".to_sym do |value|
80
+ raise Cfdi40::Error, "attribute '#{accessor}' can not be modified"
81
+ end
82
+ else
83
+ case format.to_s
84
+ when 't_FechaH', 't_FechaHora'
85
+ define_method "#{accessor}=".to_sym do |value|
86
+ raise Cfdi40::Error, "CFDI is read only" if self.readonly
87
+
88
+ clean_cached_xml
89
+ if value.nil? || value.is_a?(Time)
90
+ instance_variable_set("@#{accessor}".to_sym, value)
91
+ return
92
+ end
93
+
94
+ begin
95
+ parsed_time = Time.strptime(value.to_s, "%Y-%m-%dT%H:%M:%S")
96
+ instance_variable_set("@#{accessor}".to_sym, parsed_time)
97
+ rescue
98
+ raise Cfdi40::Error, "#{value} must have format 'yyyy-mm-ddTHH:MM:SS'"
99
+ end
100
+ end
101
+ else
102
+ define_method "#{accessor}=".to_sym do |value|
103
+ raise Cfdi40::Error, "CFDI loaded in read only mode" if self.readonly
104
+
105
+ clean_cached_xml
106
+ instance_variable_set("@#{accessor}".to_sym, value)
107
+ end
108
+ end
109
+ end
42
110
  end
43
111
 
44
112
  def self.define_namespace(namespace, value)
@@ -95,6 +163,14 @@ module Cfdi40
95
163
  @children_nodes << child_node
96
164
  end
97
165
 
166
+ # Locks for readonly this node and children
167
+ def lock
168
+ @readonly = true
169
+ @children_nodes.each(&:lock)
170
+
171
+ true
172
+ end
173
+
98
174
  def current_namespace
99
175
  return unless self.class.respond_to?(:namespaces)
100
176
 
@@ -116,8 +192,6 @@ module Cfdi40
116
192
  end
117
193
 
118
194
  def create_xml_node
119
- # TODO: Quitar la siguiente linea (set_defaults) si funciona poniendo los defaults en initialize
120
- # set_defaults
121
195
  before_add if respond_to?(:before_add, true)
122
196
  xml_node = xml_document.create_element(expanded_element_name)
123
197
  add_namespaces_to(xml_node)
@@ -145,12 +219,14 @@ module Cfdi40
145
219
  end
146
220
  end
147
221
 
222
+ # Add defined attributes. Skip unused attributes
148
223
  def add_attributes_to(node)
149
224
  self.class.attributes.each do |object_accessor, xml_attribute|
150
225
  next unless respond_to?(object_accessor)
151
- next if public_send(object_accessor).nil?
226
+ next unless instance_variable_defined?("@#{object_accessor}".to_sym)
227
+ next if instance_variable_get("@#{object_accessor}".to_sym).nil?
152
228
 
153
- node[xml_attribute] = formated_value(object_accessor)
229
+ node[xml_attribute] = formatted_value(object_accessor)
154
230
  end
155
231
  end
156
232
 
@@ -162,15 +238,28 @@ module Cfdi40
162
238
  end
163
239
  end
164
240
 
165
- def formated_value(accessor)
241
+ def formatted_value(accessor)
166
242
  case self.class.formats[accessor]
167
- when :t_Importe
243
+ when :t_Importe, :decimal
168
244
  public_send(accessor).to_f == 0.0 ? "0" : format("%0.6f", public_send(accessor).to_f)
169
245
  when :t_ImporteMXN
170
246
  public_send(accessor).to_f == 0.0 ? "0" : format("%0.2f", public_send(accessor).to_f)
247
+ when :t_FechaH, :t_FechaHora
248
+ value = public_send(accessor)
249
+ value.is_a?(Time) ? value.strftime("%Y-%m-%dT%H:%M:%S") : ''
171
250
  else
172
251
  public_send(accessor)
173
252
  end
174
253
  end
254
+
255
+ # Cleans a Nokogiri object and/or loaded_xml string if exists
256
+ def clean_cached_xml
257
+ @docxml = nil
258
+ @loaded_xml = nil
259
+
260
+ parent_node&.clean_cached_xml
261
+ end
262
+
263
+
175
264
  end
176
265
  end
@@ -0,0 +1,23 @@
1
+ # Extracts the "cadena original" from a XML
2
+ module Cfdi40
3
+ class OriginalContent
4
+ LOCAL_XSLT_PATH = File.join(File.dirname(__FILE__), "..", "..", "lib/xslt/cadenaoriginal_local.xslt")
5
+
6
+ def self.generate(xml_string)
7
+ generator = new(xml_string)
8
+ generator.original_content
9
+ end
10
+
11
+ def initialize(xml)
12
+ @xml_doc = Nokogiri::XML(xml)
13
+ end
14
+
15
+ def original_content
16
+ xslt = Nokogiri::XSLT(File.open(LOCAL_XSLT_PATH))
17
+ transformed = xslt.transform(@xml_doc)
18
+ # The ampersand (&) char must be used in original content
19
+ # even though the documentation indicates otherwise
20
+ transformed.children.to_s.gsub("&amp;", "&").strip
21
+ end
22
+ end
23
+ end
data/lib/cfdi40/pago.rb CHANGED
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # /cfdi:Comprobante/cfdi:Complemento/pago20:Pagos/pago20:Pago
3
4
  module Cfdi40
4
5
  class Pago < Node
5
- define_attribute :monto, xml_attribute: "Monto"
6
- define_attribute :fecha_pago, xml_attribute: "FechaPago"
6
+ define_attribute :monto, xml_attribute: "Monto", format: :t_Importe
7
+ define_attribute :fecha_pago, xml_attribute: "FechaPago", format: :t_FechaH
7
8
  define_attribute :forma_pago, xml_attribute: "FormaDePagoP"
8
9
  define_attribute :moneda, xml_attribute: "MonedaP", readonly: true, default: "MXN"
9
10
  define_attribute :tipo_cambio, xml_attribute: "TipoCambioP", readonly: true, default: "1"
data/lib/cfdi40/pagos.rb CHANGED
@@ -33,7 +33,7 @@ module Cfdi40
33
33
  end
34
34
 
35
35
  def update_totales_traslado_iva16
36
- key = ["002", "Tasa", "0.160000"]
36
+ key = ["002", "Tasa", 0.160000]
37
37
  return if traslados_summary[key].nil?
38
38
 
39
39
  totales_node.base_iva16 = traslados_summary[key][:base]
@@ -61,7 +61,7 @@ module Cfdi40
61
61
  def totales_node
62
62
  return @totales_node if defined?(@totales_node)
63
63
 
64
- @totales_node = Totales.new
64
+ @totales_node = CpTotales.new
65
65
  add_child_node @totales_node
66
66
  @totales_node
67
67
  end
@@ -13,6 +13,12 @@ module Cfdi40
13
13
  @x509_cert = OpenSSL::X509::Certificate.new(data)
14
14
  end
15
15
 
16
+ # Loads certficate encoded in Base64.
17
+ # Certs with Base64 encoding are used in CFDIs
18
+ def cert64=(data)
19
+ self.cert_der = Base64.decode64(data)
20
+ end
21
+
16
22
  def load_private_key(key_path, key_pass)
17
23
  key_pem = key_to_pem(File.read(key_path))
18
24
  @private_key = OpenSSL::PKey::RSA.new(key_pem, key_pass)
@@ -70,7 +76,14 @@ module Cfdi40
70
76
  def subject_data
71
77
  return unless x509_cert
72
78
 
73
- x509_cert.subject.to_a
79
+ # The #to_a method doesn't convert to_utf8
80
+ # https://ruby-doc.org/core-3.1.2/OpenSSL/X509/Name.html
81
+ #
82
+ # A simpliest
83
+ # x509_cert.subject.to_a
84
+ # could't be used so, we need first convert to UTF-8
85
+
86
+ x509_cert.subject.to_utf8.split(',').map { |str| str.split '=' }
74
87
  end
75
88
 
76
89
  def key_to_pem(key_der)
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Verfies signature
4
+ module Cfdi40
5
+ class SignatureValidator
6
+ def initialize(xml)
7
+ @xml_doc = Nokogiri::XML(xml)
8
+
9
+ end
10
+
11
+ def valid?
12
+ original_content = OriginalContent.generate(@xml_doc.to_s)
13
+ cert.public_key.verify(OpenSSL::Digest.new('SHA256'), sign, original_content)
14
+ end
15
+
16
+ def sign
17
+ sign = @xml_doc.root.attributes["Sello"].to_s
18
+ raise Cfdi40::Error, 'CFDI is not signed' if sign == ''
19
+
20
+ Base64.decode64 sign
21
+ end
22
+
23
+ def cert
24
+ return @cert if defined?(@cert)
25
+
26
+ cert_string = @xml_doc.root.attributes["Certificado"].to_s
27
+ raise Cfdi40::Error, 'Certificate is not included in XML' if cert_string == ''
28
+ sat_csd = SatCsd.new
29
+ sat_csd.cert64 = cert_string
30
+ sat_csd.x509_cert.serial
31
+
32
+ if @xml_doc.root.attributes["NoCertificado"].to_s != sat_csd.no_certificado
33
+ raise Cfdi40::Error, 'Certificate number in XML does not correspond to the included certificate'
34
+ end
35
+
36
+ @cert = sat_csd.x509_cert
37
+ end
38
+ end
39
+ end
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # /cfdi:Comprobante/cfdi:Impuestos/cfdi:Traslados/cfdi:Traslado
3
4
  module Cfdi40
4
5
  class Traslado < Node
5
- define_attribute :base, xml_attribute: "Base", format: :t_ImporteMXN
6
+ define_attribute :base, xml_attribute: "Base", format: :t_Importe
6
7
  define_attribute :impuesto, xml_attribute: "Impuesto"
7
8
  define_attribute :tipo_factor, xml_attribute: "TipoFactor", default: "Tasa"
8
9
  define_attribute :tasa_o_cuota, xml_attribute: "TasaOCuota", format: :t_Importe
9
- define_attribute :importe, xml_attribute: "Importe", format: :t_ImporteMXN
10
+ define_attribute :importe, xml_attribute: "Importe", format: :t_Importe
10
11
  end
11
12
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # /cfdi:Comprobante/cfdi:Impuestos/cfdi:Traslados
3
4
  module Cfdi40
4
5
  class Traslados < Node
5
6
  def traslado_iva
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cfdi40
4
- VERSION = "0.1.2"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -4,10 +4,11 @@
4
4
  # from an XML (string)
5
5
  module Cfdi40
6
6
  class XmlLoader
7
- attr_reader :cfdi, :xml_doc
7
+ attr_reader :cfdi, :xml_doc, :mode
8
8
 
9
- def initialize(xml_string)
9
+ def initialize(xml_string, mode)
10
10
  @xml_doc = Nokogiri::XML(xml_string)
11
+ @mode = mode
11
12
  # TODO. validar versión del CFDI definido en xml_doc
12
13
  @cfdi = Cfdi40::Comprobante.new
13
14
  @cfdi.load_from_ng_node(xml_doc.root)
@@ -16,11 +17,20 @@ module Cfdi40
16
17
  load_receptor
17
18
  load_conceptos
18
19
  load_impuestos
20
+
21
+ @cfdi.lock if readonly?
22
+ @cfdi.loaded_xml = xml_string
19
23
  @cfdi
20
24
  end
21
25
 
22
26
  private
23
27
 
28
+ def readonly?
29
+ return true if mode == 'ro'
30
+
31
+ @xml_doc.root.attributes["Sello"].to_s != ''
32
+ end
33
+
24
34
  def load_conceptos
25
35
  n_concepto = 0
26
36
  xml_doc.xpath("//cfdi:Concepto").each do |node|
data/lib/cfdi40.rb CHANGED
@@ -3,8 +3,11 @@
3
3
  require "nokogiri"
4
4
  require "base64"
5
5
  require "openssl"
6
+ require "time"
6
7
  require_relative "cfdi40/version"
7
8
  require_relative "cfdi40/schema_validator"
9
+ require_relative "cfdi40/signature_validator"
10
+ require_relative "cfdi40/original_content"
8
11
  require_relative "cfdi40/sat_csd"
9
12
  require_relative "cfdi40/node"
10
13
  require_relative "cfdi40/comprobante"
@@ -12,6 +15,7 @@ require_relative "cfdi40/emisor"
12
15
  require_relative "cfdi40/receptor"
13
16
  require_relative "cfdi40/conceptos"
14
17
  require_relative "cfdi40/concepto"
18
+ require_relative "cfdi40/concepto_impuestos"
15
19
  require_relative "cfdi40/impuestos"
16
20
  require_relative "cfdi40/traslados"
17
21
  require_relative "cfdi40/traslado"
@@ -27,7 +31,7 @@ require_relative "cfdi40/traslado_dr"
27
31
  require_relative "cfdi40/impuestos_p"
28
32
  require_relative "cfdi40/traslados_p"
29
33
  require_relative "cfdi40/traslado_p"
30
- require_relative "cfdi40/totales"
34
+ require_relative "cfdi40/cp_totales"
31
35
  require_relative "cfdi40/xml_loader"
32
36
 
33
37
  # Leading module and entry point for all features and classes
@@ -42,8 +46,15 @@ module Cfdi40
42
46
  Comprobante.new
43
47
  end
44
48
 
45
- def self.open(xml_string, mode: :readwrite)
46
- loader = XmlLoader.new(xml_string)
49
+ # Modes:
50
+ # * 'rw' read and write
51
+ # * 'ro' read only
52
+ def self.open(xml_string, mode: 'rw')
53
+ unless mode.nil? || %w[rw ro].include?(mode)
54
+ STDERR.puts "Unknow mode '#{mode}' ignored. Valid modes are 'ro', 'rw'"
55
+ mode = nil
56
+ end
57
+ loader = XmlLoader.new(xml_string, mode)
47
58
  loader.cfdi
48
59
  end
49
60
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cfdi40
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Israel Benítez
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-06-19 00:00:00.000000000 Z
11
+ date: 2025-08-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -47,7 +47,9 @@ files:
47
47
  - lib/cfdi40/complemento_concepto.rb
48
48
  - lib/cfdi40/comprobante.rb
49
49
  - lib/cfdi40/concepto.rb
50
+ - lib/cfdi40/concepto_impuestos.rb
50
51
  - lib/cfdi40/conceptos.rb
52
+ - lib/cfdi40/cp_totales.rb
51
53
  - lib/cfdi40/docto_relacionado.rb
52
54
  - lib/cfdi40/emisor.rb
53
55
  - lib/cfdi40/impuestos.rb
@@ -55,12 +57,13 @@ files:
55
57
  - lib/cfdi40/impuestos_p.rb
56
58
  - lib/cfdi40/inst_educativas.rb
57
59
  - lib/cfdi40/node.rb
60
+ - lib/cfdi40/original_content.rb
58
61
  - lib/cfdi40/pago.rb
59
62
  - lib/cfdi40/pagos.rb
60
63
  - lib/cfdi40/receptor.rb
61
64
  - lib/cfdi40/sat_csd.rb
62
65
  - lib/cfdi40/schema_validator.rb
63
- - lib/cfdi40/totales.rb
66
+ - lib/cfdi40/signature_validator.rb
64
67
  - lib/cfdi40/traslado.rb
65
68
  - lib/cfdi40/traslado_dr.rb
66
69
  - lib/cfdi40/traslado_p.rb