cfdi40 0.0.1.alfa → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +10 -5
  4. data/README_es-MX.md +62 -0
  5. data/cfdi40.gemspec +2 -2
  6. data/lib/cfdi40/comprobante.rb +215 -20
  7. data/lib/cfdi40/concepto.rb +126 -0
  8. data/lib/cfdi40/conceptos.rb +4 -0
  9. data/lib/cfdi40/emisor.rb +7 -0
  10. data/lib/cfdi40/impuestos.rb +28 -0
  11. data/lib/cfdi40/node.rb +122 -0
  12. data/lib/cfdi40/receptor.rb +11 -0
  13. data/lib/cfdi40/sat_csd.rb +83 -0
  14. data/lib/cfdi40/schema_validator.rb +29 -0
  15. data/lib/cfdi40/traslado.rb +9 -0
  16. data/lib/cfdi40/traslados.rb +17 -0
  17. data/lib/cfdi40/version.rb +1 -1
  18. data/lib/cfdi40.rb +12 -0
  19. data/lib/xsd/README.md +30 -0
  20. data/lib/xsd/catCFDI.xsd +162329 -0
  21. data/lib/xsd/cfdv40.xsd +856 -0
  22. data/lib/xsd/tdCFDI.xsd +157 -0
  23. data/lib/xslt/CartaPorte20.xslt +615 -0
  24. data/lib/xslt/GastosHidrocarburos10.xslt +171 -0
  25. data/lib/xslt/IngresosHidrocarburos.xslt +39 -0
  26. data/lib/xslt/Pagos20.xslt +233 -0
  27. data/lib/xslt/TuristaPasajeroExtranjero.xslt +40 -0
  28. data/lib/xslt/aerolineas.xslt +50 -0
  29. data/lib/xslt/cadenaoriginal.xslt +405 -0
  30. data/lib/xslt/cadenaoriginal_local.xslt +405 -0
  31. data/lib/xslt/certificadodedestruccion.xslt +60 -0
  32. data/lib/xslt/cfdiregistrofiscal.xslt +19 -0
  33. data/lib/xslt/consumodeCombustibles11.xslt +94 -0
  34. data/lib/xslt/detallista.xslt +42 -0
  35. data/lib/xslt/divisas.xslt +13 -0
  36. data/lib/xslt/donat11.xslt +13 -0
  37. data/lib/xslt/iedu.xslt +26 -0
  38. data/lib/xslt/implocal.xslt +39 -0
  39. data/lib/xslt/ine11.xslt +30 -0
  40. data/lib/xslt/leyendasFisc.xslt +28 -0
  41. data/lib/xslt/nomina12.xslt +412 -0
  42. data/lib/xslt/notariospublicos.xslt +301 -0
  43. data/lib/xslt/obrasarteantiguedades.xslt +33 -0
  44. data/lib/xslt/pagoenespecie.xslt +39 -0
  45. data/lib/xslt/pfic.xslt +13 -0
  46. data/lib/xslt/renovacionysustitucionvehiculos.xslt +152 -0
  47. data/lib/xslt/servicioparcialconstruccion.xslt +44 -0
  48. data/lib/xslt/utilerias.xslt +22 -0
  49. data/lib/xslt/valesdedespensa.xslt +70 -0
  50. data/lib/xslt/vehiculousado.xslt +63 -0
  51. metadata +50 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c30e39f4584ad4ec24f70ae950700568d72b0941adb3535ece9132f0ff7f3d19
4
- data.tar.gz: 9dc2cf08802bee96c4a2cfa362101be5ac6926bf67f613f1d2293b6480c6316c
3
+ metadata.gz: 000e715fcb57e869f47058bb05bc2cadc558f188db6da41e1c0d0240873d910b
4
+ data.tar.gz: 2ab314d7b2477308a4cc073f8ebe35a31098b8ac5ecb23aa0dd4d43babc95ae8
5
5
  SHA512:
