afipws 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +69 -0
- data/README.rdoc +32 -0
- data/Rakefile +7 -0
- data/afipws.gemspec +29 -0
- data/autotest/discover.rb +1 -0
- data/lib/afipws.rb +23 -0
- data/lib/afipws/client.rb +48 -0
- data/lib/afipws/excepciones.rb +13 -0
- data/lib/afipws/type_conversions.rb +38 -0
- data/lib/afipws/version.rb +3 -0
- data/lib/afipws/wsaa.rb +77 -0
- data/lib/afipws/wsfe.rb +88 -0
- data/lib/core_ext/hash.rb +21 -0
- data/lib/core_ext/string.rb +9 -0
- data/spec/afipws/test.crt +17 -0
- data/spec/afipws/test.key +15 -0
- data/spec/afipws/type_conversions_spec.rb +30 -0
- data/spec/afipws/wsaa_spec.rb +68 -0
- data/spec/afipws/wsfe_spec.rb +163 -0
- data/spec/core_ext/hash_spec.rb +44 -0
- data/spec/fixtures/fe_comp_consultar/success.xml +41 -0
- data/spec/fixtures/fe_comp_tot_x_request/success.xml +9 -0
- data/spec/fixtures/fe_comp_ultimo_autorizado/success.xml +11 -0
- data/spec/fixtures/fe_dummy/success.xml +11 -0
- data/spec/fixtures/fe_param_get_cotizacion/dolar.xml +13 -0
- data/spec/fixtures/fe_param_get_cotizacion/inexistente.xml +14 -0
- data/spec/fixtures/fe_param_get_tipos_cbte/failure_1_error.xml +14 -0
- data/spec/fixtures/fe_param_get_tipos_cbte/failure_2_errors.xml +18 -0
- data/spec/fixtures/fe_param_get_tipos_cbte/success.xml +22 -0
- data/spec/fixtures/fe_param_get_tipos_doc/success.xml +16 -0
- data/spec/fixtures/fe_param_get_tipos_iva/success.xml +16 -0
- data/spec/fixtures/fe_param_get_tipos_monedas/success.xml +22 -0
- data/spec/fixtures/fe_param_get_tipos_tributos/success.xml +16 -0
- data/spec/fixtures/fecae_solicitar/autorizacion_1_cbte.xml +30 -0
- data/spec/fixtures/fecae_solicitar/autorizacion_2_cbtes.xml +41 -0
- data/spec/fixtures/fecae_solicitar/observaciones.xml +40 -0
- data/spec/fixtures/login_cms/fault.xml +12 -0
- data/spec/fixtures/login_cms/success.xml +16 -0
- data/spec/fixtures/login_cms/token_expirado.xml +14 -0
- data/spec/fixtures/wsaa.wsdl +103 -0
- data/spec/fixtures/wsfe.wsdl +1372 -0
- data/spec/manual/autorizar_comprobante.rb +22 -0
- data/spec/manual/generar_keys.txt +8 -0
- data/spec/manual/obtener_ta.rb +18 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/matchers.rb +37 -0
- metadata +237 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm 1.9.2
|
data/Gemfile
ADDED
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
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,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
|
data/lib/afipws/wsaa.rb
ADDED
@@ -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
|
data/lib/afipws/wsfe.rb
ADDED
@@ -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
|