cfdi 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9f817ca5a0b899f9d7eb858a4093065851a97076
4
+ data.tar.gz: f3ec315d75256251637af8ba492042dbd00bd322
5
+ SHA512:
6
+ metadata.gz: 4818dd7e9c79070fc32fc3ba853898ec2c48430e4f82718673a9509a4a533141d3f6f2e19001ed0d7ffcf492ef27dedc38b29df9891c80674133ad0610633fdb
7
+ data.tar.gz: 875fdf5ec3adb21f7a49b16f5cf30abf654a5a9dccc1704f601844791b906e418dc07209dfaf1a4cb725323dcaef0bef393155cf2425e69e4acd8c771420000f
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ previous.rb
2
+ key.pem
3
+ cert.cer
4
+ pkg/
5
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ # para tener compatibilidad con Savon 2
6
+ gem 'nokogiri', '< 1.6', '>= 1.4.0'
data/LICENSE.txt ADDED
@@ -0,0 +1,31 @@
1
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
2
+ Version 2, December 2004
3
+
4
+ (C) 2013 Roberto Hidalgo <un@rob.mx>, Aquellos listados en CONTRIBUTORS
5
+
6
+ Everyone is permitted to copy and distribute verbatim or modified
7
+ copies of this license document, and changing it is allowed as long
8
+ as the name is changed.
9
+
10
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
11
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
12
+
13
+ 0. You just DO WHAT THE FUCK YOU WANT TO.
14
+
15
+
16
+ ----
17
+ Traducción:
18
+
19
+ LICENCIA PÚBLICA HAZ LO QUE TE DE TU CHINGADA GANA
20
+ Version 2, Diciembre 2004
21
+
22
+ (C) 2013 Roberto Hidalgo <un@rob.mx>, Aquellos listados en CONTRIBUTORS
23
+
24
+ Se permite la copia y distribución textual o con modificaciones de
25
+ este documento de licencia, y los cambios son permitidos siempre y
26
+ cuando el nombre sea cambiado.
27
+
28
+ LICENCIA PÚBLICA HAZ LO QUE TE DE TU CHINGADA GANA
29
+ TÉRMINOS Y CONDICIONES PARA COPIAR, DISTRIBUIR Y MODIFICAR ESTE SOFTWARE
30
+
31
+ 0. Solamente HAZ LO QUE TE DE TU CHINGADA GANA.
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ task :release do
4
+ system "gem build cfdi.gemspec"
5
+ end
data/cfdi.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require './cfdi.rb'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "cfdi"
8
+ gem.version = CFDI::VERSION
9
+ gem.authors = ["Roberto Hidalgo"]
10
+ gem.email = ["un@rob.mx"]
11
+ gem.description = "Comprobantes fiscales digitales por internet"
12
+ gem.summary = "Digitales!! por Internet!!"
13
+ gem.homepage = "https://github.com/unRob/cfdi"
14
+ gem.licenses = ['WTFPL', 'GPLv2']
15
+ gem.has_rdoc = true
16
+
17
+ gem.files = `git ls-files`.split($/)
18
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
19
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
20
+ gem.require_paths = ["lib"]
21
+
22
+
23
+ gem.add_runtime_dependency 'nokogiri'
24
+
25
+ end
data/cfdi.rb ADDED
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+ gem 'nokogiri', '< 1.6', '>= 1.4.0'
3
+
4
+ require_relative 'lib/comun.rb'
5
+ require_relative 'lib/comprobante.rb'
6
+ require_relative 'lib/entidad.rb'
7
+ require_relative 'lib/concepto.rb'
8
+ require_relative 'lib/certificado.rb'
9
+ require_relative 'lib/key.rb'
10
+
11
+ module CFDI
12
+
13
+ require 'nokogiri'
14
+ require 'time'
15
+ require 'openssl'
16
+ require 'base64'
17
+
18
+ VERSION = '0.0.2'
19
+
20
+ end
@@ -0,0 +1,78 @@
1
+ require_relative '../cfdi.rb'
2
+ require 'json'
3
+ require 'time'
4
+
5
+ # el archivo .cer, tal cual
6
+ certificado = CFDI::Certificado.new './cfdi/examples/data/cert.cer'
7
+ # la llave en formato pem, porque no encontré como usar OpenSSL con llaves tipo PKCS8
8
+ # Esta se convierte de un archivo .key con:
9
+ # openssl pkcs8 -inform DER -in someKey.key -passin pass:somePassword -out key.pem
10
+ llave = CFDI::Key.new './cfdi/examples/data/key.pem', 'somePassword'
11
+
12
+ # Así instanciamos el comprobante nuevo
13
+ factura = CFDI::Comprobante.new ({
14
+ folio: 1,
15
+ serie: 'A',
16
+ fecha: Time.now,
17
+ formaDePago: 'PAGO EN UNA SOLA EXHIBICION',
18
+ condicionesDePago: 'Sera marcada como pagada en cuanto el receptor haya cubierto el pago.',
19
+ metodoDePago: 'Transferencia Bancaria',
20
+ lugarExpedicion: 'Nutopia, Nutopia'
21
+ })
22
+
23
+ # Esto es un domicilio casi completo
24
+ domicilioEmisor = CFDI::Domicilio.new({
25
+ calle: 'Calle Feliz',
26
+ noExterior: '42',
27
+ noInterior: '314',
28
+ colonia: 'Centro',
29
+ localidad: 'No se que sea esto, pero va',
30
+ referencia: 'Sin Referencia',
31
+ municipio: 'Nutopía',
32
+ estado: 'Nutopía',
33
+ pais: 'Nutopía',
34
+ codigoPostal: '31415'
35
+ })
36
+
37
+ # y esto es una persona fiscal
38
+ factura.emisor = CFDI::Entidad.new({
39
+ rfc: 'XAXX010101000',
40
+ nombre: 'Me cago en sus estándares S.A. de C.V.',
41
+ domicilioFiscal: domicilioEmisor,
42
+ expedidoEn: domicilioEmisor,
43
+ regimenFiscal: 'Pruebas Fiscales'
44
+ })
45
+
46
+ # misma mierda
47
+ domicilioReceptor = CFDI::Domicilio.new({
48
+ referencia: 'Sin Referencia',
49
+ estado: 'Nutopía',
50
+ pais: 'Nutopía'
51
+ })
52
+ factura.receptor = CFDI::Entidad.new({rfc: 'XAXX010101000', nombre: 'El SAT apesta S. de R.L.', domicilioFiscal: domicilioReceptor})
53
+
54
+
55
+ # Así agregamos conceptos, en este caso, 2 Kg de Verga
56
+ factura.conceptos << CFDI::Concepto.new({
57
+ cantidad: 2,
58
+ unidad: 'Kilos',
59
+ noIdentificacion: 'KDV',
60
+ descripcion: 'Verga',
61
+ valorUnitario:5500.00 #el importe se calcula solo
62
+ })
63
+
64
+ # Todavía no agarro bien el pedo sobre como salen los impuestos, pull request?
65
+ factura.impuestos << {impuesto: 'IVA'}
66
+
67
+ # Esto hace que se le agregue al comprobante el certificado y su número de serie (noCertificado)
68
+ certificado.certifica factura
69
+
70
+ # Para mandarla a un PAC, necesitamos sellarla, y esto lo hace agregando el sello
71
+ # La cadena original que generamos sólo tiene los datos que u
72
+ llave.sella factura
73
+
74
+ # Esto genera la factura como xml
75
+ puts factura.to_xml
76
+
77
+ # Esto nos da un hash con todo lo que pusimos
78
+ puts JSON.pretty_generate factura.to_h
File without changes
@@ -0,0 +1,36 @@
1
+ module CFDI
2
+
3
+ require 'openssl'
4
+
5
+ class Certificado < OpenSSL::X509::Certificate
6
+
7
+ attr_reader :noCertificado, :data
8
+
9
+ def initialize (file)
10
+
11
+ if file.is_a? String
12
+ file = File.read(file)
13
+ end
14
+
15
+ super file
16
+
17
+ @noCertificado = '';
18
+ # Normalmente son strings de tipo:
19
+ # 3230303031303030303030323030303030323933
20
+ # por eso sólo tomamos cada segundo dígito
21
+ self.serial.to_s(16).scan(/.{2}/).each {|v| @noCertificado += v[1]; }
22
+ @data = self.to_s.gsub(/^-.+/, '').gsub(/\n/, '')
23
+
24
+ end
25
+
26
+ def certifica factura
27
+
28
+ factura.noCertificado = @noCertificado
29
+ factura.certificado = @data
30
+
31
+ end
32
+
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,175 @@
1
+ module CFDI
2
+ class Comprobante
3
+
4
+ @@datosCadena = [:version, :fecha, :tipoDeComprobante, :formaDePago, :condicionesDePago, :subTotal, :moneda, :total, :metodoDePago, :lugarExpedicion]
5
+ @@data = @@datosCadena+[:emisor, :receptor, :conceptos, :serie, :folio, :sello, :noCertificado, :certificado, :impuestos]
6
+ attr_accessor *@@data
7
+
8
+ @@options = {
9
+ tasa: 0.16,
10
+ defaults: {
11
+ moneda: 'pesos',
12
+ version: '3.2',
13
+ subTotal: 0.0,
14
+ conceptos: [],
15
+ impuestos: [],
16
+ tipoDeComprobante: 'ingreso'
17
+ }
18
+ }
19
+
20
+ def configure (options)
21
+ @@options.merge! options
22
+ end
23
+
24
+ def initialize (data={}, options={})
25
+ data = @@options[:defaults].merge data
26
+ @opciones = @@options.merge options
27
+ data.each do |k,v|
28
+ self.send "#{k}=", v
29
+ end
30
+ end
31
+
32
+ def subTotal
33
+ ret = 0
34
+ @conceptos.each do |c|
35
+ ret += c.importe
36
+ end
37
+ ret
38
+ end
39
+
40
+ def total
41
+ self.subTotal+(self.subTotal*@opciones[:tasa])
42
+ end
43
+
44
+ def fecha= fecha
45
+ fecha = fecha.strftime('%FT%R:%S') unless fecha.is_a? String
46
+ @fecha = fecha
47
+ end
48
+
49
+ def to_xml
50
+ ns = {
51
+ 'xmlns:cfdi' => "http://www.sat.gob.mx/cfd/3",
52
+ 'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance",
53
+ 'xsi:schemaLocation' => "http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd",
54
+ version: @version,
55
+ folio: @folio,
56
+ fecha: @fecha,
57
+ formaDePago: @formaDePago,
58
+ condicionesDePago: @condicionesDePago,
59
+ subTotal: self.subTotal,
60
+ Moneda: @moneda,
61
+ total: self.total,
62
+ metodoDePago: @metodoDePago,
63
+ tipoDeComprobante: @tipoDeComprobante,
64
+ LugarExpedicion: @lugarExpedicion,
65
+ }
66
+
67
+ if @noCertificado
68
+ ns[:noCertificado] = @noCertificado
69
+ ns[:certificado] = @certificado
70
+ end
71
+
72
+ if @sello
73
+ ns[:sello] = @sello
74
+ end
75
+
76
+ @builder = Nokogiri::XML::Builder.new do |xml|
77
+ xml.Comprobante(ns) do
78
+ ins = xml.doc.root.add_namespace_definition('cfdi', 'http://www.sat.gob.mx/cfd/3')
79
+ xml.doc.root.namespace = ins
80
+
81
+ xml.Emisor(@emisor.ns) {
82
+ xml.DomicilioFiscal(@emisor.domicilioFiscal.to_h.reject {|k,v| v == nil})
83
+ xml.ExpedidoEn(@emisor.expedidoEn.to_h.reject {|k,v| v == nil})
84
+ xml.RegimenFiscal({Regimen: @emisor.regimenFiscal})
85
+ }
86
+ xml.Receptor(@receptor.ns) {
87
+ xml.Domicilio(@receptor.domicilioFiscal.to_h.reject {|k,v| v == nil})
88
+ }
89
+ xml.Conceptos {
90
+ @conceptos.each do |concepto|
91
+ xml.Concepto(concepto.to_h) {
92
+ xml.ComplementoConcepto
93
+ }
94
+ end
95
+ }
96
+ xml.Impuestos({totalImpuestosTrasladados: self.subTotal*@opciones[:tasa]}) {
97
+ xml.Traslados {
98
+ @impuestos.each do |impuesto|
99
+ xml.Traslado({impuesto: impuesto[:impuesto], tasa:@opciones[:tasa]*100.to_i, importe: self.subTotal*@opciones[:tasa]})
100
+ end
101
+ }
102
+ }
103
+ xml.Complemento
104
+ end
105
+ end
106
+ @builder.to_xml
107
+ end
108
+
109
+ def to_h
110
+ hash = {}
111
+ @@data.each do |key|
112
+ data = deep_to_h send(key)
113
+ hash[key] = data
114
+ end
115
+
116
+ return hash
117
+ end
118
+
119
+ def cadena_original
120
+ params = []
121
+
122
+ @@datosCadena.each {|key| params.push send(key) }
123
+ params += @emisor.cadena_original
124
+ params << @regimen
125
+ params += @receptor.cadena_original
126
+
127
+ @conceptos.each do |concepto|
128
+ params += concepto.cadena_original
129
+ end
130
+
131
+ @impuestos.each do |traslado|
132
+ params += [traslado[:impuesto], @opciones[:tasa]*100, self.subTotal*@opciones[:tasa], self.subTotal*@opciones[:tasa]]
133
+ end
134
+
135
+ params.select! { |i| i != nil }
136
+ params.map! do |elem|
137
+ if elem.is_a? Float
138
+ elem = sprintf('%.2f', elem)
139
+ else
140
+ elem = elem.to_s
141
+ end
142
+ elem
143
+ end
144
+
145
+ return "||#{params.join '|'}||"
146
+ end
147
+
148
+ private
149
+ def deep_to_h value
150
+
151
+ if value.is_a? ElementoComprobante
152
+ original = value.to_h
153
+ value = {}
154
+ original.each do |k,v|
155
+ value[k] = deep_to_h v
156
+ end
157
+
158
+ elsif value.is_a?(Array)
159
+ value = value.map do |v|
160
+ deep_to_h v
161
+ end
162
+ end
163
+ value
164
+
165
+ #value = value.to_h if value.respond_to? :to_h
166
+ #if value.each do |vi|
167
+ # value.map do |k,v|
168
+ # v = deep_to_h v
169
+ # end
170
+ #end
171
+ value
172
+ end
173
+
174
+ end
175
+ end
data/lib/comun.rb ADDED
@@ -0,0 +1,42 @@
1
+ class Float
2
+
3
+ def to_s
4
+ sprintf('%.2f', self)
5
+ end
6
+
7
+ end
8
+
9
+ module CFDI
10
+
11
+ class ElementoComprobante
12
+
13
+ def initialize data={}
14
+ #puts self.class
15
+ data.each do |k,v|
16
+ self.instance_variable_set "@#{k}", v
17
+ end
18
+ end
19
+
20
+ def self.data
21
+ @cadenaOriginal
22
+ end
23
+
24
+ def cadena_original
25
+ params = []
26
+ data = {}
27
+ data = self.class.data
28
+ # puts self.class.cadenaOriginal
29
+
30
+ data.each {|key| params.push instance_variable_get('@'+key.to_s) }
31
+ return params
32
+ end
33
+
34
+ def to_h
35
+ Hash[*self.class.data.map { |v|
36
+ [v, self.send(v)]
37
+ }.flatten]
38
+ end
39
+
40
+ end
41
+
42
+ end
data/lib/concepto.rb ADDED
@@ -0,0 +1,26 @@
1
+ module CFDI
2
+
3
+ class Concepto < ElementoComprobante
4
+
5
+ @cadenaOriginal = [:cantidad, :unidad, :noIdentificacion, :descripcion, :valorUnitario, :importe]
6
+ @data = @cadenaOriginal
7
+ attr_accessor *@cadenaOriginal
8
+
9
+ def cadena_original
10
+ return [
11
+ @cantidad.to_i,
12
+ @unidad,
13
+ @noIdentificacion,
14
+ @descripcion,
15
+ @valorUnitario,
16
+ @importe
17
+ ]
18
+ end
19
+
20
+ def importe
21
+ return @valorUnitario*@cantidad
22
+ end
23
+
24
+ end
25
+
26
+ end
data/lib/entidad.rb ADDED
@@ -0,0 +1,34 @@
1
+ module CFDI
2
+
3
+ class Entidad < ElementoComprobante
4
+ @cadenaOriginal = [:rfc, :nombre, :domicilioFiscal, :expedidoEn, :regimenFiscal]
5
+ @data = @cadenaOriginal
6
+ attr_accessor *@cadenaOriginal
7
+
8
+ def cadena_original
9
+ expedido = @expedidoEn ? @expedidoEn.cadena_original : nil
10
+ return [
11
+ @rfc,
12
+ @nombre,
13
+ *@domicilioFiscal.cadena_original,
14
+ expedido,
15
+ @regimenFiscal
16
+ ].flatten
17
+ end
18
+
19
+ def ns
20
+ return ({
21
+ nombre: @nombre,
22
+ rfc: @rfc
23
+ })
24
+ end
25
+
26
+ end
27
+
28
+ class Domicilio < ElementoComprobante
29
+ @cadenaOriginal = [:calle, :noExterior, :noInterior, :colonia, :localidad, :referencia, :municipio, :estado, :pais, :codigoPostal]
30
+ attr_accessor *@cadenaOriginal
31
+
32
+ end
33
+
34
+ end
data/lib/key.rb ADDED
@@ -0,0 +1,22 @@
1
+ module CFDI
2
+
3
+ require 'openssl'
4
+
5
+ class Key < OpenSSL::PKey::RSA
6
+
7
+ def initialize file, password
8
+ if file.is_a? String
9
+ file = File.read(file)
10
+ end
11
+ super file, password
12
+ end
13
+
14
+ # sella una factura
15
+ def sella factura
16
+ cadena_original = factura.cadena_original
17
+ factura.sello = Base64::encode64(self.sign(OpenSSL::Digest::SHA1.new, cadena_original)).gsub(/\n/, '')
18
+ end
19
+
20
+ end
21
+
22
+ end
data/readme.md ADDED
@@ -0,0 +1,37 @@
1
+ # CFDI para principiantes en CFDI
2
+
3
+ El sistema de generación y sellado de facturas es una patada en los genitales. Este gem pretende ser una bolsa de hielos. Igual va a doler, pero espero que al menos no quede moretón.
4
+
5
+ ## Instalación
6
+
7
+ gem install cfdi
8
+
9
+ ## Uso
10
+
11
+ Puedes ver [crear_factura.rb](examples/crear_factura.rb) para darse una mejor idea, pero acá va un resumen:
12
+
13
+ require 'cfdi'
14
+ factura = CFDI::Comprobante.new
15
+
16
+ emisor = {
17
+ rfc: 'un RFC',
18
+ nombre: 'una razón social o nombre',
19
+ domicilioFiscal: CFDI::Domicilio.new
20
+ expedidoEn: CFDI::Domicilio.new
21
+ regimenFiscal: 'general'
22
+ }
23
+
24
+ # lo mismo para el receptor
25
+
26
+ # porque XML! ES LO DE HOY! BIENVENIDOS A 2001!
27
+ puts factura.to_xml
28
+
29
+ # O talvez evolucionamos a un formato de intercambio de datos menos castroso
30
+ require 'json'
31
+ puts JSON.pretty_generate(factura.to_h)
32
+
33
+ ## Licencia
34
+
35
+ ![What the fuck Public License](http://www.wtfpl.net/wp-content/uploads/2012/12/wtfpl-badge-1.png)
36
+
37
+ Como es costumbre, todo bajo WTFPL. La licencia completa la puedes leer acá: [Licencia](LICENSE.txt)
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cfdi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Roberto Hidalgo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-10-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nokogiri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Comprobantes fiscales digitales por internet
28
+ email:
29
+ - un@rob.mx
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - .gitignore
35
+ - Gemfile
36
+ - LICENSE.txt
37
+ - Rakefile
38
+ - cfdi.gemspec
39
+ - cfdi.rb
40
+ - examples/crear_factura.rb
41
+ - examples/data/_empty
42
+ - lib/certificado.rb
43
+ - lib/comprobante.rb
44
+ - lib/comun.rb
45
+ - lib/concepto.rb
46
+ - lib/entidad.rb
47
+ - lib/key.rb
48
+ - readme.md
49
+ homepage: https://github.com/unRob/cfdi
50
+ licenses:
51
+ - WTFPL
52
+ - GPLv2
53
+ metadata: {}
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubyforge_project:
70
+ rubygems_version: 2.0.3
71
+ signing_key:
72
+ specification_version: 4
73
+ summary: Digitales!! por Internet!!
74
+ test_files: []