afipws 0.0.1

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