cfdi40 0.0.1.alfa → 0.0.2

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.
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