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 +4 -4
- data/lib/cfdi40/comprobante.rb +25 -9
- data/lib/cfdi40/concepto.rb +4 -4
- data/lib/cfdi40/concepto_impuestos.rb +31 -0
- data/lib/cfdi40/{totales.rb → cp_totales.rb} +13 -12
- data/lib/cfdi40/docto_relacionado.rb +1 -1
- data/lib/cfdi40/impuestos.rb +3 -2
- data/lib/cfdi40/node.rb +101 -12
- data/lib/cfdi40/original_content.rb +23 -0
- data/lib/cfdi40/pago.rb +3 -2
- data/lib/cfdi40/pagos.rb +2 -2
- data/lib/cfdi40/sat_csd.rb +14 -1
- data/lib/cfdi40/signature_validator.rb +39 -0
- data/lib/cfdi40/traslado.rb +3 -2
- data/lib/cfdi40/traslados.rb +1 -0
- data/lib/cfdi40/version.rb +1 -1
- data/lib/cfdi40/xml_loader.rb +12 -2
- data/lib/cfdi40.rb +14 -3
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cad6896abeef260eaf8d4f9e659da6b6572fd693e51713261c982addf854373a
|
4
|
+
data.tar.gz: c68c19bb783b5864c81103899d5ab46b1281d6eccaecbd43efee35cf927777d6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ffc2e71680140e910f44dc35c5201ad28747454d4167290c57f52836bbd75dcae1aaa6768735d7435b2510cd96b6d9be6d271dddddce59e598c86ae71a502f1
|
7
|
+
data.tar.gz: 8649c8ade0dbbbee0caf4e9a31692d30874eb86ee5712ae3144501cd41614e6739c3d7d90b9e2584d561d7856345ad6b37063921671d50daa76c60b95d907d62
|
data/lib/cfdi40/comprobante.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
-
|
213
|
-
|
214
|
-
|
215
|
-
# The ampersand (&) char must be used in original content
|
216
|
-
# even though the documentation indicates otherwise
|
217
|
-
transformed.children.to_s.gsub("&", "&").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
|
data/lib/cfdi40/concepto.rb
CHANGED
@@ -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?(
|
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?(
|
143
|
+
@impuestos_node = children_nodes.select { |child| child.is_a?(ConceptoImpuestos) }.first
|
144
144
|
return if @impuestos_node
|
145
145
|
|
146
|
-
@impuestos_node =
|
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
|
5
|
-
|
6
|
-
define_attribute :
|
7
|
-
define_attribute :
|
8
|
-
define_attribute :
|
9
|
-
define_attribute :
|
10
|
-
define_attribute :
|
11
|
-
define_attribute :
|
12
|
-
define_attribute :
|
13
|
-
define_attribute :
|
14
|
-
define_attribute :
|
15
|
-
define_attribute :
|
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
|
data/lib/cfdi40/impuestos.rb
CHANGED
@@ -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: :
|
6
|
-
define_attribute :total_impuestos_trasladados, xml_attribute: "TotalImpuestosTrasladados", format: :
|
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
|
-
|
33
|
-
|
34
|
-
|
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
|
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] =
|
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
|
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("&", "&").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",
|
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 =
|
64
|
+
@totales_node = CpTotales.new
|
65
65
|
add_child_node @totales_node
|
66
66
|
@totales_node
|
67
67
|
end
|
data/lib/cfdi40/sat_csd.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/cfdi40/traslado.rb
CHANGED
@@ -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: :
|
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: :
|
10
|
+
define_attribute :importe, xml_attribute: "Importe", format: :t_Importe
|
10
11
|
end
|
11
12
|
end
|
data/lib/cfdi40/traslados.rb
CHANGED
data/lib/cfdi40/version.rb
CHANGED
data/lib/cfdi40/xml_loader.rb
CHANGED
@@ -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/
|
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
|
-
|
46
|
-
|
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.
|
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-
|
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/
|
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
|