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