Afip 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.DS_Store +0 -0
- data/Afip-0.1.0.gem +0 -0
- data/Gemfile.lock +20 -0
- data/lib/.DS_Store +0 -0
- data/lib/Afip/auth_data.rb +69 -0
- data/lib/Afip/authorizer.rb +10 -0
- data/lib/Afip/bill.rb +178 -0
- data/lib/Afip/constants.rb +63 -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/padron.rb +181 -0
- data/lib/Afip/version.rb +1 -1
- data/lib/Afip/wsaa.rb +94 -0
- data/lib/Afip.rb +1 -1
- metadata +14 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3940933ea4642917aef2e693aa7c170052d19d4b
|
4
|
+
data.tar.gz: 9766eff2f7d9e62c270d977f24e1b5c3967bf45d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cbda0d40344b3637967d267cb3272a2c7a0e2ec5e074d471e45605da7a8dabdd5aabf7a781a34a99d7e5fdc014c3010ab8d835bac62a41179b4aec8b4dcc8860
|
7
|
+
data.tar.gz: e203ce591b376f75f54fa62ea5bc83e22a8e7aeec9ddf4916db2a8e51dad0ea7f935f091c4bac838bde9665af0777669c2828960edfa373cb0d19c028a9bf270
|
data/.DS_Store
ADDED
Binary file
|
data/Afip-0.1.0.gem
ADDED
Binary file
|
data/Gemfile.lock
ADDED
data/lib/.DS_Store
ADDED
Binary file
|
@@ -0,0 +1,69 @@
|
|
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
|
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
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns the right wsaa url for the specific environment
|
47
|
+
# @return [String]
|
48
|
+
#
|
49
|
+
def wsaa_url
|
50
|
+
pp Afip::URLS[environment][:wsaa]
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns the right wsfe url for the specific environment
|
54
|
+
# @return [String]
|
55
|
+
#
|
56
|
+
def wsfe_url
|
57
|
+
raise 'Environment not sent to either :test or :production' unless Afip::URLS.keys.include? environment
|
58
|
+
Afip::URLS[environment][:wsfe]
|
59
|
+
end
|
60
|
+
|
61
|
+
# Creates the data file name for a cuit number and the current day
|
62
|
+
# @return [String]
|
63
|
+
#
|
64
|
+
def todays_data_file_name
|
65
|
+
@todays_data_file ||= "/tmp/#{environment.to_s}_Afip_#{ Afip.cuit }_#{ Time.new.strftime('%Y_%m_%d') }.yml"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/Afip/bill.rb
ADDED
@@ -0,0 +1,178 @@
|
|
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
|
+
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
|
+
@cbte_type = Afip::BILL_TYPE[Afip.own_iva_cond][iva_cond] || raise(NullOrInvalidAttribute.new, "Please choose a valid document type.")
|
20
|
+
@total = net.zero? ? 0 : net + iva_sum
|
21
|
+
end
|
22
|
+
|
23
|
+
def set_client
|
24
|
+
Afip::AuthData.fetch
|
25
|
+
@client = Savon.client(
|
26
|
+
wsdl: Afip.service_url,
|
27
|
+
namespaces: { "xmlns" => "http://ar.gov.afip.dif.FEV1/" },
|
28
|
+
log_level: :debug,
|
29
|
+
ssl_cert_key_file: Afip.pkey,
|
30
|
+
ssl_cert_file: Afip.cert,
|
31
|
+
ssl_verify_mode: :none,
|
32
|
+
read_timeout: 90,
|
33
|
+
open_timeout: 90,
|
34
|
+
headers: { "Accept-Encoding" => "gzip, deflate", "Connection" => "Keep-Alive" }
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
def authorize
|
39
|
+
body = setup_bill
|
40
|
+
response = client.call(:fecae_solicitar, message: body)
|
41
|
+
setup_response(response.to_hash)
|
42
|
+
authorized?
|
43
|
+
end
|
44
|
+
|
45
|
+
def setup_bill
|
46
|
+
array_ivas = Array.new
|
47
|
+
ivas.each{ |i|
|
48
|
+
array_ivas << {
|
49
|
+
"Id" => Afip::ALIC_IVA[ i[0] ][0],
|
50
|
+
"BaseImp" => i[1] ,
|
51
|
+
"Importe" => i[2] }
|
52
|
+
}
|
53
|
+
|
54
|
+
fecaereq = {
|
55
|
+
"FeCAEReq" => {
|
56
|
+
"FeCabReq" => Afip::Bill.header(cbte_type, sale_point),
|
57
|
+
"FeDetReq" => {
|
58
|
+
"FECAEDetRequest" => {
|
59
|
+
"Concepto" => Afip::CONCEPTOS[concepto],
|
60
|
+
"DocTipo" => Afip::DOCUMENTOS[documento],
|
61
|
+
"DocNro" => doc_num,
|
62
|
+
"CbteFch" => fecha_emision.strftime('%Y%m%d'),
|
63
|
+
"ImpTotConc" => 0.00,
|
64
|
+
"ImpNeto" => net.to_f,
|
65
|
+
"MonId" => Afip::MONEDAS[moneda][:codigo],
|
66
|
+
"MonCotiz" => exchange_rate,
|
67
|
+
"ImpOpEx" => 0.00,
|
68
|
+
"ImpTrib" => 0.00,
|
69
|
+
"ImpTotal" => (Afip.own_iva_cond == :responsable_monotributo ? total : net).to_f.round(2),
|
70
|
+
"CbteDesde" => next_bill_number,
|
71
|
+
"CbteHasta" => next_bill_number
|
72
|
+
}
|
73
|
+
}
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
detail = fecaereq["FeCAEReq"]["FeDetReq"]["FECAEDetRequest"]
|
78
|
+
|
79
|
+
if (Afip.own_iva_cond == :responsable_monotributo)
|
80
|
+
detail["ImpIVA"] = iva_sum
|
81
|
+
detail["Iva"] = { "AlicIva" => array_ivas }
|
82
|
+
end
|
83
|
+
|
84
|
+
unless concepto == "Productos" # En "Productos" ("01"), si se mandan estos parámetros la afip rechaza.
|
85
|
+
detail.merge!({"FchServDesde" => fch_serv_desde,
|
86
|
+
"FchServHasta" => fch_serv_hasta,
|
87
|
+
"FchVtoPago" => due_date})
|
88
|
+
end
|
89
|
+
|
90
|
+
body.merge!(fecaereq)
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.header(cbte_type, sale_point)
|
94
|
+
{"CantReg" => "1", "CbteTipo" => cbte_type, "PtoVta" => sale_point}
|
95
|
+
end
|
96
|
+
|
97
|
+
def exchange_rate
|
98
|
+
return 1 if moneda == :peso
|
99
|
+
response = client.call :fe_param_get_cotizacion do
|
100
|
+
message = body.merge!({"MonId" => Afip::MONEDAS[moneda][:codigo]})
|
101
|
+
end
|
102
|
+
response.to_hash[:fe_param_get_cotizacion_response][:fe_param_get_cotizacion_result][:result_get][:mon_cotiz].to_f
|
103
|
+
end
|
104
|
+
|
105
|
+
def iva_sum
|
106
|
+
iva_sum = 0.0
|
107
|
+
self.ivas.each{ |i|
|
108
|
+
iva_sum += i[1] * Afip::ALIC_IVA[ i[0] ][1]
|
109
|
+
}
|
110
|
+
return iva_sum.round(2)
|
111
|
+
end
|
112
|
+
|
113
|
+
def next_bill_number
|
114
|
+
var = {"Auth" => Afip.auth_hash, "PtoVta" => sale_point, "CbteTipo" => cbte_type}
|
115
|
+
resp = client.call :fe_comp_ultimo_autorizado do
|
116
|
+
message(var)
|
117
|
+
end
|
118
|
+
|
119
|
+
resp.to_hash[:fe_comp_ultimo_autorizado_response][:fe_comp_ultimo_autorizado_result][:cbte_nro].to_i + 1
|
120
|
+
end
|
121
|
+
|
122
|
+
def setup_response(response)
|
123
|
+
# TODO: turn this into an all-purpose Response class
|
124
|
+
|
125
|
+
result = response[:fecae_solicitar_response][:fecae_solicitar_result]
|
126
|
+
|
127
|
+
if not result[:fe_det_resp] or not result[:fe_cab_resp] then
|
128
|
+
# Si no obtuvo respuesta ni cabecera ni detalle, evito hacer '[]' sobre algo indefinido.
|
129
|
+
# Ejemplo: Error con el token-sign de WSAA
|
130
|
+
keys, values = {
|
131
|
+
:errores => result[:errors],
|
132
|
+
:header_result => {:resultado => "X" },
|
133
|
+
:observaciones => nil
|
134
|
+
}.to_a.transpose
|
135
|
+
@response = (defined?(Struct::ResponseMal) ? Struct::ResponseMal : Struct.new("ResponseMal", *keys)).new(*values)
|
136
|
+
return
|
137
|
+
end
|
138
|
+
|
139
|
+
response_header = result[:fe_cab_resp]
|
140
|
+
response_detail = result[:fe_det_resp][:fecae_det_response]
|
141
|
+
|
142
|
+
request_header = body["FeCAEReq"]["FeCabReq"].underscore_keys.symbolize_keys
|
143
|
+
request_detail = body["FeCAEReq"]["FeDetReq"]["FECAEDetRequest"].underscore_keys.symbolize_keys
|
144
|
+
|
145
|
+
# Esto no funciona desde que se soportan múltiples alícuotas de iva simultáneas
|
146
|
+
# FIX ? TO-DO
|
147
|
+
# iva = request_detail.delete(:iva)["AlicIva"].underscore_keys.symbolize_keys
|
148
|
+
# request_detail.merge!(iva)
|
149
|
+
|
150
|
+
if result[:errors] then
|
151
|
+
response_detail.merge!( result[:errors] )
|
152
|
+
end
|
153
|
+
|
154
|
+
response_hash = {
|
155
|
+
:header_result => response_header.delete(:resultado),
|
156
|
+
:authorized_on => response_header.delete(:fch_proceso),
|
157
|
+
:detail_result => response_detail.delete(:resultado),
|
158
|
+
:cae_due_date => response_detail.delete(:cae_fch_vto),
|
159
|
+
:cae => response_detail.delete(:cae),
|
160
|
+
:iva_id => request_detail.delete(:id),
|
161
|
+
:iva_importe => request_detail.delete(:importe),
|
162
|
+
:moneda => request_detail.delete(:mon_id),
|
163
|
+
:cotizacion => request_detail.delete(:mon_cotiz),
|
164
|
+
:iva_base_imp => request_detail.delete(:base_imp),
|
165
|
+
:doc_num => request_detail.delete(:doc_nro),
|
166
|
+
:observaciones => response_detail.delete(:observaciones),
|
167
|
+
:errores => response_detail.delete(:err)
|
168
|
+
}.merge!(request_header).merge!(request_detail)
|
169
|
+
|
170
|
+
keys, values = response_hash.to_a.transpose
|
171
|
+
@response = (defined?(Struct::Response) ? Struct::Response : Struct.new("Response", *keys)).new(*values)
|
172
|
+
end
|
173
|
+
|
174
|
+
def authorized?
|
175
|
+
!response.nil? && response.header_result == "A" && response.detail_result == "A"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,63 @@
|
|
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
|
+
BILL_TYPE = {
|
29
|
+
:responsable_inscripto => {
|
30
|
+
:responsable_inscripto => "01",
|
31
|
+
:consumidor_final => "06",
|
32
|
+
:exento => "06",
|
33
|
+
:responsable_monotributo => "06",
|
34
|
+
:nota_credito_a => "03",
|
35
|
+
:nota_credito_b => "08",
|
36
|
+
:nota_debito_a => "02",
|
37
|
+
:nota_debito_b => "07"
|
38
|
+
},
|
39
|
+
:responsable_monotributo => {
|
40
|
+
:responsable_inscripto => "11",
|
41
|
+
:consumidor_final => "11",
|
42
|
+
:exento => "11",
|
43
|
+
:responsable_monotributo => "11",
|
44
|
+
:nota_credito_c => "13",
|
45
|
+
:nota_debito_c => "12"
|
46
|
+
}
|
47
|
+
}
|
48
|
+
|
49
|
+
URLS =
|
50
|
+
{
|
51
|
+
:test => {
|
52
|
+
:wsaa => 'https://wsaahomo.afip.gov.ar/ws/services/LoginCms',
|
53
|
+
:padron => "https://awshomo.afip.gov.ar/sr-padron/webservices/personaServiceA5?WSDL",
|
54
|
+
:wsfe => 'https://wswhomo.afip.gov.ar/wsfev1/service.asmx?WSDL'
|
55
|
+
},
|
56
|
+
:production => {
|
57
|
+
:wsaa => 'https://wsaa.afip.gov.ar/ws/services/LoginCms',
|
58
|
+
:padron => "https://aws.afip.gov.ar/sr-padron/webservices/personaServiceA5?WSDL",
|
59
|
+
:wsfe => 'https://servicios1.afip.gov.ar/wsfev1/service.asmx'
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
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/padron.rb
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
module Afip
|
2
|
+
class Padron
|
3
|
+
attr_reader :client, :body, :fault_code, :data
|
4
|
+
attr_accessor :dni, :tipo
|
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 = "#{Rails.root}/afip/desideral_prod.crt"
|
11
|
+
Afip.pkey = "#{Rails.root}/afip/desideral.key"
|
12
|
+
Afip::AuthData.fetch("ws_sr_padron_a4")
|
13
|
+
|
14
|
+
@client = Savon.client(
|
15
|
+
ssl_cert_key_file: "#{Rails.root}/afip/desideral.key",
|
16
|
+
ssl_cert_file: "#{Rails.root}/afip/desideral_prod.crt",
|
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] || "F" #F femenino M masculino E empresa
|
25
|
+
@cuit = get_cuit
|
26
|
+
end
|
27
|
+
|
28
|
+
def get_persona
|
29
|
+
body = setup_body
|
30
|
+
|
31
|
+
pp response = client.call(:get_persona,message: body)
|
32
|
+
rescue Savon::SOAPFault => error
|
33
|
+
if !error.blank?
|
34
|
+
pp @fault_code = error.to_hash[:fault][:faultstring]
|
35
|
+
end
|
36
|
+
return response
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_data
|
40
|
+
@data = get_persona
|
41
|
+
if not fault_code
|
42
|
+
set_data
|
43
|
+
else
|
44
|
+
return nil
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
def set_data
|
50
|
+
pp data.body
|
51
|
+
if not data.body[:get_persona_response][:persona_return][:persona][:actividad].nil?
|
52
|
+
{
|
53
|
+
:last_name => data.body[:get_persona_response][:persona_return][:persona][:apellido],
|
54
|
+
:first_name => data.body[:get_persona_response][:persona_return][:persona][:nombre],
|
55
|
+
:cuit => data.body[:get_persona_response][:persona_return][:persona][:id_persona],
|
56
|
+
:cp => data.body[:get_persona_response][:persona_return][:persona][:domicilio].last[:cod_postal],
|
57
|
+
:address => data.body[:get_persona_response][:persona_return][:persona][:domicilio].last[:direccion],
|
58
|
+
:city_id => data.body[:get_persona_response][:persona_return][:persona][:domicilio].last[:id_provincia],
|
59
|
+
:city => PROVINCIAS[data.body[:get_persona_response][:persona_return][:persona][:domicilio].last[:id_provincia]],
|
60
|
+
:locality => data.body[:get_persona_response][:persona_return][:persona][:domicilio].last[:localidad],
|
61
|
+
:birthday => data.body[:get_persona_response][:persona_return][:persona][:fecha_nacimiento].to_date
|
62
|
+
}
|
63
|
+
else
|
64
|
+
{
|
65
|
+
:last_name => Padron.divide_name(data.body[:get_persona_response][:persona_return][:persona][:apellido])[0],
|
66
|
+
:first_name => Padron.divide_name(data.body[:get_persona_response][:persona_return][:persona][:apellido])[1],
|
67
|
+
:cuit => data.body[:get_persona_response][:persona_return][:persona][:id_persona],
|
68
|
+
:cp => data.body[:get_persona_response][:persona_return][:persona][:domicilio][:cod_postal],
|
69
|
+
:address => data.body[:get_persona_response][:persona_return][:persona][:domicilio][:direccion],
|
70
|
+
:city_id => data.body[:get_persona_response][:persona_return][:persona][:domicilio][:id_provincia],
|
71
|
+
:city => PROVINCIAS[data.body[:get_persona_response][:persona_return][:persona][:domicilio][:id_provincia]],
|
72
|
+
:locality => data.body[:get_persona_response][:persona_return][:persona][:domicilio][:localidad],
|
73
|
+
:birthday => data.body[:get_persona_response][:persona_return][:persona][:fecha_nacimiento].to_date
|
74
|
+
}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.divide_name(full_name)
|
79
|
+
full_name = full_name.strip.split(/\s+/)
|
80
|
+
last_name = ''
|
81
|
+
last = (full_name.count / 2) - 1
|
82
|
+
(0..last).each do |i|
|
83
|
+
if i != last
|
84
|
+
last_name += full_name[i] + ' '
|
85
|
+
else
|
86
|
+
last_name += full_name[i]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
full_name = full_name - (last_name.strip.split(/\s+/))
|
90
|
+
first_name = full_name.join(", ").gsub(",","").split.map(&:capitalize).join(' ')
|
91
|
+
last_name = last_name.split.map(&:capitalize).join(' ')
|
92
|
+
return [last_name, first_name]
|
93
|
+
end
|
94
|
+
|
95
|
+
def get_cuit
|
96
|
+
if dni.length == 11
|
97
|
+
@cuit = @dni
|
98
|
+
else
|
99
|
+
@cuit = calculate_cuit
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def calculate_cuit
|
104
|
+
multiplicador = "2345672345"
|
105
|
+
|
106
|
+
case tipo
|
107
|
+
when "F"
|
108
|
+
xy = 27
|
109
|
+
xy_dni = "27#{dni}"
|
110
|
+
when "M"
|
111
|
+
xy = 20
|
112
|
+
xy_dni = "20#{dni}"
|
113
|
+
end
|
114
|
+
verificador = 0
|
115
|
+
(0..9).each do |i|
|
116
|
+
verificador += (xy_dni.reverse[i].to_i * multiplicador[i].to_i)
|
117
|
+
end
|
118
|
+
pp verificador
|
119
|
+
z = verificador - (verificador / 11 * 11)
|
120
|
+
|
121
|
+
case z
|
122
|
+
when 0
|
123
|
+
z = 0
|
124
|
+
when 1
|
125
|
+
if tipo == "M"
|
126
|
+
z = 9
|
127
|
+
xy = 23
|
128
|
+
elsif tipo == "F"
|
129
|
+
z = 4
|
130
|
+
xy = 23
|
131
|
+
else
|
132
|
+
z = 11 - z
|
133
|
+
end
|
134
|
+
else
|
135
|
+
z = 11 - z
|
136
|
+
end
|
137
|
+
|
138
|
+
return "#{xy}#{dni}#{z}"
|
139
|
+
end
|
140
|
+
|
141
|
+
def setup_body
|
142
|
+
body = {
|
143
|
+
'token' => Afip::TOKEN,
|
144
|
+
'sign' => Afip::SIGN,
|
145
|
+
'cuitRepresentada' => Afip.cuit,
|
146
|
+
'idPersona' => @cuit.to_s
|
147
|
+
}
|
148
|
+
end
|
149
|
+
|
150
|
+
PROVINCIAS = {
|
151
|
+
"0" => 'CIUDAD AUTONOMA BUENOS AIRES',
|
152
|
+
"1" => 'BUENOS AIRES',
|
153
|
+
"2" => 'CATAMARCA',
|
154
|
+
"3" => 'CORDOBA',
|
155
|
+
"4" => 'CORRIENTES',
|
156
|
+
"5" => 'ENTRE RIOS',
|
157
|
+
"6" => 'JUJUY',
|
158
|
+
"7" => 'MENDOZA',
|
159
|
+
"8" => 'LA RIOJA',
|
160
|
+
"9" => 'SALTA',
|
161
|
+
"10" => 'SAN JUAN',
|
162
|
+
"11" => 'SAN LUIS',
|
163
|
+
"12" => 'SANTA FE',
|
164
|
+
"13" => 'SANTIAGO DEL ESTERO',
|
165
|
+
"14" => 'TUCUMAN',
|
166
|
+
"16" => 'CHACO',
|
167
|
+
"17" => 'CHUBUT',
|
168
|
+
"18" => 'FORMOSA',
|
169
|
+
"19" => 'MISIONES',
|
170
|
+
"20" => 'NEUQUEN',
|
171
|
+
"21" => 'LA PAMPA',
|
172
|
+
"22" => 'RIO NEGRO',
|
173
|
+
"23" => 'SANTA CRUZ',
|
174
|
+
"24" => 'TIERRA DEL FUEGO'
|
175
|
+
}
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
|
180
|
+
|
181
|
+
|
data/lib/Afip/version.rb
CHANGED
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
CHANGED
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.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Facundo A. Díaz Martínez
|
@@ -45,16 +45,29 @@ executables: []
|
|
45
45
|
extensions: []
|
46
46
|
extra_rdoc_files: []
|
47
47
|
files:
|
48
|
+
- ".DS_Store"
|
48
49
|
- ".gitignore"
|
50
|
+
- Afip-0.1.0.gem
|
49
51
|
- Afip.gemspec
|
50
52
|
- Gemfile
|
53
|
+
- Gemfile.lock
|
51
54
|
- LICENSE.txt
|
52
55
|
- README.md
|
53
56
|
- Rakefile
|
54
57
|
- bin/console
|
55
58
|
- bin/setup
|
59
|
+
- lib/.DS_Store
|
56
60
|
- lib/Afip.rb
|
61
|
+
- lib/Afip/auth_data.rb
|
62
|
+
- lib/Afip/authorizer.rb
|
63
|
+
- lib/Afip/bill.rb
|
64
|
+
- lib/Afip/constants.rb
|
65
|
+
- lib/Afip/core_ext/float.rb
|
66
|
+
- lib/Afip/core_ext/hash.rb
|
67
|
+
- lib/Afip/core_ext/string.rb
|
68
|
+
- lib/Afip/padron.rb
|
57
69
|
- lib/Afip/version.rb
|
70
|
+
- lib/Afip/wsaa.rb
|
58
71
|
homepage: https://www.desideral.com
|
59
72
|
licenses:
|
60
73
|
- MIT
|