6
- metadata.gz: '070922f0002e8948bec5ce379e198b61a442bcc54ba47ac45f9f46f2bd58bbae9bcdb4b54b5527bc2996a32c793d8f7ea75235bc2b96925f0cdb4d0ef3c4783a'
7
- data.tar.gz: 552352ed70288e9307c6eb065fe57d0d45f84be27085ceb4698fd19a16ec3f0042ced1f57d7acdb467c254ff504a5bb60522402874e736da1e2f2d6b7cf30271
6
+ metadata.gz: fccd3b919ade4540d90a2b9ff81778e68cd900f121c7e92d4e4bb192585d5bb0d921a1a148bca0df23a5f26ab7da0dc5de165d7f068352701a93f2936d65792e
7
+ data.tar.gz: ae39a825b18d9f3cbbbd8403b6afb4a920bc4abd61479777d35d774bf5fe44426dd1b472856f966f95b10b50b04ee28773c121431f6d3c4990127bacb02fd09a
data/Gemfile.lock CHANGED
@@ -2,7 +2,7 @@ PATH
2
2
  remote: .
3
3
  specs:
4
4
  cfdi40 (0.0.1.alfa)
5
- nokogiri (~> 1.13, >= 1.13.9)
5
+ nokogiri (>= 1.10.10)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -1,14 +1,19 @@
1
1
  # Cfdi40
2
2
 
3
- Tool for read, create, edit, validate and sign CFDIs version 4.0
3
+ Tool for read, create, validate and sign CFDIs version 4.0
4
4
 
5
5
  CFDI (Comprobante Fiscal Digital por Internet) are XML documents
6
6
  regulated by mexican goverment for tax purpouses.
7
7
 
8
- ## Future features
8
+ Please see `README_es-MX.md`
9
+
10
+ TODO: Document, document, document
11
+
12
+ ## Features
9
13
 
10
- * Create and sign a valid XML of an invoice for general public
11
- (público en general) with minimum data
14
+ * XML generation and sign.
15
+
16
+ ## Future features
12
17
 
13
18
  ## Installation
14
19
 
@@ -48,7 +53,7 @@ Bug reports and pull requests are welcome on GitHub at
48
53
  https://github.com/[USERNAME]/cfdi40. This project is intended to be a
49
54
  safe, welcoming space for collaboration, and contributors are expected
