mirror of
https://github.com/rickbarrette/redmine_qbo.git
synced 2025-11-08 17:04:23 -05:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bee48c4f0f | |||
| 0f8fbfb8df | |||
| d71e9a78a1 | |||
| 7ac778586c | |||
| 2558cb69b1 | |||
| 55ac8d12a5 | |||
| a5dc5ce921 | |||
| de65cc0926 | |||
| 80d3eed224 | |||
| 76beccfb9f | |||
| 5579cd9255 | |||
| 236e84f11a | |||
| ed61dc6bbf | |||
| 2b7ac05338 | |||
| 36e63995aa | |||
| 58d16fbc7d | |||
| aa78482c36 | |||
| c35b6a3f6b | |||
| 8d52c46a53 | |||
| 325f124e4e | |||
| 18d71a69f8 | |||
| 7d5fd72297 | |||
| a625f6d9fc | |||
| ede89cc6cf | |||
| c60f06e8ed | |||
| 863a5efa38 | |||
| 670b0aac67 | |||
| d261b156bd | |||
| c49bdb731a | |||
| dc2993bdea | |||
| 09e1c0ad48 | |||
| 370153bed9 | |||
| b115c4bf67 | |||
| 90a7ac1267 | |||
| 887d330ba9 | |||
| fe97a589d9 | |||
| 37d0b2321f | |||
| 47aa454895 | |||
| fecc4956b4 | |||
| 0d5fb8d3e3 | |||
| ca6e51911b | |||
| 8159487631 | |||
| 392b27563a | |||
| 60dced41db | |||
| 8b02a80904 | |||
| ff977cc364 | |||
| 3578908832 | |||
| 4fcde967f1 | |||
| 4e81c16617 | |||
| 8149f5ab9b | |||
| 7800e52299 | |||
| c6f8fd7561 | |||
| e3d26cea23 | |||
| 4fd35d4cb6 | |||
| cf00331497 | |||
| dc8ea82b61 | |||
| aab99e3abe | |||
| f240a5a6a4 | |||
| 05ce348d8a | |||
| 67afbff93d | |||
| bd92ca8f2c | |||
| 7ef3e31465 | |||
| f59aa18be8 | |||
| 2699b37e4f | |||
| 22f8138422 | |||
| 0727257d72 | |||
| f8a9ffbe15 |
@@ -13,7 +13,8 @@ class QboController < ApplicationController
|
||||
|
||||
include AuthHelper
|
||||
|
||||
before_filter :require_user
|
||||
before_filter :require_user, :except => :qbo_webhook
|
||||
skip_before_filter :verify_authenticity_token, :check_if_login_required
|
||||
|
||||
#
|
||||
# Called when the QBO Top Menu us shown
|
||||
@@ -31,9 +32,10 @@ class QboController < ApplicationController
|
||||
# Called when the user requests that Redmine to connect to QBO
|
||||
#
|
||||
def authenticate
|
||||
callback = request.base_url + qbo_oauth_callback_path
|
||||
callback = qbo_oauth_callback_url
|
||||
token = Qbo.get_oauth_consumer.get_request_token(:oauth_callback => callback)
|
||||
session[:qb_request_token] = token
|
||||
#session[:qb_request_token] = token
|
||||
session[:qb_request_token] = Marshal.dump(token)
|
||||
redirect_to("https://appcenter.intuit.com/Connect/Begin?oauth_token=#{token.token}") and return
|
||||
end
|
||||
|
||||
@@ -41,7 +43,9 @@ class QboController < ApplicationController
|
||||
# Called by QBO after authentication has been processed
|
||||
#
|
||||
def oauth_callback
|
||||
at = session[:qb_request_token].get_access_token(:oauth_verifier => params[:oauth_verifier])
|
||||
#at = session[:qb_request_token].get_access_token(:oauth_verifier => params[:oauth_verifier])
|
||||
# If Rails >= 4.1 you need to do this =>
|
||||
at = Marshal.load(session[:qb_request_token]).get_access_token(:oauth_verifier => params[:oauth_verifier])
|
||||
|
||||
#There can only be one...
|
||||
Qbo.destroy_all
|
||||
@@ -54,12 +58,47 @@ class QboController < ApplicationController
|
||||
qbo.reconnect_token_at = 5.months.from_now.utc
|
||||
qbo.company_id = params['realmId']
|
||||
if qbo.save!
|
||||
redirect_to qbo_sync_path, :flash => { :notice => "Successfully connected to Quickbooks" }
|
||||
redirect_to qbo_path, :flash => { :notice => "Successfully connected to Quickbooks" }
|
||||
else
|
||||
redirect_to plugin_settings_path(:redmine_qbo), :flash => { :error => "Error" }
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# Quickbooks Webhook Callback
|
||||
def qbo_webhook
|
||||
|
||||
if request.headers['Content-Type'] == 'application/json'
|
||||
data = JSON.parse(request.body.read)
|
||||
else
|
||||
# application/x-www-form-urlencoded
|
||||
data = params.as_json
|
||||
end
|
||||
|
||||
entities = data['eventNotifications'][0]['dataChangeEvent']['entities']
|
||||
|
||||
entities.each do |entity|
|
||||
if entity['name'].eql? "Customer"
|
||||
Customer.sync_by_id(entity['id'].to_i)
|
||||
end
|
||||
|
||||
if entity['name'].eql? "Invoice"
|
||||
QboInvoice.sync_by_id(entity['id'].to_i)
|
||||
end
|
||||
|
||||
if entity['name'].eql? "Estimate"
|
||||
QboEstimate.sync_by_id(entity['id'].to_i)
|
||||
end
|
||||
|
||||
if entity['name'].eql? "Employee"
|
||||
QboEmployee.sync_by_id(entity['id'].to_i)
|
||||
end
|
||||
end
|
||||
|
||||
# The webhook doesn't require a response but let's make sure
|
||||
# we don't send anything
|
||||
render :nothing => true
|
||||
end
|
||||
|
||||
#
|
||||
# Synchronizes the QboCustomer table with QBO
|
||||
|
||||
@@ -36,6 +36,13 @@ class Customer < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
# Convenience Method
|
||||
# Sets the email
|
||||
def email=(s)
|
||||
pull unless @details
|
||||
@details.email_address = s
|
||||
end
|
||||
|
||||
# Convenience Method
|
||||
# returns the customer's primary phone
|
||||
def primary_phone
|
||||
@@ -108,14 +115,14 @@ class Customer < ActiveRecord::Base
|
||||
service = Qbo.get_base(:customer).service
|
||||
|
||||
# Sync ALL customers if the database is empty
|
||||
if count == 0
|
||||
#if count == 0
|
||||
customers = service.all
|
||||
else
|
||||
last = Qbo.first.last_sync
|
||||
query = "Select Id, DisplayName From Customer"
|
||||
query << " Where Metadata.LastUpdatedTime >= '#{last.iso8601}' " if last
|
||||
customers = service.query(query)
|
||||
end
|
||||
#else
|
||||
# last = Qbo.first.last_sync
|
||||
# query = "Select Id, DisplayName From Customer"
|
||||
# query << " Where Metadata.LastUpdatedTime >= '#{last.iso8601}' " if last
|
||||
# customers = service.query(query)
|
||||
#end
|
||||
|
||||
customers.each do |customer|
|
||||
qbo_customer = Customer.find_or_create_by(id: customer.id)
|
||||
@@ -133,6 +140,26 @@ class Customer < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
# proforms a bruteforce sync operation
|
||||
# This needs to be simplified
|
||||
def self.sync_by_id(id)
|
||||
service = Qbo.get_base(:customer).service
|
||||
|
||||
customer = service.fetch_by_id(id)
|
||||
qbo_customer = Customer.find_or_create_by(id: customer.id)
|
||||
if customer.active?
|
||||
if not qbo_customer.name.eql? customer.display_name
|
||||
qbo_customer.name = customer.display_name
|
||||
qbo_customer.id = customer.id
|
||||
qbo_customer.save_without_push
|
||||
end
|
||||
else
|
||||
if not qbo_customer.new_record?
|
||||
qbo_customer.delete
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Push the updates
|
||||
def save_with_push
|
||||
begin
|
||||
|
||||
@@ -11,18 +11,17 @@
|
||||
class Qbo < ActiveRecord::Base
|
||||
unloadable
|
||||
validates_presence_of :qb_token, :qb_secret, :company_id, :token_expires_at, :reconnect_token_at
|
||||
|
||||
QB_KEY = Setting.plugin_redmine_qbo['settingsOAuthConsumerKey']
|
||||
QB_SECRET = Setting.plugin_redmine_qbo['settingsOAuthConsumerSecret']
|
||||
|
||||
# Quickbooks Config Info
|
||||
$qb_oauth_consumer = OAuth::Consumer.new(QB_KEY, QB_SECRET, {
|
||||
:site => "https://oauth.intuit.com",
|
||||
:request_token_path => "/oauth/v1/get_request_token",
|
||||
:authorize_url => "https://appcenter.intuit.com/Connect/Begin",
|
||||
:access_token_path => "/oauth/v1/get_access_token"
|
||||
})
|
||||
|
||||
|
||||
OAUTH_CONSUMER_KEY = Setting.plugin_redmine_qbo['settingsOAuthConsumerKey']
|
||||
OAUTH_CONSUMER_SECRET = Setting.plugin_redmine_qbo['settingsOAuthConsumerSecret']
|
||||
|
||||
$qb_oauth_consumer = OAuth::Consumer.new(OAUTH_CONSUMER_KEY, OAUTH_CONSUMER_SECRET, {
|
||||
:site => "https://oauth.intuit.com",
|
||||
:request_token_path => "/oauth/v1/get_request_token",
|
||||
:authorize_url => "https://appcenter.intuit.com/Connect/Begin",
|
||||
:access_token_path => "/oauth/v1/get_access_token"
|
||||
})
|
||||
|
||||
# Configure quickbooks-ruby-base to access our database
|
||||
Quickbooks::Base.configure do |c|
|
||||
c.persistent_token = 'qb_token'
|
||||
@@ -31,6 +30,7 @@ class Qbo < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def self.get_oauth_consumer
|
||||
# Quickbooks Config Info
|
||||
return $qb_oauth_consumer
|
||||
end
|
||||
|
||||
|
||||
@@ -30,8 +30,14 @@ class QboEmployee < ActiveRecord::Base
|
||||
qbo_employee.save!
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def self.sync_by_id(id)
|
||||
employee = get_base.service.fetch_by_id(id)
|
||||
|
||||
#remove deleted employees
|
||||
where.not(employees.map(&:id)).destroy_all
|
||||
qbo_employee = find_or_create_by(id: employee.id)
|
||||
qbo_employee.name = employee.display_name
|
||||
qbo_employee.id = employee.id
|
||||
qbo_employee.save!
|
||||
end
|
||||
end
|
||||
|
||||
@@ -35,6 +35,14 @@ class QboEstimate < ActiveRecord::Base
|
||||
where.not(estimates.map(&:id)).destroy_all
|
||||
end
|
||||
|
||||
def self.sync_by_id(id)
|
||||
estimate = get_base.service.fetch_by_id(id)
|
||||
qbo_estimate = QboEstimate.find_or_create_by(id: estimate.id)
|
||||
qbo_estimate.doc_number = estimate.doc_number
|
||||
qbo_estimate.id = estimate.id
|
||||
qbo_estimate.save!
|
||||
end
|
||||
|
||||
def self.update(id)
|
||||
# Update the item table
|
||||
estimate = get_base.service.fetch_by_id(id)
|
||||
|
||||
@@ -45,6 +45,14 @@ class QboInvoice < ActiveRecord::Base
|
||||
#where.not(invoices.map(&:id)).destroy_all
|
||||
end
|
||||
|
||||
def self.sync_by_id(id)
|
||||
invoice = get_base.service.fetch_by_id(id)
|
||||
qbo_invoice = find_or_create_by(id: invoice.id)
|
||||
qbo_invoice.doc_number = invoice.doc_number
|
||||
qbo_invoice.id = invoice.id
|
||||
qbo_invoice.save!
|
||||
end
|
||||
|
||||
def self.update(id)
|
||||
# Update the item table
|
||||
invoice = get_base.service.fetch_by_id(id)
|
||||
|
||||
@@ -12,6 +12,8 @@ class Vehicle < ActiveRecord::Base
|
||||
|
||||
unloadable
|
||||
|
||||
API_KEY = Setting.plugin_redmine_qbo['settingsEdmundsAPIKey']
|
||||
|
||||
belongs_to :customer
|
||||
has_many :issues, :foreign_key => 'vehicles_id'
|
||||
|
||||
@@ -91,7 +93,7 @@ class Vehicle < ActiveRecord::Base
|
||||
# returns the Edmunds decoder service
|
||||
def get_decoder
|
||||
#TODO API Code via Settings
|
||||
return decoder = Edmunds::Vin.new('2dheutzvhxs28dzukx5tgu47')
|
||||
return decoder = Edmunds::Vin.new(API_KEY)
|
||||
end
|
||||
|
||||
# decodes a vin and updates self
|
||||
|
||||
@@ -10,10 +10,27 @@ The above copyright notice and this permission notice shall be included in all c
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
-->
|
||||
|
||||
<!-- somewhere in your document include the Javascript -->
|
||||
<script type="text/javascript" src="https://appcenter.intuit.com/Content/IA/intuit.ipp.anywhere.js"></script>
|
||||
|
||||
<!-- configure the Intuit object: 'grantUrl' is a URL in your application which kicks off the flow, see below -->
|
||||
<script>
|
||||
intuit.ipp.anywhere.setup({menuProxy: '/path/to/blue-dot', grantUrl: '<%= qbo_authenticate_url %>'});
|
||||
</script>
|
||||
|
||||
<table >
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>OAuth Consumer Key</th>
|
||||
<th>Edmunds API Key</th>
|
||||
<td>
|
||||
<input type="text" style="width:350px" id="settingsEdmundsAPIKey"
|
||||
value="<%= settings['settingsEdmundsAPIKey'] %>"
|
||||
name="settings[settingsEdmundsAPIKey]" >
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Intuit QBO OAuth Consumer Key</th>
|
||||
<td>
|
||||
<input type="text" style="width:350px" id="settingsOAuthConsumerKey"
|
||||
value="<%= settings['settingsOAuthConsumerKey'] %>"
|
||||
@@ -22,7 +39,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>OAuth Consumer Secret</th>
|
||||
<th>Intuit QBO OAuth Consumer Secret</th>
|
||||
<td>
|
||||
<input type="text" style="width:350px" id="settingsOAuthConsumerSecret"
|
||||
value="<%= settings['settingsOAuthConsumerSecret'] %>"
|
||||
@@ -30,14 +47,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br/>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<th>Token Expires At</th>
|
||||
<td><%= if Qbo.exists? then Qbo.first.token_expires_at end %>
|
||||
@@ -54,5 +63,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
<br/>
|
||||
Note: You need to authenticate after saving your key and secret above
|
||||
<br/>
|
||||
|
||||
<%= link_to "Authenticate", qbo_authenticate_path, :method => :get %>
|
||||
|
||||
<!-- this will display a button that the user clicks to start the flow -->
|
||||
<ipp:connectToIntuit></ipp:connectToIntuit>
|
||||
|
||||
@@ -44,6 +44,8 @@
|
||||
<tr>
|
||||
<td/>
|
||||
<td>
|
||||
|
||||
<%= button_to "New Issue", new_issue_path(:vehicle_id => vehicle.id, :customer_id => vehicle.customer.id), method: :get%>
|
||||
<%= button_to "Edit", edit_vehicle_path(vehicle), method: :get%>
|
||||
<%= button_to "Delete", vehicle, method: :delete, data: {confirm: "You sure?"} %>
|
||||
</td>
|
||||
|
||||
@@ -17,5 +17,8 @@ get 'qbo/oauth_callback', :to => 'qbo#oauth_callback'
|
||||
get 'qbo/sync', :to => 'qbo#sync'
|
||||
get 'qbo/estimate/:id', :to => 'estimate#show', as: :estimate
|
||||
get 'qbo/invoice/:id', :to => 'invoice#show', as: :invoice
|
||||
|
||||
post 'qbo/webhook', :to => 'qbo#qbo_webhook'
|
||||
|
||||
resources :vehicles
|
||||
resources :customers
|
||||
|
||||
2
init.rb
2
init.rb
@@ -25,7 +25,7 @@ Redmine::Plugin.register :redmine_qbo do
|
||||
name 'Redmine Quickbooks Online plugin'
|
||||
author 'Rick Barrette'
|
||||
description 'This is a plugin for Redmine to intergrate with Quickbooks Online to allow for seamless intergration CRM and invoicing of completed issues'
|
||||
version '0.0.6'
|
||||
version '0.0.7'
|
||||
url 'https://github.com/rickbarrette/redmine_qbo'
|
||||
author_url 'http://rickbarrette.org'
|
||||
settings :default => {'empty' => true}, :partial => 'qbo/settings'
|
||||
|
||||
@@ -26,7 +26,7 @@ module IssuePatch
|
||||
belongs_to :qbo_item, primary_key: :id
|
||||
belongs_to :qbo_estimate, primary_key: :id
|
||||
belongs_to :qbo_invoice, primary_key: :id
|
||||
belongs_to :vehicle
|
||||
belongs_to :vehicle, primary_key: :id
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -16,14 +16,15 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener
|
||||
f = context[:form]
|
||||
|
||||
# Check to see if there is a quickbooks user attached to the issue
|
||||
@selected_customer = context[:issue].customer ? context[:issue].customer.id : nil
|
||||
selected_customer = context[:issue].customer ? context[:issue].customer.id : nil
|
||||
selected_item = context[:issue].qbo_item ? context[:issue].qbo_item.id : nil
|
||||
selected_invoice = context[:issue].qbo_invoice ? context[:issue].qbo_invoice.id : nil
|
||||
selected_estimate = context[:issue].qbo_estimate ? context[:issue].qbo_estimate.id : nil
|
||||
selected_vehicle = context[:issue].vehicles_id ? context[:issue].vehicles_id : nil
|
||||
|
||||
# Load customer information without callbacks
|
||||
@customer = Customer.find_by_id(@selected_customer) if @selected_customer
|
||||
@select_customer = f.select :customer_id, Customer.all.pluck(:name, :id).sort, :selected => @selected_customer, include_blank: true
|
||||
customer = Customer.find_by_id(selected_customer) if selected_customer
|
||||
select_customer = f.select :customer_id, Customer.all.pluck(:name, :id).sort, :selected => selected_customer, include_blank: true
|
||||
|
||||
# Generate the drop down list of quickbooks items
|
||||
select_item = f.select :qbo_item_id, QboItem.all.pluck(:name, :id).sort, :selected => selected_item, include_blank: true
|
||||
@@ -34,10 +35,14 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener
|
||||
# Generate the drop down list of quickbooks extimates
|
||||
select_estimate = f.select :qbo_estimate_id, QboEstimate.all.pluck(:doc_number, :id).sort! {|x, y| y <=> x}, :selected => selected_estimate, include_blank: true
|
||||
|
||||
vehicles = @customer.vehicles.pluck(:name, :id).sort! if context[:issue].customer
|
||||
vehicles = Vehicle.all.order(:name) if not vehicles
|
||||
vehicle = f.select :vehicles_id, vehicles, include_blank: true, :selected => vehicle
|
||||
if context[:issue].customer
|
||||
vehicles = customer.vehicles.pluck(:name, :id).sort!
|
||||
else
|
||||
vehicles = Vehicle.all.order(:name).pluck(:name, :id)
|
||||
end
|
||||
|
||||
return "<p>#{@select_customer}</p> <p>#{select_item}</p> <p>#{select_invoice}</p> <p>#{select_estimate}</p> <p>#{vehicle}</p>"
|
||||
vehicle = f.select :vehicles_id, vehicles, :selected => selected_vehicle, include_blank: true
|
||||
|
||||
return "<p>#{select_customer}</p> <p>#{select_item}</p> <p>#{select_invoice}</p> <p>#{select_estimate}</p> <p>#{vehicle}</p>"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -55,11 +55,14 @@ class IssuesSaveHookListener < Redmine::Hook::ViewListener
|
||||
# Called After Issue Saved
|
||||
def controller_issues_edit_after_save(context={})
|
||||
issue = context[:issue]
|
||||
employee_id = issue.assigned_to.qbo_employee_id
|
||||
|
||||
if issue.assigned_to
|
||||
employee_id = issue.assigned_to.qbo_employee_id
|
||||
|
||||
# Check to see if we have registered with QBO and if the issue is closed.
|
||||
# If so then we need to create a new billable time activity for the customer
|
||||
bill_time(issue, employee_id) if Qbo.first && issue.customer && issue.qbo_item && employee_id && issue.status.is_closed?
|
||||
# Check to see if we have registered with QBO and if the issue is closed.
|
||||
# If so then we need to create a new billable time activity for the customer
|
||||
bill_time(issue, employee_id) if Qbo.first && issue.customer && issue.qbo_item && employee_id && issue.status.is_closed?
|
||||
end
|
||||
end
|
||||
|
||||
# Create billable time entries
|
||||
|
||||
@@ -82,7 +82,7 @@ class IssuesShowHookListener < Redmine::Hook::ViewListener
|
||||
|
||||
<div class=\"vehicle_vin attribute\">
|
||||
<div class=\"label\"><span>VIN</span>:</div>
|
||||
<div class=\"value\">#{vin}</div>
|
||||
<div class=\"value\">#{vin.gsub(/(.{9})/, '\1 ') if vin}</div>
|
||||
</div>
|
||||
|
||||
<div class=\"vehicle_notes attribute\">
|
||||
|
||||
@@ -51,7 +51,7 @@ module IssuesPdfHelperPatch
|
||||
vin = v ? v.vin : nil
|
||||
notes = v ? v.notes : nil
|
||||
left << [l(:field_vehicles), vehicle]
|
||||
left << [l(:field_vin), vin]
|
||||
left << [l(:field_vin), vin.gsub(/(.{9})/, '\1 ')]
|
||||
#left << [l(:field_notes), notes]
|
||||
|
||||
right = []
|
||||
|
||||
@@ -14,7 +14,7 @@ class UsersShowHookListener < Redmine::Hook::ViewListener
|
||||
def view_users_form(context={})
|
||||
|
||||
# Update the users
|
||||
QboEmployee.update_all
|
||||
#QboEmployee.update_all
|
||||
|
||||
# Check to see if there is a quickbooks user attached to the issue
|
||||
@selected = context[:user].qbo_employee.id if context[:user].qbo_employee
|
||||
|
||||
Reference in New Issue
Block a user