Afip 0.1.1 → 0.1.2
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.
- 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
|