50
55
  to adhere to the [code of
51
- conduct](https://github.com/[USERNAME]/cfdi40/blob/master/CODE_OF_CONDUCT.md).
56
+ conduct](https://github.com/israelbz/cfdi40/blob/master/CODE_OF_CONDUCT.md).
52
57
 
53
58
  ## License
54
59
 
data/README_es-MX.md ADDED
@@ -0,0 +1,62 @@
1
+ # Cfdi40
2
+
3
+ Herramienta para crear, leer, validar y firmam CFDis en
4
+ versión 4.0.
5
+
6
+ El CFDi (Comprobante Fiscal Digital por internet) es un
7
+ documento en formato XML usado en México.
8
+
9
+ Esta herramienta tiene la intención de simplificar la
10
+ generación de los archivos XML, para ello, se pretende
11
+ que esta herramienta:
12
+
13
+ * Ofrezca una interfaz simple para colocar la
14
+ información esencial para la elaboración del CFDi.
15
+ * Realice los cálculos complementarios como impuestos,
16
+ totales, etcétera.
17
+ * Valide el CFDi contra los CSD
18
+ * Selle el CFDi.
19
+
20
+ # Uso
21
+
22
+ ## Ejemplo básico
23
+
24
+ # Inicia un cfdi
25
+ cfdi = Cfdi40.new
26
+
27
+ # Datos del emisor
28
+ cfdi.lugar_expedicion = '06000'
29
+ cfdi.emisor.regimen_fiscal = '612'
30
+
31
+ # Datos del receptor
32
+ cfdi.receptor.nombre = 'JUAN PUEBLO BUENO'
33
+ cfdi.receptor.rfc = 'XAXX010101000'
34
+ cfdi.receptor.domicilio_fiscal = '06000'
35
+ cfdi.receptor.regimen_fiscal = '616'
36
+ cfdi.receptor.uso_cfdi = 'G03'
37
+
38
+ # Agrega un concepto en pesos,
39
+ # precio final al cliente (neto)
40
+ # causa IVA con tasa de 16% (default)
41
+ cfdi.add_concepto(
42
+ clave_prod_serv: '81111500',
43
+ clave_unidad: "E48",
44
+ descripcion: 'Prueba de concepto',
45
+ precio_neto: 40
46
+ )
47
+
48
+ # Archivos CSD
49
+ cfdi.cert_path = '/path_to/certificado.cer'
50
+ cfdi.key_path = '/path_to/llave_privada.key'
51
+ cfdi.key_pass = 'contraseña'
52
+
53
+ # Genera CFDI firmado
54
+ xml_string = cfdi.to_xml
55
+
56
+
57
+ # ¿Que sigue?
58
+
59
+ * Complemento de pagos
60
+ * Retenciones
61
+ * IEPS
62
+ * Complemento para colegiaturas
data/cfdi40.gemspec CHANGED
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
14
14
  "regulated by Mexican Government"
15
15
  spec.homepage = "https://github.com/israelbz/cfdi40"
16
16
  spec.license = "MIT"
17
- spec.required_ruby_version = ">= 2.6.0"
17
+ spec.required_ruby_version = ">= 2.3.3"
18
18
 
19
19
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
20
20
 
@@ -34,7 +34,7 @@ Gem::Specification.new do |spec|
34
34
  spec.require_paths = ["lib"]
35
35
 
36
36
  # Uncomment to register a new dependency of your gem
37
- spec.add_dependency "nokogiri", "~> 1.13", ">= 1.13.9"
37
+ spec.add_dependency "nokogiri", ">= 1.10.10"
38
38
 
39
39
  # For more information and examples about making a new gem, check out our
40
40
  # guide at: https://bundler.io/guides/creating_gem.html
@@ -1,30 +1,225 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Create and Read XML documents
4
- class Comprobante
5
- attr_reader :docxml
4
+ module Cfdi40
5
+ class Comprobante < Node
6
+ define_namespace "xsi", "http://www.w3.org/2001/XMLSchema-instance"
7
+ define_namespace "cfdi", "http://www.sat.gob.mx/cfd/4"
8
+ define_attribute :schema_location,
9
+ xml_attribute: 'xsi:schemaLocation',
10
+ readonly: true,
11
+ default: "http://www.sat.gob.mx/cfd/4 " \
12
+ "http://www.sat.gob.mx/sitio_internet/cfd/4/cfdv40.xsd"
13
+ define_attribute :version, xml_attribute: 'Version', readonly: true, default: '4.0'
14
+ define_attribute :serie, xml_attribute: 'Serie'
15
+ define_attribute :folio, xml_attribute: 'Folio'
16
+ define_attribute :fecha, xml_attribute: 'Fecha'
17
+ define_attribute :sello, xml_attribute: 'Sello', readonly: true
18
+ define_attribute :forma_pago, xml_attribute: 'FormaPago'
19
+ define_attribute :no_certificado, xml_attribute: 'NoCertificado'
20
+ define_attribute :certificado, xml_attribute: 'Certificado'
21
+ define_attribute :condiciones_de_pago, xml_attribute: 'CondicionesDePago'
22
+ define_attribute :subtotal, xml_attribute: 'SubTotal', format: :t_Importe
23
+ define_attribute :descuento, xml_attribute: 'Descuento', format: :t_Importe
24
+ define_attribute :moneda, xml_attribute: 'Moneda', default: 'MXN'
25
+ define_attribute :tipo_cambio, xml_attribute: 'TipoCambio'
26
+ define_attribute :total, xml_attribute: 'Total', format: :t_Importe
27
+ define_attribute :tipo_de_comprobante, xml_attribute: 'TipoDeComprobante', default: 'I'
28
+ define_attribute :exportacion, xml_attribute: 'Exportacion', default: '01'
29
+ define_attribute :metodo_pago, xml_attribute: 'MetodoPago'
30
+ define_attribute :lugar_expedicion, xml_attribute: 'LugarExpedicion'
31
+ define_attribute :confirmacion, xml_attribute: 'Confirmacion'
6
32
 
7
- def initialize
8
- new_docxml
9
- end
33
+ attr_reader :emisor, :receptor, :x509_cert, :conceptos, :private_key
34
+ attr_reader :errors
35
+ attr_writer :key_der, :key_pass
10
36
 
11
- def to_s
12
- docxml.to_xml
13
- end
37
+ def initialize
38
+ super
39
+ @errors = []
40
+ @conceptos = Conceptos.new
41
+ @emisor = Emisor.new
42
+ @receptor = Receptor.new
43
+ @sat_csd = SatCsd.new
44
+ @fecha ||= Time.now.strftime("%Y-%m-%dT%H:%M:%S")
45
+ @children_nodes = [@emisor, @receptor, @conceptos]
46
+ set_defaults
47
+ end
48
+
49
+ # Accept a path to read the certificate.
50
+ # Certificate is a X509 file. SAT generates those files in
51
+ # DER format.
52
+ def cert_path=(path)
53
+ self.cert_der = File.read(path)
54
+ end
55
+
56
+ def cert_der=(cert_data)
57
+ @sat_csd ||= SatCsd.new
58
+ @sat_csd.cert_der = cert_data
59
+ emisor.rfc = @sat_csd.rfc
60
+ emisor.nombre = @sat_csd.name
61
+ @no_certificado = @sat_csd.no_certificado
62
+ @certificado = @sat_csd.cert64
63
+ true
64
+ end
65
+
66
+ def key_path=(path)
67
+ @key_der = File.read(path)
68
+ end
69
+
70
+ def sign
71
+ @sat_csd ||= SatCsd.new
72
+ load_private_key if @sat_csd.private_key.nil?
73
+ return unless @sat_csd.private_key
74
+
75
+ raise Error, 'Key and certificate not match' unless @sat_csd.valid_pair?
76
+
77
+ digest = @sat_csd.private_key.sign(OpenSSL::Digest.new('SHA256'), original_content)
78
+ @sello = Base64.strict_encode64 digest
79
+ @docxml = nil
80
+ end
81
+
82
+ # clave_prod_serv
83
+ # no_identificacion
84
+ # cantidad
85
+ # clave_unidad
86
+ # unidad
87
+ # descripcion
88
+ # valor_unitario
89
+ # importe
90
+ # descuento
91
+ # objeto_imp
92
+ def add_concepto(attributes = {})
93
+ concepto = Concepto.new
94
+ attributes.each do |key, value|
95
+ method_name = "#{key}=".to_sym
96
+ if concepto.respond_to?(method_name)
97
+ concepto.public_send(method_name, value)
98
+ else
99
+ raise Error, ":#{key} no se puede asignar al concepto"
100
+ end
101
+ end
102
+ concepto.calculate!
103
+ @conceptos.children_nodes << concepto
104
+ calculate!
105
+ concepto
106
+ end
107
+
108
+ def to_s
109
+ to_xml
110
+ end
111
+
112
+ def to_xml
113
+ sign
114
+ docxml.to_xml
115
+ end
116
+
117
+ def valid?
118
+ schema_validator = SchemaValidator.new(to_s)
119
+ return true if schema_validator.valid?
120
+
121
+ @errors = schema_validator.errors
122
+ @errors.empty?
123
+ end
124
+
125
+ def cadena_original
126
+ original_content
127
+ end
128
+
129
+ def original_content
130
+ xslt = Nokogiri::XSLT(File.open('lib/xslt/cadenaoriginal_local.xslt'))
131
+ transformed = xslt.transform(docxml)
132
+ # The ampersand (&) char must be used in original content
133
+ # even though the documentation indicates otherwise
134
+ transformed.children.to_s.gsub('&amp;', '&').strip
135
+ end
136
+
137
+ private
138
+
139
+ def docxml
140
+ return @docxml if defined?(@docxml) && !@docxml.nil?
141
+
142
+ @docxml = Nokogiri::XML::Document.new("1.0")
143
+ @docxml.encoding = "utf-8"
144
+ add_root_node
145
+ @docxml
146
+ end
147
+
148
+ def add_root_node
149
+ self.xml_document = @docxml
150
+ self.xml_parent = @docxml
151
+ create_xml_node
152
+ end
153
+
154
+ def calculate!
155
+ @subtotal = @conceptos.children_nodes.map(&:importe).sum
156
+ @total = @conceptos.children_nodes.map(&:importe_neto).sum
157
+ summarize_traslados
158
+ end
159
+
160
+ def summarize_traslados
161
+ impuestos.total_impuestos_trasladados = 0
162
+ traslados.children_nodes = []
163
+ traslados_summary.each do |key, value|
164
+ #TODO: Sumar los impuestos y agregarlos a los nodos globales de traslados
165
+ traslado = Traslado.new
166
+ traslado.impuesto, traslado.tasa_o_cuota, traslado.tipo_factor = key
167
+ traslado.base = value[:base]
168
+ traslado.importe = value[:importe]
169
+ traslados.children_nodes << traslado
170
+ impuestos.total_impuestos_trasladados += value[:importe]
171
+ end
172
+ end
173
+
174
+ def concepto_nodes
175
+ @conceptos.children_nodes
176
+ end
177
+
178
+ # Returns a hash with a summary.
179
+ # The key is an Array ['impuesto, 'tasa_o_cuota', 'TipoFactor] and the value is
180
+ # another hash the sum of 'Importe' and 'Base'
181
+ def traslados_summary
182
+ summary = {}
183
+ concepto_nodes.map(&:traslado_nodes).flatten.each do |traslado|
184
+ key = [traslado.impuesto, traslado.tasa_o_cuota, traslado.tipo_factor]
185
+ summary[key] ||= { base: 0, importe: 0 }
186
+ summary[key][:base] += traslado.base
187
+ summary[key][:importe] += traslado.importe
188
+ end
189
+ summary
190
+ end
191
+
192
+ # TODO: Este método tiene que ser 'impuestos'
193
+ # si nos atenemos a que los que acaban con _node buscan en los hijos
194
+ # y los que no terminan con _node crean el nodo
195
+ def impuestos
196
+ return @impuestos if defined?(@impuestos)
197
+
198
+ @impuestos = Impuestos.new
199
+ @children_nodes << @impuestos
200
+ @impuestos
201
+ end
202
+
203
+ def impuestos_node
204
+ children_nodes.select { |n| n.is_a?(Impuestos)}.first
205
+ end
206
+
207
+ def traslados
208
+ return nil if impuestos_node.nil?
14
209
 
15
- private
210
+ impuestos_node.traslados
211
+ end
16
212
 
17
- def new_docxml
18
- @docxml = Nokogiri::XML::Document.new("1.0")
19
- @docxml.encoding = "utf-8"
20
- root = @docxml.create_element "cfdi:Comprobante"
21
- root.add_namespace "xsi", "http://www.w3.org/2001/XMLSchema-instance"
22
- root.add_namespace "cfdi", "http://www.sat.gob.mx/cfd/4"
23
- root["xsi:schemaLocation"] = "http://www.sat.gob.mx/cfd/3 " \
24
- "http://www.sat.gob.mx/sitio_internet/cfd/4/cfdv40.xsd"
25
- root["Version"] = "4.0"
213
+ # Eliminar
214
+ def traslado_iva_node
215
+ impuestos_node.traslado_iva
216
+ end
26
217
 
27
- @docxml.add_child root
28
- @docxml
218
+ def load_private_key
219
+ return unless defined?(@key_der) && defined?(@key_pass)
220
+
221
+ @sat_csd ||= SatCsd.new
222
+ @sat_csd.set_crypted_private_key(@key_der, @key_pass)
223
+ end
29
224
  end
30
225
  end
@@ -0,0 +1,126 @@
1
+ # Represents node 'concepto'
2
+ #
3
+ # * Attribute +Importe+ represente gross amount. Gross amount id before taxes and the result of multiply
4
+ # +ValorUnitario+ by +Cantidad+
5
+ #
6
+ module Cfdi40
7
+ class Concepto < Node
8
+ define_attribute :clave_prod_serv, xml_attribute: 'ClaveProdServ'
9
+ define_attribute :no_identificacion,xml_attribute: 'NoIdentificacion'
10
+ define_attribute :cantidad, xml_attribute: 'Cantidad', default: 1
11
+ define_attribute :clave_unidad, xml_attribute: 'ClaveUnidad'
12
+ define_attribute :unidad, xml_attribute: 'Unidad'
13
+ define_attribute :descripcion, xml_attribute: 'Descripcion'
14
+ define_attribute :valor_unitario, xml_attribute: 'ValorUnitario', format: :t_Importe
15
+ define_attribute :importe, xml_attribute: 'Importe', format: :t_Importe
16
+ define_attribute :descuento, xml_attribute: 'Descuento', format: :t_Importe
17
+ define_attribute :objeto_impuestos, xml_attribute: 'ObjetoImp', default: '01'
18
+
19
+ attr_accessor :tasa_iva, :tasa_ieps, :precio_neto, :precio_bruto
20
+ attr_reader :iva, :ieps, :base_iva, :importe_neto, :importe_bruto
21
+
22
+ def initialize
23
+ @tasa_iva = 0.16
24
+ @tasa_ieps = 0
25
+ super
26
+ end
27
+
28
+ # Calculate taxes, amounts from gross price
29
+ # or net price
30
+ def calculate!
31
+ set_defaults
32
+ assign_objeto_imp
33
+ if defined?(@precio_neto) && !@precio_neto.nil?
34
+ calculate_from_net_price
35
+ elsif defined?(@precio_bruto) && !@precio_bruto.nil?
36
+ calculate_from_gross_price
37
+ elsif !self.valor_unitario.nil?
38
+ @precio_bruto = valor_unitario
39
+ calculate_from_gross_price
40
+ end
41
+ add_info_to_traslado_iva
42
+ # TODO: add_info_to_traslado_ieps if @ieps > 0
43
+ true
44
+ end
45
+
46
+ def objeto_impuestos?
47
+ objeto_impuestos == '02'
48
+ end
49
+
50
+ def traslado_nodes
51
+ return [] if impuestos_node.nil?
52
+
53
+ impuestos_node.traslado_nodes
54
+ end
55
+
56
+ private
57
+
58
+ def calculate_from_net_price
59
+ set_defaults
60
+ @importe_neto = precio_neto * cantidad
61
+ breakdown_taxes
62
+ update_xml_attributes
63
+ end
64
+
65
+ def breakdown_taxes
66
+ @base_iva = @importe_neto / (1 + tasa_iva)
67
+ @iva = @importe_neto - @base_iva
68
+ @base_ieps = @base_iva / (1 + tasa_ieps)
69
+ @ieps = @base_iva - @base_ieps
70
+ @importe_bruto = @base_ieps
71
+ @precio_bruto = @importe_bruto / @cantidad
72
+ end
73
+
74
+ def calculate_from_gross_price
75
+ @importe_bruto = @precio_bruto * cantidad
76
+ add_taxes
77
+ update_xml_attributes
78
+ end
79
+
80
+ def add_taxes
81
+ @base_ieps = @importe_bruto
82
+ @ieps = @base_ieps * tasa_ieps
83
+ @base_iva = @base_ieps + @ieps
84
+ @iva = @base_iva * tasa_iva
85
+ @importe_neto = @base_iva + @iva
86
+ @precio_neto = @importe_neto / cantidad
87
+ end
88
+
89
+ def update_xml_attributes
90
+ self.importe = @importe_bruto
91
+ self.valor_unitario = @precio_bruto
92
+ end
93
+
94
+ def add_info_to_traslado_iva
95
+ return unless @iva > 0
96
+
97
+ traslado_iva_node.importe = @iva
98
+ traslado_iva_node.base = @base_iva
99
+ traslado_iva_node.tasa_o_cuota = @tasa_iva
100
+ end
101
+
102
+ def assign_objeto_imp
103
+ return if objeto_impuestos == '03'
104
+
105
+ self.objeto_impuestos = (@tasa_iva > 0 || @tasa_ieps > 0 ? '02' : '01')
106
+ end
107
+
108
+ def impuestos_node
109
+ return @impuestos_node if defined?(@impuestos_node)
110
+ return nil unless objeto_impuestos?
111
+
112
+ @impuestos_node = children_nodes.select { |child| child.is_a?(Impuestos) }.first
113
+ return if @impuestos_node
114
+
115
+ @impuestos_node = Impuestos.new
116
+ self.children_nodes << @impuestos_node
117
+ @impuestos_node
118
+ end
119
+
120
+ def traslado_iva_node
121
+ return nil unless impuestos_node.is_a?(Impuestos)
122
+
123
+ impuestos_node.traslado_iva
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,4 @@
1
+ module Cfdi40
2
+ class Conceptos < Node
3
+ end
4
+ end
@@ -0,0 +1,7 @@
1
+ module Cfdi40
2
+ class Emisor < Node
3
+ define_attribute :rfc, xml_attribute: 'Rfc'
4
+ define_attribute :nombre, xml_attribute: 'Nombre'
5
+ define_attribute :regimen_fiscal, xml_attribute: 'RegimenFiscal'
6
+ end
7
+ end
@@ -0,0 +1,28 @@
1
+ module Cfdi40
2
+ class Impuestos < Node
3
+ define_attribute :total_impuestos_retenidos, xml_attribute: 'TotalImpuestosRetenidos', format: :t_Importe
4
+ define_attribute :total_impuestos_trasladados, xml_attribute: 'TotalImpuestosTrasladados', format: :t_Importe
5
+
6
+ def traslados
7
+ return @traslados if defined?(@traslados)
8
+
9
+ @traslados = Traslados.new
10
+ self.children_nodes << @traslados
11
+ @traslados
12
+ end
13
+
14
+ def traslados_node
15
+ children_nodes.select { |n| n.is_a?(Traslados)}.first
16
+ end
17
+
18
+ def traslado_nodes
19
+ return [] if traslados_node.nil?
20
+
21
+ traslados_node.traslado_nodes
22
+ end
23
+
24
+ def traslado_iva
25
+ traslados.traslado_iva
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,122 @@
1
+ module Cfdi40
2
+ class Node
3
+ # Nokigir XML Document for the xml_node
4
+ attr_accessor :xml_document, :xml_parent, :children_nodes
5
+
6
+ def initialize
7
+ self.class.verify_class_variables
8
+ @children_nodes = []
9
+ end
10
+
11
+ # Use class variables to define attributes used to create nodes
12
+ # Class variables are the same for children classes, so are organized by
13
+ # the name of the class.
14
+ def self.verify_class_variables
15
+ @@attributes ||= {}
16
+ @@attributes[name] ||= {}
17
+ @@namespaces ||= {}
18
+ @@namespaces[name] ||= {}
19
+ @@default_values ||= {}
20
+ @@default_values[name] ||= {}
21
+ @@formats ||= {}
22
+ @@formats[name] ||= {}
23
+ end
24
+
25
+ def self.define_attribute(accessor, xml_attribute:, default: nil, format: nil, readonly: false)
26
+ verify_class_variables
27
+ if readonly
28
+ attr_reader accessor.to_sym
29
+ else
30
+ attr_accessor accessor.to_sym
31
+ end
32
+ @@attributes[name][accessor.to_sym] = xml_attribute
33
+ if default
34
+ @@default_values[name][accessor.to_sym] = default
35
+ end
36
+ if format
37
+ @@formats[name][accessor.to_sym] = format
38
+ end
39
+ end
40
+
41
+ def self.define_namespace(namespace, value)
42
+ verify_class_variables
43
+ @@namespaces[name][namespace] = value
44
+ end
45
+
46
+ def self.namespaces
47
+ @@namespaces[name]
48
+ end
49
+
50
+ def self.attributes
51
+ @@attributes[name]
52
+ end
53
+
54
+ def self.default_values
55
+ @@default_values[name]
56
+ end
57
+
58
+ def self.formats
59
+ @@formats[name]
60
+ end
61
+
62
+ def set_defaults
63
+ return if self.class.default_values.nil?
64
+
65
+ self.class.default_values.each do |accessor, value|
66
+ next unless attibute_is_null?(accessor)
67
+
68
+ instance_variable_set "@#{accessor}".to_sym, value
69
+ end
70
+ end
71
+
72
+ def attibute_is_null?(accessor)
73
+ return true unless instance_variable_defined?("@#{accessor}".to_sym)
74
+
75
+ instance_variable_get("@#{accessor}".to_sym).nil?
76
+ end
77
+
78
+ def create_xml_node
79
+ set_defaults
80
+ if self.respond_to?(:before_add, true)
81
+ self.before_add
82
+ end
83
+ xml_node = xml_document.create_element("cfdi:#{self.class.name.split('::').last}")
84
+ add_namespaces_to(xml_node)
85
+ add_attributes_to(xml_node)
86
+ add_children_to(xml_node)
87
+ xml_parent.add_child xml_node
88
+ end
89
+
90
+ def add_namespaces_to(xml_node)
91
+ self.class.namespaces.each do |namespace, value|
92
+ xml_node.add_namespace namespace, value
93
+ end
94
+ end
95
+
96
+ def add_attributes_to(node)
97
+ self.class.attributes.each do |object_accessor, xml_attribute|
98
+ next unless respond_to?(object_accessor)
99
+ next if public_send(object_accessor).nil?
100
+
101
+ node[xml_attribute] = formated_value(object_accessor)
102
+ end
103
+ end
104
+
105
+ def add_children_to(xml_node)
106
+ children_nodes.each do |node|
107
+ node.xml_document = xml_document
108
+ node.xml_parent = xml_node
109
+ node.create_xml_node
110
+ end
111
+ end
112
+
113
+ def formated_value(accessor)
114
+ case self.class.formats[accessor]
115
+ when :t_Importe
116
+ sprintf("%0.6f", public_send(accessor).to_f)
117
+ else
118
+ public_send(accessor)
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,11 @@
1
+ module Cfdi40
2
+ class Receptor < Node
3
+ define_attribute :rfc, xml_attribute: 'Rfc'
4
+ define_attribute :nombre, xml_attribute: 'Nombre'
5
+ define_attribute :domicilio_fiscal, xml_attribute: 'DomicilioFiscalReceptor'
6
+ define_attribute :residencia_fiscal, xml_attribute: 'ResidenciaFiscal'
7
+ define_attribute :num_reg_id_trib, xml_attribute: 'NumRegIdTrib'
8
+ define_attribute :regimen_fiscal, xml_attribute: 'RegimenFiscalReceptor'
9
+ define_attribute :uso_cfdi, xml_attribute: 'UsoCFDI'
10
+ end
11
+ end