Afip 1.4.7 → 1.4.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45fffc2182c70856fbf8f8c12facd1bd5f8bf0653bbd319f24a87cab99da787e
4
- data.tar.gz: f38fe48699d8ab2c5c790fbb723d0e04c7f44fa1050203605f8171875ccbc82c
3
+ metadata.gz: 8f2b65711ad19aaedc0e7c47d8ae3de32243566256163e5a3e91df1e79c0ebab
4
+ data.tar.gz: 46944b5fa828c6355251b17787d411b49e6eaa35cfe0517bd967d4615e6ab687
5
5
  SHA512:
6
- metadata.gz: b94a4851e7ce54f99cc2ee98af7a59401cf67236b6b0c27b2317d63c602ce2d38f76362125fdb2015196288f756c8b882640c50b44a529dfded54496b2b5e0b0
7
- data.tar.gz: e48f2867900d2bf664ee1ac067bb8393738b5f67af95e48c05ca26fbdd3ec8dff3da1f31b7905a70d20519e428a7eb25914f7abd2398359e0cb9abc40b4550db
6
+ metadata.gz: f81f51171d8b9adaa4d83952899c11f136fac0435961d26aea64a107d23b74142cc3befb1b5fcff03f50e36d6ccd603a8d8d02bfdaf6520c8377534c15435bb0
7
+ data.tar.gz: cff67aa184aff54ec64a77be0d077bba5355c19dae43ee9449f7cfc0c985c47650580c5f7c78d8fda20a4443a929daceaf475e53c899344c05ffcc554014acf5
data/Afip-1.2.1.gem ADDED
Binary file
data/Afip-1.4.gem ADDED
Binary file
data/Afip.gemspec ADDED
@@ -0,0 +1,41 @@
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 = "http://litecode.com.ar/"
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_dependency "nokogiri", ">= 1.10.4"
39
+ spec.add_development_dependency "bundler"
40
+ spec.add_development_dependency "rake"
41
+ end
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in Afip.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,49 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ Afip (1.4.7)
5
+ httpi
6
+ nokogiri (>= 1.10.4)
7
+ savon
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ akami (1.3.1)
13
+ gyoku (>= 0.4.0)
14
+ nokogiri
15
+ builder (3.2.4)
16
+ gyoku (1.3.1)
17
+ builder (>= 2.1.2)
18
+ httpi (2.4.4)
19
+ rack
20
+ socksify
21
+ mini_portile2 (2.4.0)
22
+ nokogiri (1.10.9)
23
+ mini_portile2 (~> 2.4.0)
24
+ nori (2.6.0)
25
+ rack (2.2.2)
26
+ rake (10.5.0)
27
+ savon (2.12.0)
28
+ akami (~> 1.2)
29
+ builder (>= 2.1.2)
30
+ gyoku (~> 1.2)
31
+ httpi (~> 2.3)
32
+ nokogiri (>= 1.8.1)
33
+ nori (~> 2.4)
34
+ wasabi (~> 3.4)
35
+ socksify (1.7.1)
36
+ wasabi (3.5.0)
37
+ httpi (~> 2.0)
38
+ nokogiri (>= 1.4.2)
39
+
40
+ PLATFORMS
41
+ ruby
42
+
43
+ DEPENDENCIES
44
+ Afip!
45
+ bundler
46
+ rake
47
+
48
+ BUNDLED WITH
49
+ 2.1.4
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
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
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,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
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
@@ -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
+ "/tmp/#{environment.to_s}_Afip_#{ Afip.cuit }_#{ Time.new.strftime('%Y_%m_%d') }.yml"
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,10 @@
1
+ module Afip
2
+ class Authorizer
3
+ attr_reader :pkey, :cert
4
+
5
+ def initialize
6
+ @pkey = Afip.pkey
7
+ @cert = Afip.cert
8
+ end
9
+ end
10
+ end
data/lib/Afip/bill.rb ADDED
@@ -0,0 +1,274 @@
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, :cbte_type,
5
+ :due_date, :fch_serv_desde, :fch_serv_hasta, :fch_emision,
6
+ :ivas, :sale_point, :cant_reg, :no_gravado, :gravado, :exento, :otros_imp, :tributos, :opcionales
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.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 = attrs[:cbte_type]
23
+ @cant_reg = attrs[:cant_reg] || 1
24
+ @no_gravado = attrs[:no_gravado] || 0.0
25
+ @gravado = attrs[:gravado] || 0.0
26
+ @exento = attrs[:exento] || 0.0
27
+ @otros_imp = attrs[:otros_imp] || 0.0
28
+ @total = net.to_f + iva_sum.to_f + exento.to_f + no_gravado.to_f + otros_imp.to_f
29
+ @tributos = attrs[:tributos] || []
30
+ @opcionales = attrs[:opcionales] || []
31
+ end
32
+
33
+ def self.get_ptos_vta
34
+ client = set_client
35
+ body = { "Auth" => Afip.auth_hash }
36
+ response = client.call(:fe_param_get_ptos_venta, message: body)
37
+ if response.body[:fe_param_get_ptos_venta_response][:fe_param_get_ptos_venta_result][:errors].nil?
38
+ if response.body[:fe_param_get_ptos_venta_response][:fe_param_get_ptos_venta_result][:result_get][:pto_venta].is_a?(Hash)
39
+ response.body[:fe_param_get_ptos_venta_response][:fe_param_get_ptos_venta_result][:result_get][:pto_venta][:nro]
40
+ else
41
+ response.body[:fe_param_get_ptos_venta_response][:fe_param_get_ptos_venta_result][:result_get][:pto_venta].map{|r| r[:nro]}
42
+ end
43
+ else
44
+ []
45
+ end
46
+ end
47
+
48
+ def self.get_tipos_cbte
49
+ client = set_client
50
+ body = { "Auth" => Afip.auth_hash }
51
+ response = client.call(:fe_param_get_tipos_cbte, message: body)
52
+ end
53
+
54
+ def self.get_tributos
55
+ client = set_client
56
+ body = { "Auth" => Afip.auth_hash }
57
+ response = client.call(:fe_param_get_tipos_tributos, message: body)
58
+ if response.body[:fe_param_get_tipos_tributos_response][:fe_param_get_tipos_tributos_result][:errors].nil?
59
+ response.body[:fe_param_get_tipos_tributos_response][:fe_param_get_tipos_tributos_result][:result_get][:tributo_tipo]
60
+ else
61
+ []
62
+ end
63
+ end
64
+
65
+ def self.set_client
66
+ Afip::AuthData.fetch
67
+ @client = Savon.client(
68
+ wsdl: Afip.service_url,
69
+ namespaces: { "xmlns" => "http://ar.gov.afip.dif.FEV1/" },
70
+ log_level: :debug,
71
+ ssl_cert_key_file: Afip.pkey,
72
+ ssl_cert_file: Afip.cert,
73
+ ssl_verify_mode: :none,
74
+ read_timeout: 90,
75
+ open_timeout: 90,
76
+ headers: { "Accept-Encoding" => "gzip, deflate", "Connection" => "Keep-Alive" }
77
+ )
78
+ end
79
+
80
+ def authorize
81
+ body = setup_bill
82
+ pp response = client.call(:fecae_solicitar, message: body)
83
+ setup_response(response.to_hash)
84
+ authorized?
85
+ end
86
+
87
+ def setup_bill
88
+ array_ivas = {}
89
+ array_ivas["AlicIva"] = ivas.map{ |i| { "Id" => i[0], "BaseImp" => i[1].round(4), "Importe" => i[2].round(4)} unless ["01", "02"].include?(i[0])}.compact
90
+
91
+ array_tributos = {}
92
+ array_tributos["Tributo"] = tributos.map{ |t|
93
+ if t[1].blank?
94
+ {
95
+ "Id" => t[0],
96
+ "BaseImp" => t[2].to_f.round(4),
97
+ "Alic" => t[3].to_f.round(4),
98
+ "Importe" => t[4].to_f.round(4)
99
+ }
100
+ else
101
+ {
102
+ "Id" => t[0],
103
+ "Desc" => t[1],
104
+ "BaseImp" => t[2].to_f.round(4),
105
+ "Alic" => t[3].to_f.round(4),
106
+ "Importe" => t[4].to_f.round(4)
107
+ }
108
+ end
109
+ }
110
+
111
+ array_opcionales = {}
112
+ array_opcionales["Opcional"] = opcionales.map{ |t|
113
+ {
114
+ "Id" => t[:id],
115
+ "Valor" => t[:valor]
116
+ }
117
+ }
118
+
119
+
120
+ fecaereq = {
121
+ "FeCAEReq" => {
122
+ "FeCabReq" => Afip::Bill.header(cant_reg, cbte_type, sale_point),
123
+ "FeDetReq" => {
124
+ "FECAEDetRequest" => {
125
+ "Concepto" => Afip::CONCEPTOS[concepto],
126
+ "DocTipo" => Afip::DOCUMENTOS[documento],
127
+ "DocNro" => doc_num,
128
+ "CbteFch" => fecha_emision.strftime('%Y%m%d'),
129
+ "ImpTotConc" => no_gravado,
130
+ "ImpNeto" => net.to_f,
131
+ "MonId" => Afip::MONEDAS[moneda][:codigo],
132
+ "MonCotiz" => exchange_rate,
133
+ "ImpOpEx" => exento,
134
+ "ImpTotal" => (Afip.own_iva_cond == :responsable_monotributo ? net : total).to_f.round(4),
135
+ "CbteDesde" => next_bill_number,
136
+ "CbteHasta" => next_bill_number
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ detail = fecaereq["FeCAEReq"]["FeDetReq"]["FECAEDetRequest"]
143
+
144
+ if (Afip.own_iva_cond == :responsable_inscripto)
145
+ detail["ImpIVA"] = iva_sum
146
+ detail["Iva"] = array_ivas unless iva_sum == 0
147
+ end
148
+
149
+ if !tributos.empty?
150
+ detail["ImpTrib"] = otros_imp
151
+ detail["Tributos"] = array_tributos
152
+ end
153
+
154
+ if !opcionales.empty?
155
+ detail["Opcionales"] = array_opcionales
156
+ end
157
+
158
+ unless concepto == "Productos" # En "Productos" ("01"), si se mandan estos parámetros la afip rechaza.
159
+ detail.merge!({"FchServDesde" => fch_serv_desde.strftime("%Y%m%d"),
160
+ "FchServHasta" => fch_serv_hasta.strftime("%Y%m%d"),
161
+ "FchVtoPago" => due_date.strftime("%Y%m%d")})
162
+ end
163
+
164
+ if ["201", "202", "203", "206", "207", "208", "211", "212", "213"].include?(cbte_type)
165
+ detail.merge!({"FchVtoPago" => due_date.strftime("%Y%m%d")})
166
+ end
167
+
168
+ body.merge!(fecaereq)
169
+ pp body
170
+ end
171
+
172
+ def self.header(cant_reg, cbte_type, sale_point)
173
+ {"CantReg" => cant_reg.to_s, "CbteTipo" => cbte_type, "PtoVta" => sale_point}
174
+ end
175
+
176
+ def exchange_rate
177
+ return 1 if moneda == :peso
178
+ response = client.call :fe_param_get_cotizacion do
179
+ message = body.merge!({"MonId" => Afip::MONEDAS[moneda][:codigo]})
180
+ end
181
+ response.to_hash[:fe_param_get_cotizacion_response][:fe_param_get_cotizacion_result][:result_get][:mon_cotiz].to_f
182
+ end
183
+
184
+ def iva_sum
185
+ iva_sum = 0.0
186
+ self.ivas.each{ |i|
187
+ iva_sum += i[2]
188
+ }
189
+ return iva_sum.round(4)
190
+ end
191
+
192
+ def next_bill_number
193
+ var = {"Auth" => Afip.auth_hash, "PtoVta" => sale_point, "CbteTipo" => cbte_type}
194
+ resp = client.call :fe_comp_ultimo_autorizado do
195
+ message(var)
196
+ end
197
+
198
+ resp.to_hash[:fe_comp_ultimo_autorizado_response][:fe_comp_ultimo_autorizado_result][:cbte_nro].to_i + 1
199
+ end
200
+
201
+ def setup_response(response)
202
+ # TODO: turn this into an all-purpose Response class
203
+ pp response
204
+ result = response[:fecae_solicitar_response][:fecae_solicitar_result]
205
+
206
+ if not result[:fe_det_resp] or not result[:fe_cab_resp] then
207
+ # Si no obtuvo respuesta ni cabecera ni detalle, evito hacer '[]' sobre algo indefinido.
208
+ # Ejemplo: Error con el token-sign de WSAA
209
+ keys, values = {
210
+ :errores => result[:errors],
211
+ :header_result => {:resultado => "X" },
212
+ :observaciones => nil
213
+ }.to_a.transpose
214
+ @response = (defined?(Struct::ResponseMal) ? Struct::ResponseMal : Struct.new("ResponseMal", *keys)).new(*values)
215
+ return
216
+ end
217
+
218
+ response_header = result[:fe_cab_resp]
219
+ response_detail = result[:fe_det_resp][:fecae_det_response]
220
+
221
+ request_header = body["FeCAEReq"]["FeCabReq"].underscore_keys.symbolize_keys
222
+ request_detail = body["FeCAEReq"]["FeDetReq"]["FECAEDetRequest"].underscore_keys.symbolize_keys
223
+
224
+ # Esto no funciona desde que se soportan múltiples alícuotas de iva simultáneas
225
+ # FIX ? TO-DO
226
+ # iva = request_detail.delete(:iva)["AlicIva"].underscore_keys.symbolize_keys
227
+ # request_detail.merge!(iva)
228
+
229
+ if result[:errors] then
230
+ response_detail.merge!( result[:errors] )
231
+ end
232
+
233
+ response_hash = {
234
+ :header_result => response_header.delete(:resultado),
235
+ :authorized_on => response_header.delete(:fch_proceso),
236
+ :detail_result => response_detail.delete(:resultado),
237
+ :cae_due_date => response_detail.delete(:cae_fch_vto),
238
+ :cae => response_detail.delete(:cae),
239
+ :iva_id => request_detail.delete(:id),
240
+ :iva_importe => request_detail.delete(:importe),
241
+ :moneda => request_detail.delete(:mon_id),
242
+ :cotizacion => request_detail.delete(:mon_cotiz),
243
+ :iva_base_imp => request_detail.delete(:base_imp),
244
+ :doc_num => request_detail.delete(:doc_nro),
245
+ :observaciones => response_detail.delete(:observaciones),
246
+ :ivas => response_detail.delete(:iva),
247
+ :tributos => response_detail.delete(:tributos),
248
+ :errores => response_detail.delete(:err)
249
+ }.merge!(request_header).merge!(request_detail)
250
+
251
+ keys, values = response_hash.to_a.transpose
252
+ pp @response = Struct.new("Response", *keys).new(*values)
253
+ end
254
+
255
+ def authorized?
256
+ !response.nil? && response.header_result == "A" && response.detail_result == "A"
257
+ end
258
+
259
+ def self.comp_consultar(cbte_tipo, cbte_nro, pto_venta)
260
+ client = set_client
261
+
262
+ body = {
263
+ "Auth" => Afip.auth_hash,
264
+ "FeCompConsReq" => {
265
+ "CbteTipo" => cbte_tipo,
266
+ "CbteNro" => cbte_nro,
267
+ "PtoVta" => pto_venta
268
+ }
269
+ }
270
+ response = client.call(:fe_comp_consultar, message: body)
271
+ return response
272
+ end
273
+ end
274
+ end