Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
23ccaa4
Modificado path al ejecutable de openssl en wsaa-client.sh. Ahora dep…
ceneon Apr 7, 2011
f0cd76e
Dependencias agregadas
ceneon Apr 7, 2011
f50409b
Corregido rechazo de comprobantes de "Productos"
ceneon Apr 7, 2011
054e475
Agregada información de instalación de openssl
ceneon Apr 7, 2011
1f55ce6
Nota sobre incompatibilidad Windows
ceneon Apr 7, 2011
4b96d69
Agregué archivo de salida parametrizable
ceneon Apr 9, 2011
df363d2
Moví archivo de salida de WSAA a directorio tmp dentro del proyecto (…
ceneon Apr 9, 2011
55ebaad
Agregué soporte a distintos CUIT en la misma aplicación (Varios archi…
ceneon Apr 9, 2011
3e96c1e
Parsing de "Observaciones" del rechazo de pedido de WSFE
ceneon Apr 9, 2011
8de5e84
Parsing de "Errores" del rechazo de pedido de WSFE
ceneon Apr 9, 2011
b87ec0e
Agregué BILL_TYPE nota de débito A y B
ceneon Apr 9, 2011
17c76a9
Captura de error en el token de WSAA
ceneon Apr 11, 2011
d67de6f
Corregido error de redondeo en cálculo de IVA!
ceneon Apr 11, 2011
eca3ab1
Removido el IVA 21% Hardcodeado !!!!
ceneon Apr 11, 2011
0d1efd5
Corregido el typo del commit anterior
ceneon Apr 11, 2011
770cd75
Ídem
ceneon Apr 11, 2011
366c695
me.stupid
ceneon Apr 11, 2011
fbc39e7
Agregué fecha de emisión del comprobante parametrizable (Bill.fch_emi…
ceneon Apr 14, 2011
3aa9a3b
corrección menor en readme
ceneon Apr 14, 2011
5ed1303
Método nuevo "deleteToken"
ceneon May 3, 2011
d39c895
Método nuevo "deleteToken"
ceneon May 3, 2011
fa14530
Edited lib/bravo/bill.rb via GitHub
ceneon Jun 10, 2011
cc1b742
Ejecuta wsaa-client incluso si no tiene permisos de ejecución
ceneon Jan 19, 2012
b050358
Soporte para múltiples IVA
ceneon Jan 20, 2012
61e5a7e
cleanup
ceneon Jan 20, 2012
5398782
Calculo de iva_sum interno
ceneon Jan 20, 2012
7758446
fix en tests
ceneon Jan 20, 2012
80de1bc
Freeze savon version
ceneon Jan 15, 2013
e87c341
Freezed savon version in gemspec
ceneon Jan 15, 2013
ce7b22d
Debugging wsaa shit
ceneon Jun 30, 2015
a2d1987
Please debug all this :shit:
ceneon Jul 1, 2015
ff70d2e
:shit:
ceneon Jul 1, 2015
49d8b35
adding monotributo invoicing
nan-apps Dec 21, 2018
4a74584
precision fix
nan-apps Jan 4, 2019
aeff827
Merge branch 'master' into c-invoicing
nan-apps Jan 4, 2019
9d54b53
Merge pull request #1 from ceneon/c-invoicing
ceneon Jan 15, 2019
5540bb9
adding related invoice params for A and B notes. New afip requirements
nan-apps Sep 23, 2020
e0a50a2
[hotfix] adding C notes to constants, it was sending always C invoice…
nan-apps Sep 25, 2020
b0127e4
method to get token last modified date time
nan-apps Oct 3, 2020
b9b1967
[WIP] Search for invoices and such
Oct 26, 2020
27d9a56
🤬
Oct 26, 2020
cefcbcf
Updated to a newer savon gem
Oct 26, 2020
506ac35
Force savon to log for emergency reasons
Oct 29, 2020
8752d83
Made Savon log through Rails
Oct 29, 2020
92ebd32
Merge pull request #2 from ceneon/search-invoices
ceneon Oct 30, 2020
dd4b0fb
Make Savon log always
Oct 30, 2020
dac1c01
Hotfix for very old code that keeps failing
Oct 30, 2020
251f76f
getting authentication errors on WSAA requests and raising exception …
nan-apps Jan 5, 2021
22eda09
Merge remote-tracking branch 'origin/master'
nan-apps Jan 5, 2021
dd53dbb
Patched constant token/sign setter for multi-tenant environments
ceneon Jun 7, 2021
85f558c
2021 AFIP update: RIs now use A invoices for MOs (because reasons)
ceneon Oct 16, 2021
235e1f1
Use PeriodoAsoc when submitting NC/ND without CbteAsoc
ceneon Nov 10, 2023
3fdd9ac
User PeriodoAsoc on nc/nd only
ceneon Nov 11, 2023
5c7d42e
Added required CondicionIVAReceptorId field
ceneon Mar 17, 2025
c172708
Regenerate token file if empty because reasons
ceneon Jun 20, 2025
ea234b3
update afip deadline description
ceneon Jul 22, 2025
dcdb8fd
Merge branch 'compat2025-04'
ceneon Jul 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ source "http://rubygems.org"
platforms :ruby_19 do
gem "httpi", "0.7.9"
end
gem "savon"
gem "savon", "1.2.0"

group :development do
platforms :ruby_18 do
Expand Down
17 changes: 17 additions & 0 deletions README.textile
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,28 @@ o

en tu @Gemfile@

Nota: Para que funcione el proceso de autenticación WSAA, chequear si el openssl instalado tiene compilado el módulo "cms" :
<pre>openssl cms</pre>

Si no dice "Invalid command", estamos bien. Caso contrario, descargar openssl y compilarlo con:
<pre>./configure enable-cms
make
make install
</pre>

Nota 2: Ojo! Probablemente el openssl instalado originalmente sea el que se acceda por el path. Normalmente esto lo soluciona:

<pre>whereis openssl
mv SALIDA_DEL_WHEREIS SALIDA_DEL_WHEREIS_old
ln -s /usr/local/ssl/bin/openssl SALIDA_DEL_WHEREIS
</pre>

h2. Configuraci&oacute;n

Los servicios de AFIP requieren la utilizaci&oacute;n del Web Service de Autorizaci&oacute;n y Autenticaci&oacute;n ("wsaa readme":http://www.afip.gov.ar/ws/WSAA/README.txt)

Nota: El proceso de WSAA en Bravo está implementado con un script Bash. Esto es incompatible con un servidor Windows.

Luego de cumplidos los pasos indicados en el readme, basta con configurar Bravo con la ruta a los archivos:

<pre>
Expand Down
6 changes: 3 additions & 3 deletions bravo.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Gem::Specification.new do |s|
s.specification_version = 3

if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
s.add_runtime_dependency(%q<savon>, [">= 0"])
s.add_runtime_dependency(%q<savon>, ["= 1.2.0"])
s.add_development_dependency(%q<ruby-debug>, [">= 0"])
s.add_development_dependency(%q<ruby-debug-base19>, ["= 0.11.24"])
s.add_development_dependency(%q<ruby-debug19>, ["= 0.11.6"])
Expand All @@ -67,7 +67,7 @@ Gem::Specification.new do |s|
s.add_development_dependency(%q<jeweler>, ["~> 1.5.1"])
s.add_development_dependency(%q<rcov>, [">= 0"])
else
s.add_dependency(%q<savon>, [">= 0"])
s.add_dependency(%q<savon>, ["= 1.2.0"])
s.add_dependency(%q<ruby-debug>, [">= 0"])
s.add_dependency(%q<ruby-debug-base19>, ["= 0.11.24"])
s.add_dependency(%q<ruby-debug19>, ["= 0.11.6"])
Expand All @@ -77,7 +77,7 @@ Gem::Specification.new do |s|
s.add_dependency(%q<rcov>, [">= 0"])
end
else
s.add_dependency(%q<savon>, [">= 0"])
s.add_dependency(%q<savon>, ["= 1.2.0"])
s.add_dependency(%q<ruby-debug>, [">= 0"])
s.add_dependency(%q<ruby-debug-base19>, ["= 0.11.24"])
s.add_dependency(%q<ruby-debug19>, ["= 0.11.6"])
Expand Down
16 changes: 15 additions & 1 deletion lib/bravo.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
require "bravo/core_ext/float"
require "bravo/core_ext/hash"
require "bravo/core_ext/string"

require 'net/http'
require 'net/https'

module Bravo

class NullOrInvalidAttribute < StandardError; end
Expand All @@ -26,10 +30,20 @@ def auth_hash
def log?
Bravo.verbose || ENV["VERBOSE"]
end

def deleteToken
AuthData.deleteToken
end

def token_modified_at
AuthData.token_modified_at
end


end

Savon.configure do |config|
config.log = Bravo.log?
config.logger = Rails.logger
config.log = true # Bravo.log?
config.log_level = :debug
end
40 changes: 35 additions & 5 deletions lib/bravo/auth_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,49 @@ def fetch
raise "Archivo certificado no encontrado en #{Bravo.cert}"
end

todays_datafile = "/tmp/bravo_#{Time.new.strftime('%d_%m_%Y')}.yml"
opts = "-u #{Bravo.auth_url}"
opts += " -k #{Bravo.pkey}"
opts += " -c #{Bravo.cert}"
opts += " -c #{Bravo.cert}"
opts += " -a #{todays_datafile}"

unless File.exists?(todays_datafile)
%x(#{File.dirname(__FILE__)}/../../wsaa-client.sh #{opts})
unless File.exists?(todays_datafile) and File.size(todays_datafile) > 29 # Avoid empty tokens
File.delete(todays_datafile) if File.exists?(todays_datafile)
command = "#{File.dirname(__FILE__)}/../../wsaa-client.sh #{opts}"
Rails.logger.warn "Haciendo request a WSAA: " + command
rsp = %x(bash #{command} )
end

@data = YAML.load_file(todays_datafile).each do |k, v|
Bravo.const_set(k.to_s.upcase, v) unless Bravo.const_defined?(k.to_s.upcase)
Bravo.const_set(k.to_s.upcase, v) #unless Bravo.const_defined?(k.to_s.upcase)
end

error_msg = nil
if @data["error"].present?
File.delete(todays_datafile) if File.exist?(todays_datafile)
error_msg = "Error autentificando con AFIP: #{@data["error"]}"
elsif not File.exists?(todays_datafile)
error_msg = "Error autentificando con AFIP, vuelva a intentar."
end

if error_msg
Rails.logger.warn error_msg
raise error_msg
end

end

def deleteToken
%x(rm #{todays_datafile})
end

def token_modified_at
File.exist?(todays_datafile) ? File.mtime(todays_datafile) : nil
end

def todays_datafile
Dir.pwd + "/tmp/bravo_#{Bravo.cuit}_#{Time.new.strftime('%d_%m_%Y')}.yml"
end

end
end
end
148 changes: 124 additions & 24 deletions lib/bravo/bill.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
module Bravo
class Bill
attr_reader :client, :base_imp, :total
attr_accessor :net, :doc_num, :iva_cond, :documento, :concepto, :moneda,
:due_date, :aliciva_id, :fch_serv_desde, :fch_serv_hasta,
:body, :response
attr_accessor :net, :doc_num, :iva_cond, :recipient_iva_cond,
:documento, :concepto, :moneda,
:due_date, :fch_serv_desde, :fch_serv_hasta, :fch_emision,
:body, :response, :ivas, :related_invoice_data

def initialize(attrs = {})
Bravo::AuthData.fetch
Expand All @@ -15,6 +16,11 @@ def initialize(attrs = {})
http.read_timeout = 90
http.open_timeout = 90
http.headers = { "Accept-Encoding" => "gzip, deflate", "Connection" => "Keep-Alive" }

# Insist because this config doesn't stick from bravo.rb for some reason
config.logger = Rails.logger
config.log = true
config.log_level = :debug
end

@body = {"Auth" => Bravo.auth_hash}
Expand All @@ -23,6 +29,7 @@ def initialize(attrs = {})
self.moneda = attrs[:moneda] || Bravo.default_moneda
self.iva_cond = attrs[:iva_cond]
self.concepto = attrs[:concepto] || Bravo.default_concepto
self.ivas = attrs[:ivas] || Array.new # [ 1, 100.00, 10.50 ], [ 2, 100.00, 21.00 ]
end

def cbte_type
Expand All @@ -44,8 +51,14 @@ def total
end

def iva_sum
@iva_sum = net * Bravo::ALIC_IVA[aliciva_id][1]
@iva_sum.round_up_with_precision(2)
@iva_sum = 0.0
self.ivas.each{ |i|
@iva_sum += i[1] * Bravo::ALIC_IVA[ i[0] ][1]
# @iva_sum += i[2]
}
#@iva_sum = net * Bravo::ALIC_IVA[TODO][1]
#@iva_sum.round_up_with_precision(2)
@iva_sum.round(2)
end

def authorize
Expand All @@ -60,35 +73,73 @@ def authorize
end

def setup_bill
today = Time.new.strftime('%Y%m%d')
if fch_emision then
fecha_emision = fch_emision.strftime('%Y%m%d')
else
fecha_emision = Time.new.strftime('%Y%m%d') #today
end


array_ivas = Array.new
self.ivas.each{ |i|
array_ivas << {
"Id" => Bravo::ALIC_IVA[ i[0] ][0],
"BaseImp" => i[1] ,
"Importe" => i[2] }
}

fecaereq = {"FeCAEReq" => {
"FeCabReq" => Bravo::Bill.header(cbte_type),
"FeDetReq" => {
"FECAEDetRequest" => {
"Concepto" => Bravo::CONCEPTOS[concepto],
"DocTipo" => Bravo::DOCUMENTOS[documento],
"CbteFch" => today,
"CbteFch" => fecha_emision,
"ImpTotConc" => 0.00,
"MonId" => Bravo::MONEDAS[moneda][:codigo],
"MonCotiz" => exchange_rate,
"ImpOpEx" => 0.00,
"ImpTrib" => 0.00,
"Iva" => {
"AlicIva" => {
"Id" => "5",
"BaseImp" => net,
"Importe" => iva_sum}}}}}}
"ImpTrib" => 0.00
}}}}

if Bravo.own_iva_cond == :responsable_inscripto
fecaereq["FeCAEReq"]["FeDetReq"]["FECAEDetRequest"]["Iva"] = { "AlicIva" => array_ivas }
elsif Bravo.own_iva_cond == :responsable_monotributo and array_ivas.any?
raise "No incluir montos iva al facturar como Responsable Monotributo"
end

self.recipient_iva_cond = parse_iva_cond(self.recipient_iva_cond) if self.recipient_iva_cond.is_a?(Symbol)
if not self.recipient_iva_cond.is_a?(Integer)
raise "La condición IVA del receptor es un campo requerido desde el 1/8/2025"
end

detail = fecaereq["FeCAEReq"]["FeDetReq"]["FECAEDetRequest"]

detail["DocNro"] = doc_num
detail["CondicionIVAReceptorId"] = self.recipient_iva_cond
detail["ImpNeto"] = net.to_f
detail["ImpIVA"] = iva_sum
detail["ImpTotal"] = total
detail["ImpTotal"] = total.round(2)
detail["CbteDesde"] = detail["CbteHasta"] = next_bill_number

unless concepto == 0
if related_invoice_data
detail["CbtesAsoc"] = {
"CbteAsoc" => [{
"Tipo" => related_invoice_data[:type],
"PtoVta" => related_invoice_data[:sale_point],
"Nro" => related_invoice_data[:number],
"Cuit" => related_invoice_data[:cuit],
"CbteFch" => related_invoice_data[:date]
}]
}
elsif ["02", "03", "07", "08"].include?(cbte_type) # NC/ND only
detail["PeriodoAsoc"] = {
"FchDesde" => fch_emision.beginning_of_month.strftime('%Y%m%d'),
"FchHasta" => fch_emision.strftime('%Y%m%d')
}
end

unless concepto == "Productos" # En "Productos" ("01"), si se mandan estos parámetros la afip rechaza.
detail.merge!({"FchServDesde" => fch_serv_desde || today,
"FchServHasta" => fch_serv_hasta || today,
"FchVtoPago" => due_date || today})
Expand All @@ -106,8 +157,27 @@ def next_bill_number
resp.to_hash[:fe_comp_ultimo_autorizado_response][:fe_comp_ultimo_autorizado_result][:cbte_nro].to_i + 1
end

def authorized?
!response.nil? && response.header_result == "A" && response.detail_result == "A"
def authorized?
!response.nil? && response.header_result == "A" && response.detail_result == "A"
end

def search(cbte_type, cbte_nro, pto_vta)
fecompconsultarreq = {
"FeCompConsReq" => {
"CbteTipo" => cbte_type,
"CbteNro" => cbte_nro,
"PtoVta" => pto_vta
}
}
body.merge!(fecompconsultarreq)

response = client.request("FECompConsultar", soap_action: "http://ar.gov.afip.dif.FEV1/FECompConsultar") do |soap|
soap.namespaces["xmlns"] = "http://ar.gov.afip.dif.FEV1/"
soap.body = body
end
puts "FECompConsultar --> "
puts body
return response.to_json
end

private
Expand All @@ -122,16 +192,34 @@ def setup_response(response)
# TODO: turn this into an all-purpose Response class

result = response[:fecae_solicitar_response][:fecae_solicitar_result]


if not result[:fe_det_resp] or not result[:fe_cab_resp] then
# Si no obtuvo respuesta ni cabecera ni detalle, evito hacer '[]' sobre algo indefinido.
# Ejemplo: Error con el token-sign de WSAA
keys, values = {
:errores => result[:errors],
:header_result => {:resultado => "X" },
:observaciones => nil
}.to_a.transpose
self.response = (defined?(Struct::ResponseMal) ? Struct::ResponseMal : Struct.new("ResponseMal", *keys)).new(*values)
return
end

response_header = result[:fe_cab_resp]
response_detail = result[:fe_det_resp][:fecae_det_response]

request_header = body["FeCAEReq"]["FeCabReq"].underscore_keys.symbolize_keys
request_detail = body["FeCAEReq"]["FeDetReq"]["FECAEDetRequest"].underscore_keys.symbolize_keys

iva = request_detail.delete(:iva)["AlicIva"].underscore_keys.symbolize_keys

request_detail.merge!(iva)

# Esto no funciona desde que se soportan múltiples alícuotas de iva simultáneas
# FIX ? TO-DO
# iva = request_detail.delete(:iva)["AlicIva"].underscore_keys.symbolize_keys
# request_detail.merge!(iva)

if result[:errors] then
response_detail.merge!( result[:errors] )
end


response_hash = {:header_result => response_header.delete(:resultado),
:authorized_on => response_header.delete(:fch_proceso),
Expand All @@ -143,11 +231,23 @@ def setup_response(response)
:moneda => request_detail.delete(:mon_id),
:cotizacion => request_detail.delete(:mon_cotiz),
:iva_base_imp => request_detail.delete(:base_imp),
:doc_num => request_detail.delete(:doc_nro)
:doc_num => request_detail.delete(:doc_nro),
:observaciones => response_detail.delete(:observaciones),
:errores => response_detail.delete(:err)
}.merge!(request_header).merge!(request_detail)

keys, values = response_hash.to_a.transpose
self.response = (defined?(Struct::Response) ? Struct::Response : Struct.new("Response", *keys)).new(*values)

#self.response = (defined?(Struct::Response) ? Struct::Response : Struct.new("Response", *keys)).new(*values)
# Even if it throws a warning, it avoids some parsing errors.
self.response = Struct.new("Response", *keys).new(*values)
end

def parse_iva_cond(sym)
return 1 if sym == :responsable_inscripto
return 6 if sym == :monotributo
return 5 if sym == :consumidor_final
return 4 if sym == :exento
end
end
end
Loading