afipws 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/.gitignore +5 -0
  2. data/.rspec +1 -0
  3. data/.rvmrc +1 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +69 -0
  6. data/README.rdoc +32 -0
  7. data/Rakefile +7 -0
  8. data/afipws.gemspec +29 -0
  9. data/autotest/discover.rb +1 -0
  10. data/lib/afipws.rb +23 -0
  11. data/lib/afipws/client.rb +48 -0
  12. data/lib/afipws/excepciones.rb +13 -0
  13. data/lib/afipws/type_conversions.rb +38 -0
  14. data/lib/afipws/version.rb +3 -0
  15. data/lib/afipws/wsaa.rb +77 -0
  16. data/lib/afipws/wsfe.rb +88 -0
  17. data/lib/core_ext/hash.rb +21 -0
  18. data/lib/core_ext/string.rb +9 -0
  19. data/spec/afipws/test.crt +17 -0
  20. data/spec/afipws/test.key +15 -0
  21. data/spec/afipws/type_conversions_spec.rb +30 -0
  22. data/spec/afipws/wsaa_spec.rb +68 -0
  23. data/spec/afipws/wsfe_spec.rb +163 -0
  24. data/spec/core_ext/hash_spec.rb +44 -0
  25. data/spec/fixtures/fe_comp_consultar/success.xml +41 -0
  26. data/spec/fixtures/fe_comp_tot_x_request/success.xml +9 -0
  27. data/spec/fixtures/fe_comp_ultimo_autorizado/success.xml +11 -0
  28. data/spec/fixtures/fe_dummy/success.xml +11 -0
  29. data/spec/fixtures/fe_param_get_cotizacion/dolar.xml +13 -0
  30. data/spec/fixtures/fe_param_get_cotizacion/inexistente.xml +14 -0
  31. data/spec/fixtures/fe_param_get_tipos_cbte/failure_1_error.xml +14 -0
  32. data/spec/fixtures/fe_param_get_tipos_cbte/failure_2_errors.xml +18 -0
  33. data/spec/fixtures/fe_param_get_tipos_cbte/success.xml +22 -0
  34. data/spec/fixtures/fe_param_get_tipos_doc/success.xml +16 -0
  35. data/spec/fixtures/fe_param_get_tipos_iva/success.xml +16 -0
  36. data/spec/fixtures/fe_param_get_tipos_monedas/success.xml +22 -0
  37. data/spec/fixtures/fe_param_get_tipos_tributos/success.xml +16 -0
  38. data/spec/fixtures/fecae_solicitar/autorizacion_1_cbte.xml +30 -0
  39. data/spec/fixtures/fecae_solicitar/autorizacion_2_cbtes.xml +41 -0
  40. data/spec/fixtures/fecae_solicitar/observaciones.xml +40 -0
  41. data/spec/fixtures/login_cms/fault.xml +12 -0
  42. data/spec/fixtures/login_cms/success.xml +16 -0
  43. data/spec/fixtures/login_cms/token_expirado.xml +14 -0
  44. data/spec/fixtures/wsaa.wsdl +103 -0
  45. data/spec/fixtures/wsfe.wsdl +1372 -0
  46. data/spec/manual/autorizar_comprobante.rb +22 -0
  47. data/spec/manual/generar_keys.txt +8 -0
  48. data/spec/manual/obtener_ta.rb +18 -0
  49. data/spec/spec_helper.rb +12 -0
  50. data/spec/support/matchers.rb +37 -0
  51. metadata +237 -0
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ spec/manual/*.key
5
+ spec/manual/*.crt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm 1.9.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in afipws.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,69 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ afipws (0.0.1)
5
+ activesupport
6
+ builder
7
+ nokogiri
8
+ savon
9
+
10
+ GEM
11
+ remote: http://rubygems.org/
12
+ specs:
13
+ activesupport (3.0.3)
14
+ archive-tar-minitar (0.5.2)
15
+ builder (2.1.2)
16
+ columnize (0.3.2)
17
+ crack (0.1.8)
18
+ diff-lcs (1.1.2)
19
+ gyoku (0.2.0)
20
+ builder (>= 2.1.2)
21
+ httpi (0.7.8)
22
+ rack
23
+ linecache19 (0.5.11)
24
+ ruby_core_source (>= 0.1.4)
25
+ mocha (0.9.10)
26
+ rake
27
+ nokogiri (1.4.4)
28
+ rack (1.2.1)
29
+ rake (0.8.7)
30
+ rspec (2.4.0)
31
+ rspec-core (~> 2.4.0)
32
+ rspec-expectations (~> 2.4.0)
33
+ rspec-mocks (~> 2.4.0)
34
+ rspec-core (2.4.0)
35
+ rspec-expectations (2.4.0)
36
+ diff-lcs (~> 1.1.2)
37
+ rspec-mocks (2.4.0)
38
+ ruby-debug-base19 (0.11.24)
39
+ columnize (>= 0.3.1)
40
+ linecache19 (>= 0.5.11)
41
+ ruby_core_source (>= 0.1.4)
42
+ ruby-debug19 (0.11.6)
43
+ columnize (>= 0.3.1)
44
+ linecache19 (>= 0.5.11)
45
+ ruby-debug-base19 (>= 0.11.19)
46
+ ruby_core_source (0.1.4)
47
+ archive-tar-minitar (>= 0.5.2)
48
+ savon (0.8.2)
49
+ builder (>= 2.1.2)
50
+ crack (~> 0.1.8)
51
+ gyoku (>= 0.1.1)
52
+ httpi (>= 0.7.5)
53
+ savon_spec (0.1.2)
54
+ mocha (>= 0.9.8)
55
+ rspec (>= 2.0.0)
56
+ savon (~> 0.8.0)
57
+
58
+ PLATFORMS
59
+ ruby
60
+
61
+ DEPENDENCIES
62
+ activesupport
63
+ afipws!
64
+ builder
65
+ nokogiri
66
+ rspec
67
+ ruby-debug19
68
+ savon
69
+ savon_spec
data/README.rdoc ADDED
@@ -0,0 +1,32 @@
1
+ = Afipws
2
+
3
+ == Uso
4
+
5
+ Primero hay que crear la clave privada y obtener el certificado correspondiente según los pasos indicados {aquí}[http://www.sistemasagiles.com.ar/trac/wiki/ManualPyAfipWs#Certificados].
6
+ Luego usamos el Web Service de la siguiente forma:
7
+
8
+ gem install 'afipws'
9
+
10
+ require 'afipws'
11
+ ws = Afipws::WSFE.new :env => :dev, :cuit => '...', :key => File.read('test.key') , :cert => File.read('test.crt')
12
+ puts ws.cotizacion 'DOL'
13
+
14
+ De momento sólo están soportados los servicios WSAA y WSFEv1.
15
+ Ver specs para más ejemplos.
16
+
17
+ == Testing
18
+
19
+ git clone git://github.com/eeng/afipws.git
20
+ cd afipws
21
+ bundle
22
+ rake
23
+
24
+ == Note on Patches/Pull Requests
25
+
26
+ * Fork the project.
27
+ * Make your feature addition or bug fix.
28
+ * Add tests for it. This is important so I don't break it in a
29
+ future version unintentionally.
30
+ * Commit, do not mess with rakefile, version, or history.
31
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
32
+ * Send me a pull request. Bonus points for topic branches.
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'bundler'
2
+ Bundler.setup
3
+ Bundler::GemHelper.install_tasks
4
+
5
+ require "rspec/core/rake_task"
6
+ RSpec::Core::RakeTask.new :spec
7
+ task :default => :spec
data/afipws.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "afipws/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "afipws"
7
+ s.version = Afipws::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Emmanuel Nicolau"]
10
+ s.email = ["emmanicolau@gmail.com"]
11
+ s.homepage = ""
12
+ s.summary = %q{Ruby wrapper para los servicios web de la AFIP}
13
+ s.description = ""
14
+
15
+ s.rubyforge_project = "afipws"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_development_dependency "rspec"
23
+ s.add_development_dependency "ruby-debug19"
24
+ s.add_development_dependency "savon_spec"
25
+ s.add_dependency "builder"
26
+ s.add_dependency "savon"
27
+ s.add_dependency "nokogiri"
28
+ s.add_dependency "activesupport"
29
+ end
@@ -0,0 +1 @@
1
+ Autotest.add_discovery { "rspec2" }
data/lib/afipws.rb ADDED
@@ -0,0 +1,23 @@
1
+ module Afipws
2
+ Root = File.expand_path File.dirname(__FILE__) + '/..'
3
+ end
4
+
5
+ require 'forwardable'
6
+ require 'builder'
7
+ require 'base64'
8
+ require 'savon'
9
+ require 'nokogiri'
10
+ require 'active_support/core_ext/array/wrap'
11
+ # TODO reemplazar wrap x un local
12
+ require 'core_ext/string'
13
+ require 'core_ext/hash'
14
+ require 'afipws/excepciones'
15
+ require 'afipws/type_conversions'
16
+ require 'afipws/client'
17
+ require 'afipws/wsaa'
18
+ require 'afipws/wsfe'
19
+
20
+ Savon.configure do |config|
21
+ config.soap_version = 2
22
+ config.log = false
23
+ end
@@ -0,0 +1,48 @@
1
+ module Afipws
2
+ class Client
3
+ def initialize wsdl_url
4
+ @client = Savon::Client.new { wsdl.document = wsdl_url }
5
+ end
6
+
7
+ def request action, body = nil
8
+ response = raw_request(action, body).to_hash[:"#{action}_response"][:"#{action}_result"]
9
+ if response[:errors]
10
+ raise WSError, Array.wrap(response[:errors][:err])
11
+ else
12
+ response
13
+ end
14
+ end
15
+
16
+ def raw_request action, body = nil
17
+ @client.request(namespace, action) { soap.body = add_ns_to_keys(body) }
18
+ end
19
+
20
+ def soap_actions
21
+ @client.wsdl.soap_actions
22
+ end
23
+
24
+ def method_missing method_sym, *args
25
+ request method_sym, *args
26
+ end
27
+
28
+ private
29
+ def add_ns_to_keys body
30
+ case body
31
+ when Hash
32
+ Hash[body.map { |k, v| ["#{namespace}:#{camelize(k)}", add_ns_to_keys(v)] }]
33
+ when Array
34
+ body.map { |x| add_ns_to_keys x }
35
+ else
36
+ body
37
+ end
38
+ end
39
+
40
+ def namespace
41
+ :wsdl
42
+ end
43
+
44
+ def camelize k
45
+ k.is_a?(String) ? k : k.to_s.camelize
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,13 @@
1
+ module Afipws
2
+ class WSError < StandardError
3
+ attr_reader :errors
4
+ def initialize errors
5
+ if errors.is_a? Array
6
+ super errors.map { |e| "#{e[:code]}: #{e[:msg]}" }.join '; '
7
+ @errors = errors
8
+ else
9
+ super
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,38 @@
1
+ module Afipws
2
+ module TypeConversions
3
+ def r2x x, types
4
+ convert x, types, marshall_fn
5
+ end
6
+
7
+ def x2r x, types
8
+ convert x, types, parsing_fn
9
+ end
10
+
11
+ private
12
+ # Hace una conversión recursiva de tipo de todos los values según los tipos de las keys indicados en types
13
+ def convert object, types, convert_fn
14
+ case object
15
+ when Array then
16
+ object.map { |e| convert e, types, convert_fn }
17
+ when Hash then
18
+ Hash[object.map { |k, v| [k, v.is_a?(Hash) || v.is_a?(Array) ? convert(v, types, convert_fn) : convert_fn[types[k]].call(v)] }]
19
+ else
20
+ object
21
+ end
22
+ end
23
+
24
+ def parsing_fn
25
+ @parsing ||= Hash.new(Proc.new { |other| other }).tap { |p|
26
+ p[:date] = Proc.new { |date| ::Date.parse(date) rescue nil }
27
+ p[:integer] = Proc.new { |integer| integer.to_i }
28
+ p[:float] = Proc.new { |float| float.to_f }
29
+ }
30
+ end
31
+
32
+ def marshall_fn
33
+ @marshall ||= Hash.new(Proc.new { |other| other }).tap { |p|
34
+ p[:date] = Proc.new { |date| date.strftime('%Y%m%d') }
35
+ }
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,3 @@
1
+ module Afipws
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,77 @@
1
+ module Afipws
2
+ class WSAA
3
+ attr_reader :key, :cert, :service, :ta, :cuit
4
+
5
+ WSDL = {
6
+ :dev => "https://wsaahomo.afip.gov.ar/ws/services/LoginCms?wsdl",
7
+ :test => Root + '/spec/fixtures/wsaa.wsdl'
8
+ }
9
+
10
+ def initialize options = {}
11
+ @key = options[:key]
12
+ @cert = options[:cert]
13
+ @service = options[:service] || 'wsfe'
14
+ @ttl = options[:ttl] || 2400
15
+ @cuit = options[:cuit]
16
+ @client = Client.new WSDL[options[:env] || :test]
17
+ end
18
+
19
+ def generar_tra service, ttl
20
+ xml = Builder::XmlMarkup.new indent: 2
21
+ xml.instruct!
22
+ xml.loginTicketRequest version: 1 do
23
+ xml.header do
24
+ xml.uniqueId Time.now.to_i
25
+ xml.generationTime xsd_datetime Time.now - ttl
26
+ # TODO me parece que no le da mucha bola el WS al expirationTime
27
+ xml.expirationTime xsd_datetime Time.now + ttl
28
+ end
29
+ xml.service service
30
+ end
31
+ end
32
+
33
+ def firmar_tra tra, key, crt
34
+ key = OpenSSL::PKey::RSA.new key
35
+ crt = OpenSSL::X509::Certificate.new crt
36
+ OpenSSL::PKCS7::sign crt, key, tra
37
+ end
38
+
39
+ def codificar_tra pkcs7
40
+ pkcs7.to_pem.lines.to_a[1..-2].join
41
+ end
42
+
43
+ def tra key, cert, service, ttl
44
+ codificar_tra firmar_tra(generar_tra(service, ttl), key, cert)
45
+ end
46
+
47
+ def login
48
+ response = @client.raw_request :login_cms, 'in0' => tra(@key, @cert, @service, @ttl)
49
+ ta = Nokogiri::XML(Nokogiri::XML(response.to_xml).text)
50
+ { :token => ta.css('token').text, :sign => ta.css('sign').text,
51
+ :generation_time => from_xsd_datetime(ta.css('generationTime').text),
52
+ :expiration_time => from_xsd_datetime(ta.css('expirationTime').text) }
53
+ rescue Savon::SOAP::Fault => f
54
+ raise WSError, f.message
55
+ end
56
+
57
+ # Obtiene un TA, lo cachea hasta que expire, y devuelve el hash Auth listo para pasarle al Client
58
+ # en los otros WS.
59
+ def auth
60
+ @ta = login if ta_expirado?
61
+ { :auth => { :token => @ta[:token], :sign => ta[:sign], :cuit => @cuit } }
62
+ end
63
+
64
+ private
65
+ def ta_expirado?
66
+ @ta.nil? or @ta[:expiration_time] <= Time.now
67
+ end
68
+
69
+ def xsd_datetime time
70
+ time.strftime('%Y-%m-%dT%H:%M:%S%z').sub /(\d{2})(\d{2})$/, '\1:\2'
71
+ end
72
+
73
+ def from_xsd_datetime str
74
+ Time.parse(str) rescue nil
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,88 @@
1
+ module Afipws
2
+ class WSFE
3
+ extend Forwardable
4
+ include TypeConversions
5
+ attr_reader :wsaa, :client
6
+ def_delegators :wsaa, :ta, :auth, :cuit
7
+
8
+ WSDL = {
9
+ :dev => "http://wswhomo.afip.gov.ar/wsfev1/service.asmx?WSDL",
10
+ :test => Root + '/spec/fixtures/wsfe.wsdl'
11
+ }
12
+
13
+ def initialize options = {}
14
+ @wsaa = options[:wsaa] || WSAA.new(options.merge(:service => 'wsfe'))
15
+ @client = Client.new WSDL[options[:env] || :test]
16
+ end
17
+
18
+ def dummy
19
+ @client.fe_dummy
20
+ end
21
+
22
+ def tipos_comprobantes
23
+ r = @client.fe_param_get_tipos_cbte auth
24
+ x2r get_array(r, :cbte_tipo), :id => :integer, :fch_desde => :date, :fch_hasta => :date
25
+ end
26
+
27
+ def tipos_documentos
28
+ r = @client.fe_param_get_tipos_doc auth
29
+ x2r get_array(r, :doc_tipo), :id => :integer, :fch_desde => :date, :fch_hasta => :date
30
+ end
31
+
32
+ def tipos_monedas
33
+ r = @client.fe_param_get_tipos_monedas auth
34
+ x2r get_array(r, :moneda), :fch_desde => :date, :fch_hasta => :date
35
+ end
36
+
37
+ def tipos_iva
38
+ r = @client.fe_param_get_tipos_iva auth
39
+ x2r get_array(r, :iva_tipo), :id => :integer, :fch_desde => :date, :fch_hasta => :date
40
+ end
41
+
42
+ def tipos_tributos
43
+ r = @client.fe_param_get_tipos_tributos auth
44
+ x2r get_array(r, :tributo_tipo), :id => :integer, :fch_desde => :date, :fch_hasta => :date
45
+ end
46
+
47
+ def cotizacion moneda_id
48
+ @client.fe_param_get_cotizacion(auth.merge(:mon_id => moneda_id))[:result_get][:mon_cotiz].to_f
49
+ end
50
+
51
+ def autorizar_comprobantes opciones
52
+ comprobantes = opciones[:comprobantes]
53
+ request = { 'FeCAEReq' => {
54
+ 'FeCabReq' => opciones.select_keys(:cbte_tipo, :pto_vta).merge(:cant_reg => comprobantes.size),
55
+ 'FeDetReq' => {
56
+ 'FECAEDetRequest' => comprobantes.map do |comprobante|
57
+ comprobante.merge(:cbte_desde => comprobante[:cbte_nro], :cbte_hasta => comprobante[:cbte_nro]).
58
+ select_keys(:concepto, :doc_tipo, :doc_nro, :cbte_desde,
59
+ :cbte_hasta, :cbte_fch, :imp_total, :imp_tot_conc, :imp_neto, :imp_op_ex, :imp_trib,
60
+ :mon_id, :mon_cotiz, :iva).merge({ 'ImpIVA' => comprobante[:imp_iva] })
61
+ end
62
+ }}}
63
+ r = @client.fecae_solicitar auth.merge r2x(request, :cbte_fch => :date)
64
+ r = Array.wrap(r[:fe_det_resp][:fecae_det_response]).map do |h|
65
+ obs = h[:observaciones] ? h[:observaciones][:obs] : nil
66
+ h.select_keys(:cae, :cae_fch_vto).merge(:cbte_nro => h[:cbte_desde]).tap { |h| h.merge!(:observaciones => obs) if obs }
67
+ end
68
+ x2r r, :cae_fch_vto => :date, :cbte_nro => :integer, :code => :integer
69
+ end
70
+
71
+ def ultimo_comprobante_autorizado opciones
72
+ @client.fe_comp_ultimo_autorizado(auth.merge(opciones))[:cbte_nro].to_i
73
+ end
74
+
75
+ def consultar_comprobante opciones
76
+ @client.fe_comp_consultar(auth.merge(opciones))[:result_get]
77
+ end
78
+
79
+ def cant_max_registros_x_request
80
+ @client.fe_comp_tot_x_request[:reg_x_req].to_i
81
+ end
82
+
83
+ private
84
+ def get_array response, array_element
85
+ Array.wrap response[:result_get][array_element]
86
+ end
87
+ end
88
+ end