Afip 0.8.2 → 0.8.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +8 -0
- data/Afip-0.8.3.gem +0 -0
- data/Afip-0.8.4.gem +0 -0
- data/Afip.gemspec +40 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +48 -0
- data/LICENSE.txt +21 -0
- data/README.md +39 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/Afip/auth_data.rb +71 -0
- data/lib/Afip/authorizer.rb +10 -0
- data/lib/Afip/bill.rb +197 -0
- data/lib/Afip/constants.rb +80 -0
- data/lib/Afip/core_ext/float.rb +8 -0
- data/lib/Afip/core_ext/hash.rb +23 -0
- data/lib/Afip/core_ext/string.rb +12 -0
- data/lib/Afip/ctg.rb +234 -0
- data/lib/Afip/padron.rb +183 -0
- data/lib/Afip/version.rb +3 -0
- data/lib/Afip/wsaa.rb +94 -0
- data/lib/Afip.rb +59 -0
- metadata +25 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1fe8d659a0e6df6e63828786c78261e24c6ff3161389dc68d4acdaad14c61174
|
4
|
+
data.tar.gz: 06a6cf1566d575a28facc865904892f8497cebc58c4fb20114f65de8c7d4a784
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 11a01e92e4355c882abe6a908b378bf58edb2c35d6f096764bc3b2da9c5943aeec3429f0dea0027ff28adea88091dc62fe5e2ff6b2fc51c3b6da3b0f4c9646e0
|
7
|
+
data.tar.gz: ccec3d5eac35c165e8f8d15b7c9e6f0ca7a0d005d7afd257650d382cd21984799a7c1ca1cd802ba6cc67c4fec42c1de46dc5cb4c6b7e671b5d38eae10bf4c05c
|
data/.gitignore
ADDED
data/Afip-0.8.3.gem
ADDED
Binary file
|
data/Afip-0.8.4.gem
ADDED
Binary file
|
data/Afip.gemspec
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "Afip/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "Afip"
|
8
|
+
spec.version = Afip::VERSION
|
9
|
+
spec.authors = ["Facundo A. Díaz Martínez"]
|
10
|
+
spec.email = ["facundo_diaz_martinez@hotmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Comunicacion con AFIP}
|
13
|
+
spec.description = %q{Gema para la comunicacion con los Web Services de AFIP.}
|
14
|
+
spec.homepage = "https://www.desideral.com"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
18
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
21
|
+
else
|
22
|
+
raise "RubyGems 2.0 or newer is required to protect against " \
|
23
|
+
"public gem pushes."
|
24
|
+
end
|
25
|
+
|
26
|
+
# Specify which files should be added to the gem when it is released.
|
27
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
28
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
29
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
30
|
+
end
|
31
|
+
|
32
|
+
spec.bindir = "exe"
|
33
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
34
|
+
spec.require_paths = ["lib"]
|
35
|
+
|
36
|
+
spec.add_dependency "savon"
|
37
|
+
spec.add_dependency "httpi"
|
38
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
39
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
40
|
+
end
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
Afip (0.8.4)
|
5
|
+
httpi
|
6
|
+
savon
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
akami (1.3.1)
|
12
|
+
gyoku (>= 0.4.0)
|
13
|
+
nokogiri
|
14
|
+
builder (3.2.3)
|
15
|
+
gyoku (1.3.1)
|
16
|
+
builder (>= 2.1.2)
|
17
|
+
httpi (2.4.4)
|
18
|
+
rack
|
19
|
+
socksify
|
20
|
+
mini_portile2 (2.4.0)
|
21
|
+
nokogiri (1.9.1)
|
22
|
+
mini_portile2 (~> 2.4.0)
|
23
|
+
nori (2.6.0)
|
24
|
+
rack (2.0.6)
|
25
|
+
rake (10.5.0)
|
26
|
+
savon (2.12.0)
|
27
|
+
akami (~> 1.2)
|
28
|
+
builder (>= 2.1.2)
|
29
|
+
gyoku (~> 1.2)
|
30
|
+
httpi (~> 2.3)
|
31
|
+
nokogiri (>= 1.8.1)
|
32
|
+
nori (~> 2.4)
|
33
|
+
wasabi (~> 3.4)
|
34
|
+
socksify (1.7.1)
|
35
|
+
wasabi (3.5.0)
|
36
|
+
httpi (~> 2.0)
|
37
|
+
nokogiri (>= 1.4.2)
|
38
|
+
|
39
|
+
PLATFORMS
|
40
|
+
ruby
|
41
|
+
|
42
|
+
DEPENDENCIES
|
43
|
+
Afip!
|
44
|
+
bundler (~> 1.16)
|
45
|
+
rake (~> 10.0)
|
46
|
+
|
47
|
+
BUNDLED WITH
|
48
|
+
1.17.1
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Facundo A. Díaz Martínez
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# Afip
|
2
|
+
|
3
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/Afip`. To experiment with that code, run `bin/console` for an interactive prompt.
|
4
|
+
|
5
|
+
TODO: Delete this and the text above, and describe your gem
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'Afip'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install Afip
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
TODO: Write usage instructions here
|
26
|
+
|
27
|
+
## Development
|
28
|
+
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
+
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
|
+
|
33
|
+
## Contributing
|
34
|
+
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/Afip.
|
36
|
+
|
37
|
+
## License
|
38
|
+
|
39
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "Afip"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
module Afip
|
2
|
+
|
3
|
+
# This class handles authorization data
|
4
|
+
#
|
5
|
+
class AuthData
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
attr_accessor :environment, :todays_data_file_name
|
10
|
+
|
11
|
+
# Fetches WSAA Authorization Data to build the datafile for the day.
|
12
|
+
# It requires the private key file and the certificate to exist and
|
13
|
+
# to be configured as Afip.pkey and Afip.cert
|
14
|
+
#
|
15
|
+
def fetch(service = "wsfe")
|
16
|
+
unless File.exists?(Afip.pkey)
|
17
|
+
raise "Archivo de llave privada no encontrado en #{ Afip.pkey }"
|
18
|
+
end
|
19
|
+
|
20
|
+
unless File.exists?(Afip.cert)
|
21
|
+
raise "Archivo certificado no encontrado en #{ Afip.cert }"
|
22
|
+
end
|
23
|
+
|
24
|
+
unless File.exists?(todays_data_file_name)
|
25
|
+
Afip::Wsaa.login(service)
|
26
|
+
end
|
27
|
+
|
28
|
+
YAML.load_file(todays_data_file_name).each do |k, v|
|
29
|
+
Afip.const_set(k.to_s.upcase, v) unless Afip.const_defined?(k.to_s.upcase)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns the authorization hash, containing the Token, Signature and Cuit
|
34
|
+
# @return [Hash]
|
35
|
+
#
|
36
|
+
def auth_hash(service = "wsfe")
|
37
|
+
fetch unless Afip.constants.include?(:TOKEN) && Afip.constants.include?(:SIGN)
|
38
|
+
case service
|
39
|
+
when "wsfe"
|
40
|
+
{ 'Token' => Afip::TOKEN, 'Sign' => Afip::SIGN, 'Cuit' => Afip.cuit }
|
41
|
+
when "ws_sr_padron_a4"
|
42
|
+
{ 'token' => Afip::TOKEN, 'sign' => Afip::SIGN, 'cuitRepresentado' => Afip.cuit }
|
43
|
+
when "wsctg"
|
44
|
+
{ 'token' => Afip::TOKEN, 'sign' => Afip::SIGN, 'cuitRepresentado' => Afip.cuit }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns the right wsaa url for the specific environment
|
49
|
+
# @return [String]
|
50
|
+
#
|
51
|
+
def wsaa_url
|
52
|
+
Afip::URLS[Afip.environment][:wsaa]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns the right wsfe url for the specific environment
|
56
|
+
# @return [String]
|
57
|
+
#
|
58
|
+
def wsfe_url
|
59
|
+
raise 'Environment not sent to either :test or :production' unless Afip::URLS.keys.include? environment
|
60
|
+
Afip::URLS[Afip.environment][:wsfe]
|
61
|
+
end
|
62
|
+
|
63
|
+
# Creates the data file name for a cuit number and the current day
|
64
|
+
# @return [String]
|
65
|
+
#
|
66
|
+
def todays_data_file_name
|
67
|
+
@todays_data_file = "/tmp/#{environment.to_s}_Afip_#{ Afip.cuit }_#{ Time.new.strftime('%Y_%m_%d') }.yml"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/lib/Afip/bill.rb
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
module Afip
|
2
|
+
class Bill
|
3
|
+
attr_reader :cbte_type, :body, :response, :fecha_emision, :total, :client
|
4
|
+
attr_accessor :net, :doc_num, :iva_cond, :documento, :concepto, :moneda,
|
5
|
+
:due_date, :fch_serv_desde, :fch_serv_hasta, :fch_emision,
|
6
|
+
:ivas, :sale_point
|
7
|
+
|
8
|
+
def initialize(attrs={})
|
9
|
+
@client = Bill.set_client
|
10
|
+
@sale_point = attrs[:sale_point]
|
11
|
+
@body = { "Auth" => Afip.auth_hash }
|
12
|
+
@net = attrs[:net] || 0
|
13
|
+
@documento = attrs[:documento] || Afip.default_documento
|
14
|
+
@moneda = attrs[:moneda] || Afip.default_moneda
|
15
|
+
@iva_cond = attrs[:iva_cond]
|
16
|
+
@concepto = attrs[:concepto] || Afip.default_concepto
|
17
|
+
@ivas = attrs[:ivas] || Array.new # [ 1, 100.00, 10.50 ], [ 2, 100.00, 21.00 ]
|
18
|
+
@fecha_emision = attrs[:fch_emision] || Time.new
|
19
|
+
@fch_serv_hasta = attrs[:fch_serv_hasta]
|
20
|
+
@fch_serv_desde = attrs[:fch_serv_desde]
|
21
|
+
@due_date = attrs[:due_date]
|
22
|
+
@cbte_type = Afip::BILL_TYPE[Afip.own_iva_cond][iva_cond]
|
23
|
+
@total = net.zero? ? 0 : net + iva_sum
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.get_ptos_vta
|
27
|
+
client = set_client
|
28
|
+
body = { "Auth" => Afip.auth_hash }
|
29
|
+
response = client.call(:fe_param_get_ptos_venta, message: body)
|
30
|
+
if response.body[:fe_param_get_ptos_venta_response][:fe_param_get_ptos_venta_result][:errors].nil?
|
31
|
+
if response.body[:fe_param_get_ptos_venta_response][:fe_param_get_ptos_venta_result][:result_get][:pto_venta].is_a?(Hash)
|
32
|
+
response.body[:fe_param_get_ptos_venta_response][:fe_param_get_ptos_venta_result][:result_get][:pto_venta][:nro]
|
33
|
+
else
|
34
|
+
response.body[:fe_param_get_ptos_venta_response][:fe_param_get_ptos_venta_result][:result_get][:pto_venta].map{|r| r[:nro]}
|
35
|
+
end
|
36
|
+
else
|
37
|
+
[]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.set_client
|
42
|
+
Afip::AuthData.fetch
|
43
|
+
@client = Savon.client(
|
44
|
+
wsdl: Afip.service_url,
|
45
|
+
namespaces: { "xmlns" => "http://ar.gov.afip.dif.FEV1/" },
|
46
|
+
log_level: :debug,
|
47
|
+
ssl_cert_key_file: Afip.pkey,
|
48
|
+
ssl_cert_file: Afip.cert,
|
49
|
+
ssl_verify_mode: :none,
|
50
|
+
read_timeout: 90,
|
51
|
+
open_timeout: 90,
|
52
|
+
headers: { "Accept-Encoding" => "gzip, deflate", "Connection" => "Keep-Alive" }
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
def authorize
|
57
|
+
body = setup_bill
|
58
|
+
response = client.call(:fecae_solicitar, message: body)
|
59
|
+
setup_response(response.to_hash)
|
60
|
+
authorized?
|
61
|
+
end
|
62
|
+
|
63
|
+
def setup_bill
|
64
|
+
array_ivas = Array.new
|
65
|
+
ivas.each{ |i|
|
66
|
+
array_ivas << {
|
67
|
+
"Id" => i[0],
|
68
|
+
"BaseImp" => i[1].round(2),
|
69
|
+
"Importe" => i[2].round(2) }
|
70
|
+
}
|
71
|
+
|
72
|
+
fecaereq = {
|
73
|
+
"FeCAEReq" => {
|
74
|
+
"FeCabReq" => Afip::Bill.header(cbte_type, sale_point),
|
75
|
+
"FeDetReq" => {
|
76
|
+
"FECAEDetRequest" => {
|
77
|
+
"Concepto" => Afip::CONCEPTOS[concepto],
|
78
|
+
"DocTipo" => Afip::DOCUMENTOS[documento],
|
79
|
+
"DocNro" => doc_num,
|
80
|
+
"CbteFch" => fecha_emision.strftime('%Y%m%d'),
|
81
|
+
"ImpTotConc" => 0.00,
|
82
|
+
"ImpNeto" => net.to_f,
|
83
|
+
"MonId" => Afip::MONEDAS[moneda][:codigo],
|
84
|
+
"MonCotiz" => exchange_rate,
|
85
|
+
"ImpOpEx" => 0.00,
|
86
|
+
"ImpTrib" => 0.00,
|
87
|
+
"ImpTotal" => (Afip.own_iva_cond == :responsable_monotributo ? net : total).to_f.round(2),
|
88
|
+
"CbteDesde" => next_bill_number,
|
89
|
+
"CbteHasta" => next_bill_number
|
90
|
+
}
|
91
|
+
}
|
92
|
+
}
|
93
|
+
}
|
94
|
+
|
95
|
+
detail = fecaereq["FeCAEReq"]["FeDetReq"]["FECAEDetRequest"]
|
96
|
+
|
97
|
+
if (Afip.own_iva_cond == :responsable_inscripto)
|
98
|
+
detail["ImpIVA"] = iva_sum
|
99
|
+
detail["Iva"] = { "AlicIva" => array_ivas }
|
100
|
+
end
|
101
|
+
|
102
|
+
unless concepto == "Productos" # En "Productos" ("01"), si se mandan estos parámetros la afip rechaza.
|
103
|
+
detail.merge!({"FchServDesde" => fch_serv_desde.strftime("%Y%m%d"),
|
104
|
+
"FchServHasta" => fch_serv_hasta.strftime("%Y%m%d"),
|
105
|
+
"FchVtoPago" => due_date.strftime("%Y%m%d")})
|
106
|
+
end
|
107
|
+
|
108
|
+
body.merge!(fecaereq)
|
109
|
+
pp body
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.header(cbte_type, sale_point)
|
113
|
+
{"CantReg" => "1", "CbteTipo" => cbte_type, "PtoVta" => sale_point}
|
114
|
+
end
|
115
|
+
|
116
|
+
def exchange_rate
|
117
|
+
return 1 if moneda == :peso
|
118
|
+
response = client.call :fe_param_get_cotizacion do
|
119
|
+
message = body.merge!({"MonId" => Afip::MONEDAS[moneda][:codigo]})
|
120
|
+
end
|
121
|
+
response.to_hash[:fe_param_get_cotizacion_response][:fe_param_get_cotizacion_result][:result_get][:mon_cotiz].to_f
|
122
|
+
end
|
123
|
+
|
124
|
+
def iva_sum
|
125
|
+
iva_sum = 0.0
|
126
|
+
self.ivas.each{ |i|
|
127
|
+
iva_sum += i[2]
|
128
|
+
}
|
129
|
+
return iva_sum.round(2)
|
130
|
+
end
|
131
|
+
|
132
|
+
def next_bill_number
|
133
|
+
var = {"Auth" => Afip.auth_hash, "PtoVta" => sale_point, "CbteTipo" => cbte_type}
|
134
|
+
resp = client.call :fe_comp_ultimo_autorizado do
|
135
|
+
message(var)
|
136
|
+
end
|
137
|
+
|
138
|
+
resp.to_hash[:fe_comp_ultimo_autorizado_response][:fe_comp_ultimo_autorizado_result][:cbte_nro].to_i + 1
|
139
|
+
end
|
140
|
+
|
141
|
+
def setup_response(response)
|
142
|
+
# TODO: turn this into an all-purpose Response class
|
143
|
+
|
144
|
+
result = response[:fecae_solicitar_response][:fecae_solicitar_result]
|
145
|
+
|
146
|
+
if not result[:fe_det_resp] or not result[:fe_cab_resp] then
|
147
|
+
# Si no obtuvo respuesta ni cabecera ni detalle, evito hacer '[]' sobre algo indefinido.
|
148
|
+
# Ejemplo: Error con el token-sign de WSAA
|
149
|
+
keys, values = {
|
150
|
+
:errores => result[:errors],
|
151
|
+
:header_result => {:resultado => "X" },
|
152
|
+
:observaciones => nil
|
153
|
+
}.to_a.transpose
|
154
|
+
@response = (defined?(Struct::ResponseMal) ? Struct::ResponseMal : Struct.new("ResponseMal", *keys)).new(*values)
|
155
|
+
return
|
156
|
+
end
|
157
|
+
|
158
|
+
response_header = result[:fe_cab_resp]
|
159
|
+
response_detail = result[:fe_det_resp][:fecae_det_response]
|
160
|
+
|
161
|
+
request_header = body["FeCAEReq"]["FeCabReq"].underscore_keys.symbolize_keys
|
162
|
+
request_detail = body["FeCAEReq"]["FeDetReq"]["FECAEDetRequest"].underscore_keys.symbolize_keys
|
163
|
+
|
164
|
+
# Esto no funciona desde que se soportan múltiples alícuotas de iva simultáneas
|
165
|
+
# FIX ? TO-DO
|
166
|
+
# iva = request_detail.delete(:iva)["AlicIva"].underscore_keys.symbolize_keys
|
167
|
+
# request_detail.merge!(iva)
|
168
|
+
|
169
|
+
if result[:errors] then
|
170
|
+
response_detail.merge!( result[:errors] )
|
171
|
+
end
|
172
|
+
|
173
|
+
response_hash = {
|
174
|
+
:header_result => response_header.delete(:resultado),
|
175
|
+
:authorized_on => response_header.delete(:fch_proceso),
|
176
|
+
:detail_result => response_detail.delete(:resultado),
|
177
|
+
:cae_due_date => response_detail.delete(:cae_fch_vto),
|
178
|
+
:cae => response_detail.delete(:cae),
|
179
|
+
:iva_id => request_detail.delete(:id),
|
180
|
+
:iva_importe => request_detail.delete(:importe),
|
181
|
+
:moneda => request_detail.delete(:mon_id),
|
182
|
+
:cotizacion => request_detail.delete(:mon_cotiz),
|
183
|
+
:iva_base_imp => request_detail.delete(:base_imp),
|
184
|
+
:doc_num => request_detail.delete(:doc_nro),
|
185
|
+
:observaciones => response_detail.delete(:observaciones),
|
186
|
+
:errores => response_detail.delete(:err)
|
187
|
+
}.merge!(request_header).merge!(request_detail)
|
188
|
+
|
189
|
+
keys, values = response_hash.to_a.transpose
|
190
|
+
@response = (defined?(Struct::Response) ? Struct::Response : Struct.new("Response", *keys)).new(*values)
|
191
|
+
end
|
192
|
+
|
193
|
+
def authorized?
|
194
|
+
!response.nil? && response.header_result == "A" && response.detail_result == "A"
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Afip
|
2
|
+
CBTE_TIPO = {
|
3
|
+
"01"=>"Factura A",
|
4
|
+
"02"=>"Nota de Débito A",
|
5
|
+
"03"=>"Nota de Crédito A",
|
6
|
+
"06"=>"Factura B",
|
7
|
+
"07"=>"Nota de Debito B",
|
8
|
+
"08"=>"Nota de Credito B",
|
9
|
+
"11"=>"Factura C",
|
10
|
+
"12"=>"Nota de Debito C",
|
11
|
+
"13"=>"Nota de Credito C"
|
12
|
+
}
|
13
|
+
|
14
|
+
CONCEPTOS = {"Productos"=>"01", "Servicios"=>"02", "Productos y Servicios"=>"03"}
|
15
|
+
|
16
|
+
DOCUMENTOS = {"CUIT"=>"80", "CUIL"=>"86", "CDI"=>"87", "LE"=>"89", "LC"=>"90", "CI Extranjera"=>"91", "en tramite"=>"92", "Acta Nacimiento"=>"93", "CI Bs. As. RNP"=>"95", "DNI"=>"96", "Pasaporte"=>"94", "Doc. (Otro)"=>"99"}
|
17
|
+
|
18
|
+
MONEDAS = {
|
19
|
+
:peso => {:codigo => "PES", :nombre =>"Pesos Argentinos"},
|
20
|
+
:dolar => {:codigo => "DOL", :nombre =>"Dolar Estadounidense"},
|
21
|
+
:real => {:codigo => "012", :nombre =>"Real"},
|
22
|
+
:euro => {:codigo => "060", :nombre =>"Euro"},
|
23
|
+
:oro => {:codigo => "049", :nombre =>"Gramos de Oro Fino"}
|
24
|
+
}
|
25
|
+
|
26
|
+
ALIC_IVA = [["03", 0], ["04", 0.105], ["05", 0.21], ["06", 0.27]]
|
27
|
+
|
28
|
+
IVA_COND = ["Responsable Inscripto", "Responsable Monotributo"]
|
29
|
+
|
30
|
+
BILL_TYPE = {
|
31
|
+
:responsable_inscripto => {
|
32
|
+
:responsable_inscripto => "01",
|
33
|
+
:consumidor_final => "06",
|
34
|
+
:exento => "06",
|
35
|
+
:responsable_monotributo => "06",
|
36
|
+
:nota_credito_a => "03",
|
37
|
+
:nota_credito_b => "08",
|
38
|
+
:nota_debito_a => "02",
|
39
|
+
:nota_debito_b => "07"
|
40
|
+
},
|
41
|
+
:responsable_monotributo => {
|
42
|
+
:responsable_inscripto => "11",
|
43
|
+
:consumidor_final => "11",
|
44
|
+
:exento => "11",
|
45
|
+
:responsable_monotributo => "11",
|
46
|
+
:nota_credito_c => "13",
|
47
|
+
:nota_debito_c => "12"
|
48
|
+
}
|
49
|
+
}
|
50
|
+
|
51
|
+
AVAILABLE_TYPES = {
|
52
|
+
:responsable_inscripto => {
|
53
|
+
:responsable_inscripto => ["01", "02", "03"],
|
54
|
+
:consumidor_final => ["06", "07", "08"],
|
55
|
+
:exento => ["06", "07", "08"],
|
56
|
+
:responsable_monotributo => ["06", "07", "08"],
|
57
|
+
},
|
58
|
+
:responsable_monotributo => {
|
59
|
+
:responsable_inscripto => ["11", "12", "13"],
|
60
|
+
:consumidor_final => ["11", "12", "13"],
|
61
|
+
:exento => ["11", "12", "13"],
|
62
|
+
:responsable_monotributo => ["11", "12", "13"]
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
URLS =
|
67
|
+
{
|
68
|
+
:test => {
|
69
|
+
:wsaa => 'https://wsaahomo.afip.gov.ar/ws/services/LoginCms',
|
70
|
+
:padron => "https://awshomo.afip.gov.ar/sr-padron/webservices/personaServiceA5?WSDL",
|
71
|
+
:wsfe => 'https://wswhomo.afip.gov.ar/wsfev1/service.asmx?WSDL'
|
72
|
+
},
|
73
|
+
:production => {
|
74
|
+
:wsaa => 'https://wsaa.afip.gov.ar/ws/services/LoginCms',
|
75
|
+
:padron => "https://aws.afip.gov.ar/sr-padron/webservices/personaServiceA5?WSDL",
|
76
|
+
:wsfe => 'https://servicios1.afip.gov.ar/wsfev1/service.asmx'
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
class Float
|
2
|
+
def round_with_precision(precision = nil)
|
3
|
+
precision.nil? ? round : (self * (10 ** precision)).round / (10 ** precision).to_f
|
4
|
+
end
|
5
|
+
def round_up_with_precision(precision = nil)
|
6
|
+
precision.nil? ? round : ((self * (10 ** precision)).round + 1) / (10 ** precision).to_f
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class Hash
|
2
|
+
def symbolize_keys!
|
3
|
+
keys.each do |key|
|
4
|
+
self[(key.to_sym rescue key) || key] = delete(key)
|
5
|
+
end
|
6
|
+
self
|
7
|
+
end unless method_defined?(:symbolize_keys!)
|
8
|
+
|
9
|
+
def symbolize_keys
|
10
|
+
dup.symbolize_keys!
|
11
|
+
end unless method_defined?(:symbolize_keys)
|
12
|
+
|
13
|
+
def underscore_keys!
|
14
|
+
keys.each do |key|
|
15
|
+
self[(key.underscore rescue key) || key] = delete(key)
|
16
|
+
end
|
17
|
+
self
|
18
|
+
end unless method_defined?(:underscore_keys!)
|
19
|
+
|
20
|
+
def underscore_keys
|
21
|
+
dup.underscore_keys!
|
22
|
+
end unless method_defined?(:underscore_keys)
|
23
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# Stolen from activesupport/lib/active_support/inflector/methods.rb, line 48
|
2
|
+
class String
|
3
|
+
def underscore
|
4
|
+
word = self.to_s.dup
|
5
|
+
word.gsub!(/::/, '/')
|
6
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
|
7
|
+
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
8
|
+
word.tr!("-", "_")
|
9
|
+
word.downcase!
|
10
|
+
word
|
11
|
+
end
|
12
|
+
end
|
data/lib/Afip/ctg.rb
ADDED
@@ -0,0 +1,234 @@
|
|
1
|
+
module Afip
|
2
|
+
class CTG
|
3
|
+
attr_reader :client, :base_imp, :total
|
4
|
+
attr_accessor :ctg_num, :cp_num, :cod_especie, :cuit_canjeador,:rccc, :cuit_destino, :cuit_destinatario, :localidad_origen, :localidad_destino,
|
5
|
+
:cosecha, :peso, :cuit_transportista, :horas, :patente, :km, :cuit_corredor, :remitente_com, :body
|
6
|
+
|
7
|
+
def initialize(attrs = {})
|
8
|
+
Afip::AuthData.fetch("wsctg")
|
9
|
+
@client = Savon.client(
|
10
|
+
ssl_cert_key_file: Afip.pkey,
|
11
|
+
ssl_cert_file: Afip.cert,
|
12
|
+
env_namespace: :soapenv,
|
13
|
+
namespace_identifier: :ctg,
|
14
|
+
log: true,
|
15
|
+
logger: Rails.logger,
|
16
|
+
log_level: :debug,
|
17
|
+
pretty_print_xml: true,
|
18
|
+
encoding: 'UTF-8',
|
19
|
+
ssl_version: :TLSv1,
|
20
|
+
wsdl: Afip.service_url
|
21
|
+
)
|
22
|
+
|
23
|
+
@ctg_num = attrs[:ctg_num]
|
24
|
+
@cp_num = attrs[:cp_num]
|
25
|
+
@cod_especie = attrs[:cod_especie]
|
26
|
+
@cuit_canjeador = attrs[:cuit_canjeador]
|
27
|
+
@rccc = attrs[:rccc]
|
28
|
+
@cuit_destino = attrs[:cuit_destino]
|
29
|
+
@cuit_destinatario = attrs[:cuit_destinatario]
|
30
|
+
@localidad_origen = attrs[:localidad_origen]
|
31
|
+
@localidad_destino = attrs[:localidad_destino]
|
32
|
+
@cosecha = attrs[:cosecha]
|
33
|
+
@peso = attrs[:peso]
|
34
|
+
@cuit_transportista = attrs[:cuit_transportista]
|
35
|
+
@horas = attrs[:horas]
|
36
|
+
@patente = attrs[:patente]
|
37
|
+
@km = attrs[:km]
|
38
|
+
@cuit_corredor = attrs[:cuit_corredor]
|
39
|
+
@remitente_com = attrs[:remitente_com]
|
40
|
+
@cuit_chofer = attrs[:cuit_chofer]
|
41
|
+
@cant_kilos_carta_porte = attrs[:cant_kilos_carta_porte]
|
42
|
+
@establecimiento = attrs[:establecimiento]
|
43
|
+
@cuit_solicitante = attrs[:cuit_solicitante]
|
44
|
+
@fecha_desde = attrs[:fecha_desde]
|
45
|
+
@fecha_hasta = attrs[:fecha_hasta]
|
46
|
+
|
47
|
+
@body = {"request" =>{"auth" => Afip.auth_hash("wsctg")}}
|
48
|
+
end
|
49
|
+
|
50
|
+
def solicitar_ctg_inicial
|
51
|
+
pp body = setup_ctg
|
52
|
+
|
53
|
+
pp response = client.call(:solicitar_ctg_inicial,message: body)
|
54
|
+
|
55
|
+
setup_response(response.to_hash)
|
56
|
+
|
57
|
+
self.authorized?
|
58
|
+
end
|
59
|
+
|
60
|
+
def setup_ctg
|
61
|
+
|
62
|
+
datos = {
|
63
|
+
"datosSolicitarCTGInicial" =>{
|
64
|
+
"cartaPorte" => @cp_num, #long
|
65
|
+
"codigoEspecie" => @cod_especie, #int
|
66
|
+
#"cuitCanjeador" => @cuit_canjeador.to_i, #long
|
67
|
+
#"remitenteComercialComoCanjeador" => @rccc,
|
68
|
+
"cuitDestino" => @cuit_destino, #long
|
69
|
+
"cuitDestinatario" => @cuit_destinatario, #long
|
70
|
+
"codigoLocalidadOrigen" => @localidad_origen, #int
|
71
|
+
"codigoLocalidadDestino" => @localidad_destino, #int
|
72
|
+
"codigoCosecha" => @cosecha, #string
|
73
|
+
"pesoNeto" => @peso.to_i, #long
|
74
|
+
#"cuitTransportista" => @cuit_transportista.to_i, #long
|
75
|
+
#"cantHoras" => @horas, #int
|
76
|
+
#"patente" => @patente, #string
|
77
|
+
"kmARecorrer" => @km, #unsignedint
|
78
|
+
#"cuitCorredor" => @cuit_corredor.to_i, #long
|
79
|
+
#"remitenteComercialcomoProductor" => @remitente_com
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
@body["request"].merge!(datos)
|
84
|
+
return @body
|
85
|
+
end
|
86
|
+
|
87
|
+
def authorized?
|
88
|
+
!response.nil?
|
89
|
+
end
|
90
|
+
|
91
|
+
def anular_ctg
|
92
|
+
request = {
|
93
|
+
"datosAnularCTG" =>{
|
94
|
+
"cartaPorte" => "@ctg_num",
|
95
|
+
"ctg" => "#{self.code}"
|
96
|
+
}
|
97
|
+
}
|
98
|
+
body["request"].merge(request)
|
99
|
+
|
100
|
+
response = client.call(:anular_ctg, message: body)
|
101
|
+
end
|
102
|
+
|
103
|
+
def cambiar_destino_detinatario_ctg_rechazado
|
104
|
+
request = {
|
105
|
+
"datosCambiarDestinoDestinatarioCTGRechazado" =>{
|
106
|
+
"cartaPorte" => "@ctg_num",
|
107
|
+
"ctg" => "#{code}",
|
108
|
+
"codigoLocalidadDestino" => @destino,
|
109
|
+
"codigoLocalidadDestinatario" => @destinatario,
|
110
|
+
"kmARecorrer" => @km
|
111
|
+
}
|
112
|
+
}
|
113
|
+
body["request"].merge(request)
|
114
|
+
|
115
|
+
response = client.call(:cambiar_destino_detinatario_ctg_rechazado, message: body)
|
116
|
+
end
|
117
|
+
|
118
|
+
def confirmar_arribo
|
119
|
+
request = {
|
120
|
+
"datosConfirmarArribo" =>{
|
121
|
+
"cartaPorte" => "@ctg_num",
|
122
|
+
"ctg" => "#{self.code}",
|
123
|
+
"cuitTransportista" => @cuit_transportista,
|
124
|
+
"cuitChofer" => @cuit_chofer,
|
125
|
+
"cantKilosCartaPorte" => @cant_kilos_carta_porte
|
126
|
+
}
|
127
|
+
}
|
128
|
+
body["request"].merge(request)
|
129
|
+
|
130
|
+
response = client.call(:confirmar_arribo, message: body)
|
131
|
+
end
|
132
|
+
|
133
|
+
def confirmar_definitivo
|
134
|
+
request = {
|
135
|
+
"datosConfirmarDefinitivo" =>{
|
136
|
+
"cartaPorte" => "@ctg_num",
|
137
|
+
"ctg" => "#{self.code}",
|
138
|
+
"establecimiento" => @establecimiento,
|
139
|
+
"codigoCosecha" => @cosecha,
|
140
|
+
"pesoNeto" => @peso
|
141
|
+
}
|
142
|
+
}
|
143
|
+
body["request"].merge(request)
|
144
|
+
|
145
|
+
response = client.call(:confirmar_definitivo, message: body)
|
146
|
+
end
|
147
|
+
|
148
|
+
def consultar_cosechas
|
149
|
+
response = client.call(:consultar_cosechas, message: body)
|
150
|
+
return response["arrayCosechas"]
|
151
|
+
end
|
152
|
+
|
153
|
+
def consultar_constancia_ctg_pdf
|
154
|
+
request = {
|
155
|
+
"ctg" => "#{code}"
|
156
|
+
}
|
157
|
+
|
158
|
+
body["request"].merge(request)
|
159
|
+
|
160
|
+
response = client.call(:consultar_constancia_ctg_pdf, message: body)
|
161
|
+
end
|
162
|
+
|
163
|
+
def consultar_ctg
|
164
|
+
request = {
|
165
|
+
"consultarCTGDatos" =>{
|
166
|
+
"cartaPorte" => "@ctg_num",
|
167
|
+
"ctg" => "#{self.code}",
|
168
|
+
"patente" => @patente,
|
169
|
+
"cuitSolicitante" => @cuit_solicitante,
|
170
|
+
"cuitDestino" => @destino,
|
171
|
+
"fechaEmisionDesde" => @fecha_desde,
|
172
|
+
"fechaEmisionHsta" => @fecha_hasta,
|
173
|
+
"cuitCorredor" => @cuit_corredor
|
174
|
+
}
|
175
|
+
}
|
176
|
+
body["request"].merge(request)
|
177
|
+
|
178
|
+
response = client.call(:consultar_ctg, message: body)
|
179
|
+
end
|
180
|
+
|
181
|
+
def consultar_ctg_rechazados
|
182
|
+
response = client.call(:consultar_ctg_rechazados, message: body)
|
183
|
+
return response.to_hash["response"]["arrayConsultarCTGRechazados"]
|
184
|
+
end
|
185
|
+
|
186
|
+
def consultar_detalle_ctg
|
187
|
+
request = {
|
188
|
+
"ctg" => "#{code}"
|
189
|
+
}
|
190
|
+
|
191
|
+
body["request"].merge(request)
|
192
|
+
|
193
|
+
response = client.call(:consultar_constancia_ctg_pdf, message: body)
|
194
|
+
return response.to_hash["response"]["consultarDetalleCTGDatos"]
|
195
|
+
end
|
196
|
+
|
197
|
+
def consultar_especies
|
198
|
+
response = client.call(:consultar_especies, message: body)
|
199
|
+
response.body[:consultar_especies_response][:response][:array_especies][:especie].map{|c| [c[:codigo],c[:descripcion]]}
|
200
|
+
end
|
201
|
+
|
202
|
+
def consultar_establecimientos
|
203
|
+
response = client.call(:consultar_establecimientos, message: body)
|
204
|
+
response.body[:consultar_especies_response][:response][:array_establecimientos][:establecimiento].map{|c| [c]}
|
205
|
+
end
|
206
|
+
|
207
|
+
def consultar_provincias
|
208
|
+
response = client.call(:consultar_provincias, message: body)
|
209
|
+
response.body[:consultar_provincias_response][:consultar_provincias_response][:array_provincias][:provincia].map{|c| [c[:codigo],c[:descripcion]]}
|
210
|
+
end
|
211
|
+
|
212
|
+
def consultar_cosechas
|
213
|
+
response = client.call(:consultar_cosechas, message: body)
|
214
|
+
response.body[:consultar_cosechas_response][:response][:array_cosechas][:cosecha].map{|c| [c[:codigo],c[:descripcion]]}
|
215
|
+
end
|
216
|
+
|
217
|
+
def consultar_localidades(city)
|
218
|
+
body["request"]["codigoProvincia"] = city
|
219
|
+
response = client.call(:consultar_localidades_por_provincia, message: body)
|
220
|
+
if response.body[:consultar_localidades_por_provincia_response][:response][:array_localidades][:localidad].class.name != "Array"
|
221
|
+
[response.body[:consultar_localidades_por_provincia_response][:response][:array_localidades][:localidad]].map{|c| [c[:codigo],c[:descripcion]]}
|
222
|
+
else
|
223
|
+
response.body[:consultar_localidades_por_provincia_response][:response][:array_localidades][:localidad].map{|c| [c[:codigo],c[:descripcion]]}
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
private
|
228
|
+
|
229
|
+
def setup_response(response)
|
230
|
+
# TODO: turn this into an all-purpose Response class
|
231
|
+
pp response
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
data/lib/Afip/padron.rb
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
module Afip
|
2
|
+
class Padron
|
3
|
+
attr_reader :client, :body, :fault_code
|
4
|
+
attr_accessor :dni, :tipo, :data
|
5
|
+
def initialize(attrs = {})
|
6
|
+
Afip::AuthData.environment = Afip.environment || :production
|
7
|
+
url = Afip::AuthData.environment == :production ? "aws" : "awshomo"
|
8
|
+
Afip.service_url = "https://#{url}.afip.gov.ar/sr-padron/webservices/personaServiceA4?WSDL"
|
9
|
+
Afip.cuit ||= "20368642682"
|
10
|
+
Afip.cert ||= "#{Afip.root}/lib/Afip/certs/desideral_prod.crt"
|
11
|
+
Afip.pkey ||= "#{Afip.root}/lib/Afip/certs/desideral.key"
|
12
|
+
Afip::AuthData.fetch("ws_sr_padron_a4")
|
13
|
+
|
14
|
+
@client = Savon.client(
|
15
|
+
ssl_cert_key_file: Afip.pkey,
|
16
|
+
ssl_cert_file: Afip.cert,
|
17
|
+
env_namespace: :soapenv,
|
18
|
+
namespace_identifier: :a4,
|
19
|
+
encoding: 'UTF-8',
|
20
|
+
wsdl: Afip.service_url
|
21
|
+
)
|
22
|
+
|
23
|
+
@dni = attrs[:dni].rjust(8, "0")
|
24
|
+
@tipo = attrs[:tipo]
|
25
|
+
@cuit = get_cuit
|
26
|
+
end
|
27
|
+
|
28
|
+
def get_persona
|
29
|
+
body = setup_body
|
30
|
+
|
31
|
+
response = client.call(:get_persona,message: body)
|
32
|
+
rescue Savon::SOAPFault => error
|
33
|
+
if !error.blank?
|
34
|
+
@fault_code = error.to_hash[:fault][:faultstring]
|
35
|
+
else
|
36
|
+
@fault_code = nil
|
37
|
+
end
|
38
|
+
return response
|
39
|
+
end
|
40
|
+
|
41
|
+
def get_data
|
42
|
+
@data = get_persona
|
43
|
+
if fault_code.nil?
|
44
|
+
set_data
|
45
|
+
else
|
46
|
+
return nil
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
def set_data
|
52
|
+
pp data.body
|
53
|
+
if not data.body[:get_persona_response][:persona_return][:persona][:actividad].nil?
|
54
|
+
{
|
55
|
+
:last_name => data.body[:get_persona_response][:persona_return][:persona][:apellido],
|
56
|
+
:first_name => data.body[:get_persona_response][:persona_return][:persona][:nombre],
|
57
|
+
:cuit => data.body[:get_persona_response][:persona_return][:persona][:id_persona],
|
58
|
+
:cp => data.body[:get_persona_response][:persona_return][:persona][:domicilio].last[:cod_postal],
|
59
|
+
:address => data.body[:get_persona_response][:persona_return][:persona][:domicilio].last[:direccion],
|
60
|
+
:city_id => data.body[:get_persona_response][:persona_return][:persona][:domicilio].last[:id_provincia],
|
61
|
+
:city => PROVINCIAS[data.body[:get_persona_response][:persona_return][:persona][:domicilio].last[:id_provincia]],
|
62
|
+
:locality => data.body[:get_persona_response][:persona_return][:persona][:domicilio].last[:localidad],
|
63
|
+
:birthday => data.body[:get_persona_response][:persona_return][:persona][:fecha_nacimiento].to_date
|
64
|
+
}
|
65
|
+
else
|
66
|
+
{
|
67
|
+
:last_name => Padron.divide_name(data.body[:get_persona_response][:persona_return][:persona][:apellido])[0],
|
68
|
+
:first_name => Padron.divide_name(data.body[:get_persona_response][:persona_return][:persona][:apellido])[1],
|
69
|
+
:cuit => data.body[:get_persona_response][:persona_return][:persona][:id_persona],
|
70
|
+
:cp => data.body[:get_persona_response][:persona_return][:persona].try(:[], :domicilio).try(:[], :cod_postal),
|
71
|
+
:address => data.body[:get_persona_response][:persona_return][:persona].try(:[], :domicilio).try(:[], :direccion),
|
72
|
+
:city_id => data.body[:get_persona_response][:persona_return][:persona].try(:[], :domicilio).try(:[], :id_provincia),
|
73
|
+
:city => PROVINCIAS[data.body[:get_persona_response][:persona_return][:persona].try(:[], :domicilio).try(:[], :id_provincia)],
|
74
|
+
:locality => data.body[:get_persona_response][:persona_return][:persona].try(:[], :domicilio).try(:[], :localidad),
|
75
|
+
:birthday => data.body[:get_persona_response][:persona_return][:persona][:fecha_nacimiento].to_date
|
76
|
+
}
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.divide_name(full_name)
|
81
|
+
full_name = full_name.strip.split(/\s+/)
|
82
|
+
last_name = ''
|
83
|
+
last = (full_name.count / 2) - 1
|
84
|
+
(0..last).each do |i|
|
85
|
+
if i != last
|
86
|
+
last_name += full_name[i] + ' '
|
87
|
+
else
|
88
|
+
last_name += full_name[i]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
full_name = full_name - (last_name.strip.split(/\s+/))
|
92
|
+
first_name = full_name.join(", ").gsub(",","").split.map(&:capitalize).join(' ')
|
93
|
+
last_name = last_name.split.map(&:capitalize).join(' ')
|
94
|
+
return [last_name, first_name]
|
95
|
+
end
|
96
|
+
|
97
|
+
def get_cuit
|
98
|
+
if dni.length == 11
|
99
|
+
@cuit = @dni
|
100
|
+
else
|
101
|
+
@cuit = calculate_cuit
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def calculate_cuit
|
106
|
+
multiplicador = "2345672345"
|
107
|
+
|
108
|
+
case tipo
|
109
|
+
when "F"
|
110
|
+
xy = 27
|
111
|
+
xy_dni = "27#{dni}"
|
112
|
+
when "M"
|
113
|
+
xy = 20
|
114
|
+
xy_dni = "20#{dni}"
|
115
|
+
end
|
116
|
+
verificador = 0
|
117
|
+
(0..9).each do |i|
|
118
|
+
verificador += (xy_dni.reverse[i].to_i * multiplicador[i].to_i)
|
119
|
+
end
|
120
|
+
verificador
|
121
|
+
z = verificador - (verificador / 11 * 11)
|
122
|
+
|
123
|
+
case z
|
124
|
+
when 0
|
125
|
+
z = 0
|
126
|
+
when 1
|
127
|
+
if tipo == "M"
|
128
|
+
z = 9
|
129
|
+
xy = 23
|
130
|
+
elsif tipo == "F"
|
131
|
+
z = 4
|
132
|
+
xy = 23
|
133
|
+
else
|
134
|
+
z = 11 - z
|
135
|
+
end
|
136
|
+
else
|
137
|
+
z = 11 - z
|
138
|
+
end
|
139
|
+
|
140
|
+
return "#{xy}#{dni}#{z}"
|
141
|
+
end
|
142
|
+
|
143
|
+
def setup_body
|
144
|
+
body = {
|
145
|
+
'token' => Afip::TOKEN,
|
146
|
+
'sign' => Afip::SIGN,
|
147
|
+
'cuitRepresentada' => Afip.cuit,
|
148
|
+
'idPersona' => @cuit.to_s
|
149
|
+
}
|
150
|
+
end
|
151
|
+
|
152
|
+
PROVINCIAS = {
|
153
|
+
"0" => 'CIUDAD AUTONOMA BUENOS AIRES',
|
154
|
+
"1" => 'BUENOS AIRES',
|
155
|
+
"2" => 'CATAMARCA',
|
156
|
+
"3" => 'CORDOBA',
|
157
|
+
"4" => 'CORRIENTES',
|
158
|
+
"5" => 'ENTRE RIOS',
|
159
|
+
"6" => 'JUJUY',
|
160
|
+
"7" => 'MENDOZA',
|
161
|
+
"8" => 'LA RIOJA',
|
162
|
+
"9" => 'SALTA',
|
163
|
+
"10" => 'SAN JUAN',
|
164
|
+
"11" => 'SAN LUIS',
|
165
|
+
"12" => 'SANTA FE',
|
166
|
+
"13" => 'SANTIAGO DEL ESTERO',
|
167
|
+
"14" => 'TUCUMAN',
|
168
|
+
"16" => 'CHACO',
|
169
|
+
"17" => 'CHUBUT',
|
170
|
+
"18" => 'FORMOSA',
|
171
|
+
"19" => 'MISIONES',
|
172
|
+
"20" => 'NEUQUEN',
|
173
|
+
"21" => 'LA PAMPA',
|
174
|
+
"22" => 'RIO NEGRO',
|
175
|
+
"23" => 'SANTA CRUZ',
|
176
|
+
"24" => 'TIERRA DEL FUEGO'
|
177
|
+
}
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
|
182
|
+
|
183
|
+
|
data/lib/Afip/version.rb
ADDED
data/lib/Afip/wsaa.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
module Afip
|
2
|
+
# Authorization class. Handles interactions wiht the WSAA, to provide
|
3
|
+
# valid key and signature that will last for a day.
|
4
|
+
#
|
5
|
+
class Wsaa
|
6
|
+
# Main method for authentication and authorization.
|
7
|
+
# When successful, produces the yaml file with auth data.
|
8
|
+
#
|
9
|
+
def self.login(service = "wsfe")
|
10
|
+
tra = build_tra(service)
|
11
|
+
cms = build_cms(tra)
|
12
|
+
req = build_request(cms)
|
13
|
+
auth = call_wsaa(req)
|
14
|
+
|
15
|
+
write_yaml(auth)
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
# Builds the xml for the 'Ticket de Requerimiento de Acceso'
|
20
|
+
# @return [String] containing the request body
|
21
|
+
#
|
22
|
+
def self.build_tra service
|
23
|
+
@now = (Time.now) - 120
|
24
|
+
@from = @now.strftime('%FT%T%:z')
|
25
|
+
@to = (@now + ((12*60*60))).strftime('%FT%T%:z')
|
26
|
+
@id = @now.strftime('%s')
|
27
|
+
tra = <<-EOF
|
28
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
29
|
+
<loginTicketRequest version="1.0">
|
30
|
+
<header>
|
31
|
+
<uniqueId>#{ @id }</uniqueId>
|
32
|
+
<generationTime>#{ @from }</generationTime>
|
33
|
+
<expirationTime>#{ @to }</expirationTime>
|
34
|
+
</header>
|
35
|
+
<service>#{service}</service>
|
36
|
+
</loginTicketRequest>
|
37
|
+
EOF
|
38
|
+
return tra
|
39
|
+
end
|
40
|
+
|
41
|
+
# Builds the CMS
|
42
|
+
# @return [String] cms
|
43
|
+
#
|
44
|
+
def self.build_cms(tra)
|
45
|
+
cms = `echo '#{ tra }' |
|
46
|
+
#{ Afip.openssl_bin } cms -sign -in /dev/stdin -signer #{ Afip.cert } -inkey #{ Afip.pkey } -nodetach \
|
47
|
+
-outform der |
|
48
|
+
#{ Afip.openssl_bin } base64 -e`
|
49
|
+
return cms
|
50
|
+
end
|
51
|
+
|
52
|
+
# Builds the CMS request to log in to the server
|
53
|
+
# @return [String] the cms body
|
54
|
+
#
|
55
|
+
def self.build_request(cms)
|
56
|
+
request = <<-XML
|
57
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
58
|
+
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://wsaa.view.sua.dvadac.desein.afip.gov">
|
59
|
+
<SOAP-ENV:Body>
|
60
|
+
<ns1:loginCms>
|
61
|
+
<ns1:in0>
|
62
|
+
#{ cms }
|
63
|
+
</ns1:in0>
|
64
|
+
</ns1:loginCms>
|
65
|
+
</SOAP-ENV:Body>
|
66
|
+
</SOAP-ENV:Envelope>
|
67
|
+
XML
|
68
|
+
return request
|
69
|
+
end
|
70
|
+
|
71
|
+
# Calls the WSAA with the request built by build_request
|
72
|
+
# @return [Array] with the token and signature
|
73
|
+
#
|
74
|
+
def self.call_wsaa(req)
|
75
|
+
response = `echo '#{ req }' | curl -k -s -H 'Content-Type: application/soap+xml; action=""' -d @- #{ Afip::AuthData.wsaa_url }`
|
76
|
+
pp response
|
77
|
+
response = CGI::unescapeHTML(response)
|
78
|
+
token = response.scan(/\<token\>(.+)\<\/token\>/).first.first
|
79
|
+
sign = response.scan(/\<sign\>(.+)\<\/sign\>/).first.first
|
80
|
+
return [token, sign]
|
81
|
+
end
|
82
|
+
|
83
|
+
# Writes the token and signature to a YAML file in the /tmp directory
|
84
|
+
#
|
85
|
+
def self.write_yaml(certs)
|
86
|
+
yml = <<-YML
|
87
|
+
token: #{certs[0]}
|
88
|
+
sign: #{certs[1]}
|
89
|
+
YML
|
90
|
+
`echo '#{ yml }' > /tmp/#{Afip::AuthData.environment.to_s}_Afip_#{ Afip.cuit }_#{ Time.new.strftime('%Y_%m_%d') }.yml`
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
data/lib/Afip.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require "Afip/version"
|
2
|
+
require "bundler/setup"
|
3
|
+
require "Afip/constants"
|
4
|
+
require "savon"
|
5
|
+
require "Afip/core_ext/float"
|
6
|
+
require "Afip/core_ext/hash"
|
7
|
+
require "Afip/core_ext/string"
|
8
|
+
|
9
|
+
require 'net/http'
|
10
|
+
require 'net/https'
|
11
|
+
|
12
|
+
#require 'net/http'
|
13
|
+
require 'net/https'
|
14
|
+
module Afip
|
15
|
+
|
16
|
+
class NullOrInvalidAttribute < StandardError; end
|
17
|
+
|
18
|
+
def self.root
|
19
|
+
File.expand_path '../..', __FILE__
|
20
|
+
end
|
21
|
+
|
22
|
+
autoload :Authorizer, "Afip/authorizer"
|
23
|
+
autoload :AuthData, "Afip/auth_data"
|
24
|
+
autoload :Padron, "Afip/padron"
|
25
|
+
autoload :Wsaa, "Afip/wsaa"
|
26
|
+
autoload :Bill, "Afip/bill"
|
27
|
+
autoload :CTG, "Afip/ctg"
|
28
|
+
|
29
|
+
|
30
|
+
class << self
|
31
|
+
mattr_accessor :cuit, :pkey, :cert, :environment, :openssl_bin,
|
32
|
+
:default_concepto, :default_documento, :default_moneda, :own_iva_cond, :service_url, :auth_url
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.setup(&block)
|
36
|
+
yield self
|
37
|
+
end
|
38
|
+
|
39
|
+
extend self
|
40
|
+
|
41
|
+
def auth_hash(service = "wsfe")
|
42
|
+
case service
|
43
|
+
when "wsfe"
|
44
|
+
{ 'Token' => Afip::TOKEN, 'Sign' => Afip::SIGN, 'Cuit' => Afip.cuit }
|
45
|
+
when "ws_sr_padron_a4"
|
46
|
+
{ 'token' => Afip::TOKEN, 'sign' => Afip::SIGN, 'cuitRepresentado' => Afip.cuit }
|
47
|
+
when "wsctg"
|
48
|
+
{ 'token' => Afip::TOKEN, 'sign' => Afip::SIGN, 'cuitRepresentado' => Afip.cuit }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def log?
|
53
|
+
Afip.verbose || ENV["VERBOSE"]
|
54
|
+
end
|
55
|
+
|
56
|
+
def deleteToken
|
57
|
+
AuthData.deleteToken
|
58
|
+
end
|
59
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: Afip
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.
|
4
|
+
version: 0.8.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Facundo A. Díaz Martínez
|
@@ -72,7 +72,30 @@ email:
|
|
72
72
|
executables: []
|
73
73
|
extensions: []
|
74
74
|
extra_rdoc_files: []
|
75
|
-
files:
|
75
|
+
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- Afip-0.8.3.gem
|
78
|
+
- Afip-0.8.4.gem
|
79
|
+
- Afip.gemspec
|
80
|
+
- Gemfile
|
81
|
+
- Gemfile.lock
|
82
|
+
- LICENSE.txt
|
83
|
+
- README.md
|
84
|
+
- Rakefile
|
85
|
+
- bin/console
|
86
|
+
- bin/setup
|
87
|
+
- lib/Afip.rb
|
88
|
+
- lib/Afip/auth_data.rb
|
89
|
+
- lib/Afip/authorizer.rb
|
90
|
+
- lib/Afip/bill.rb
|
91
|
+
- lib/Afip/constants.rb
|
92
|
+
- lib/Afip/core_ext/float.rb
|
93
|
+
- lib/Afip/core_ext/hash.rb
|
94
|
+
- lib/Afip/core_ext/string.rb
|
95
|
+
- lib/Afip/ctg.rb
|
96
|
+
- lib/Afip/padron.rb
|
97
|
+
- lib/Afip/version.rb
|
98
|
+
- lib/Afip/wsaa.rb
|
76
99
|
homepage: https://www.desideral.com
|
77
100
|
licenses:
|
78
101
|
- MIT
|