15 Commits
1.1.0 ... 1.1.1

23 changed files with 122 additions and 100 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 512 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -92,7 +92,7 @@ class QboController < ApplicationController
# Quickbooks Webhook Callback # Quickbooks Webhook Callback
def qbo_webhook def qbo_webhook
logger.debug "Quickbooks is calling webhook" logger.info "Quickbooks is calling webhook"
# check the payload # check the payload
signature = request.headers['intuit-signature'] signature = request.headers['intuit-signature']
@@ -149,14 +149,14 @@ class QboController < ApplicationController
render nothing: true, status: 400 render nothing: true, status: 400
end end
logger.debug "Quickbooks webhook complete" logger.info "Quickbooks webhook complete"
end end
# #
# Synchronizes the QboCustomer table with QBO # Synchronizes the QboCustomer table with QBO
# #
def sync def sync
logger.debug "Syncing EVERYTHING" logger.info "Syncing EVERYTHING"
# Update info in background # Update info in background
Thread.new do Thread.new do
if Qbo.exists? if Qbo.exists?

View File

@@ -62,6 +62,8 @@ class VehiclesController < ApplicationController
begin begin
@vehicle = Vehicle.find_by_id(params[:id]) @vehicle = Vehicle.find_by_id(params[:id])
@vin = @vehicle.vin.scan(/.{1,9}/) if @vehicle.vin @vin = @vehicle.vin.scan(/.{1,9}/) if @vehicle.vin
@issues = @vehicle.issues.order(id: :desc)
@closed_issues = (@issues - @issues.open)
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
end end

View File

@@ -23,7 +23,8 @@ class QboEstimate < ActiveRecord::Base
end end
# sync all estimates # sync all estimates
def self.sync def self.sync
logger.debug "Syncing ALL estimates"
estimates = get_base.all estimates = get_base.all
estimates.each { |estimate| estimates.each { |estimate|
process_estimate(estimate) process_estimate(estimate)
@@ -35,6 +36,7 @@ class QboEstimate < ActiveRecord::Base
# sync only one estimate # sync only one estimate
def self.sync_by_id(id) def self.sync_by_id(id)
logger.debug "Syncing estimate #{id}"
process_estimate(get_base.fetch_by_id(id)) process_estimate(get_base.fetch_by_id(id))
end end
@@ -44,11 +46,13 @@ class QboEstimate < ActiveRecord::Base
estimate = get_base.fetch_by_id(id) estimate = get_base.fetch_by_id(id)
qbo_estimate = find_or_create_by(id: id) qbo_estimate = find_or_create_by(id: id)
qbo_estimate.doc_number = estimate.doc_number qbo_estimate.doc_number = estimate.doc_number
qbo_estimate.txn_date = estimate.txn_date
qbo_estimate.save! qbo_estimate.save!
end end
# process an estimate into the database # process an estimate into the database
def self.process_estimate(estimate) def self.process_estimate(estimate)
logger.info "Processing estimate #{estimate.id}"
qbo_estimate = find_or_create_by(id: estimate.id) qbo_estimate = find_or_create_by(id: estimate.id)
qbo_estimate.doc_number = estimate.doc_number qbo_estimate.doc_number = estimate.doc_number
qbo_estimate.customer_id = estimate.customer_ref.value qbo_estimate.customer_id = estimate.customer_ref.value

View File

@@ -71,7 +71,7 @@ class QboInvoice < ActiveRecord::Base
# processes the invoice into the database # processes the invoice into the database
def self.process_invoice(invoice) def self.process_invoice(invoice)
logger.debug "Processing invoice" logger.info "Processing invoice #{invoice.id}"
# Load the invoice into the database # Load the invoice into the database
qbo_invoice = QboInvoice.find_or_create_by(id: invoice.id) qbo_invoice = QboInvoice.find_or_create_by(id: invoice.id)
@@ -104,6 +104,7 @@ class QboInvoice < ActiveRecord::Base
# this condions causes an infinite loop as the webhook is called when an invoice is updated # this condions causes an infinite loop as the webhook is called when an invoice is updated
# TODO maybe add a cf_sync_confict flag to invoices # TODO maybe add a cf_sync_confict flag to invoices
def self.compare_custom_fields(issue, invoice) def self.compare_custom_fields(issue, invoice)
logger.debug "Comparing custom fields"
# TODO break if QboInvoice.find(invoice.id).cf_sync_confict # TODO break if QboInvoice.find(invoice.id).cf_sync_confict
is_changed = false is_changed = false
@@ -115,12 +116,18 @@ class QboInvoice < ActiveRecord::Base
# TODO create hook for seperate plugin # TODO create hook for seperate plugin
begin begin
if cf.name.eql? "VIN" if cf.name.eql? "VIN"
vin = Vehicle.find(issue.vehicles_id).vin # Only update if blank to prevent infite loops
break if vin.nil? # TODO check cf_sync_confict flag once implemented
if not cf.string_value.to_s.eql? vin if cf.string_value.to_s.blank?
cf.string_value = vin.to_s logger.debug " VIN was blank, updating the invoice vin in quickbooks"
logger.debug "VIN has changed" vin = Vehicle.find(issue.vehicles_id).vin
is_changed = true break if vin.nil?
if not cf.string_value.to_s.eql? vin
cf.string_value = vin.to_s
logger.debug "VIN has changed"
is_changed = true
end
end end
end end
rescue rescue

View File

@@ -7,28 +7,28 @@
<div class="clearfix"> <div class="clearfix">
<%=t(:label_display_name)%> <%=t(:label_display_name)%>
<div class="input"> <div class="input">
<%= f.text_field :name, :required => true %> <%= f.text_field :name, :required => true, :autocomplete => "off" %>
</div> </div>
</div> </div>
<div class="clearfix"> <div class="clearfix">
<%=t(:label_primary_phone)%> <%=t(:label_primary_phone)%>
<div class="input"> <div class="input">
<%= f.telephone_field :primary_phone %> <%= f.telephone_field :primary_phone, :autocomplete => "off" %>
</div> </div>
</div> </div>
<div class="clearfix"> <div class="clearfix">
<%=t(:label_mobile_phone)%>: <%=t(:label_mobile_phone)%>:
<div class="input"> <div class="input">
<%= f.telephone_field :mobile_phone %> <%= f.telephone_field :mobile_phone, :autocomplete => "off" %>
</div> </div>
</div> </div>
<div class="clearfix"> <div class="clearfix">
<%=t(:label_email)%>: <%=t(:label_email)%>:
<div class="input"> <div class="input">
<%= f.email_field :email %> <%= f.email_field :email, :autocomplete => "off" %>
</div> </div>
</div> </div>

View File

@@ -1,2 +1,6 @@
<%= render :partial => 'customers/sidebar' %> <% if User.current.logged? %>
<%= render :partial => 'estimates/sidebar' %>
<%= render :partial => 'customers/sidebar' %>
<%= render :partial => 'estimates/sidebar' %>
<% end %>

View File

@@ -21,11 +21,6 @@
<td><%= vehicle.notes %></td> <td><%= vehicle.notes %></td>
</tr> </tr>
<tr>
<th> <%= t(:issues) %> </th>
<td><%= vehicle.issues.count %></td>
</tr>
<tr> <tr>
<td> <td>
<%= button_to t(:label_edit), edit_vehicle_path(vehicle), method: :get%> <%= button_to t(:label_edit), edit_vehicle_path(vehicle), method: :get%>

View File

@@ -14,21 +14,21 @@
<div class="clearfix"> <div class="clearfix">
<%=t(:label_year)%>: <%=t(:label_year)%>:
<div class="input"> <div class="input">
<%= f.number_field :year %> <%= f.number_field :year, :autocomplete => "off" %>
</div> </div>
</div> </div>
<div class="clearfix"> <div class="clearfix">
<%=t(:label_make)%>: <%=t(:label_make)%>:
<div class="input"> <div class="input">
<%= f.text_field :make %> <%= f.text_field :make, :autocomplete => "off" %>
</div> </div>
</div> </div>
<div class="clearfix"> <div class="clearfix">
<%=t(:label_model)%>: <%=t(:label_model)%>:
<div class="input"> <div class="input">
<%= f.text_field :model %> <%= f.text_field :model, :autocomplete => "off" %>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,4 @@
<%= form_tag(vehicles_path, :method => "get", id: "search-form") do %> <%= form_tag(vehicles_path, :method => "get", id: "search-form") do %>
<%= text_field_tag :search, params[:search], placeholder: t(:label_seach_vin) %> <%= text_field_tag :search, params[:search], placeholder: t(:label_search_vin), :autocomplete => "off" %>
<%= submit_tag t(:label_search) %> <%= submit_tag t(:label_search) %>
<% end %> <% end %>

View File

@@ -4,11 +4,11 @@
<div style="text-align: left; width:90%;"> <div style="text-align: left; width:90%;">
<%= render :partial => 'vehicles/details', locals: {vehicle: @vehicle} %> <%= render :partial => 'vehicles/details', locals: {vehicle: @vehicle} %>
<p> <b> <%=t(:label_open_issues)%> </b> </p> <p><b><%=@issues.open.count%> <%=t(:label_open_issues)%> </b> </p>
<%= render :partial => 'issues/list_simple', locals: {issues: @vehicle.issues.open} %> <%= render :partial => 'issues/list_simple', locals: {issues: @issues.open} %>
<p> <b> <%=t(:label_closed_issues)%> </b> </p> <p><b><%=@closed_issues.count%> <%=t(:label_closed_issues)%> </b> </p>
<%= render :partial => 'issues/list_simple', locals: {issues: (@vehicle.issues - @vehicle.issues.open)} %> <%= render :partial => 'issues/list_simple', locals: {issues: (@closed_issues)} %>
</div> </div>

View File

@@ -76,4 +76,5 @@ en:
field_customers: "Customers" field_customers: "Customers"
label_no_estimates: "No Estimates" label_no_estimates: "No Estimates"
label_no_invoices: "No Invoices" label_no_invoices: "No Invoices"
label_invoices: "Invoices" label_invoices: "Invoices"
label_load_customer: "Load Customer"

97
init.rb
View File

@@ -8,61 +8,52 @@
# #
#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. #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.
# Dynamically load all Hooks & Patches
ActiveSupport::Reloader.to_prepare do
Dir::foreach(File.join(File.dirname(__FILE__), 'lib')) do |file|
next unless /\.rb$/ =~ file
require_dependency file
end
end
Redmine::Plugin.register :redmine_qbo do Redmine::Plugin.register :redmine_qbo do
# View Hook Listeners
require_dependency 'issues_form_hook_listener'
require_dependency 'issues_save_hook_listener'
require_dependency 'issues_show_hook_listener'
require_dependency 'users_show_hook_listener'
require_dependency 'header_footer_hook_listener'
require_dependency 'projects_form_hook_listener'
require_dependency 'view_hook_listener'
# Patches to the Redmine core. Will not work in development mode
require_dependency 'issue_patch'
require_dependency 'project_patch'
require_dependency 'user_patch'
require_dependency 'query_patch'
require_dependency 'time_entry_query_patch'
require_dependency 'pdf_patch'
require_dependency 'attachments_controller_patch'
# About
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 '1.1.0'
url 'https://github.com/rickbarrette/redmine_qbo'
author_url 'http://rickbarrette.org'
settings :default => {'empty' => true}, :partial => 'qbo/settings'
# Add safe attributes # About
Issue.safe_attributes 'customer_id' name 'Redmine Quickbooks Online plugin'
Issue.safe_attributes 'qbo_item_id' author 'Rick Barrette'
Issue.safe_attributes 'qbo_estimate_id' description 'This is a plugin for Redmine to intergrate with Quickbooks Online to allow for seamless intergration CRM and invoicing of completed issues'
Issue.safe_attributes 'qbo_invoice_id' version '1.1.1'
Issue.safe_attributes 'vehicles_id' url 'https://github.com/rickbarrette/redmine_qbo'
User.safe_attributes 'qbo_employee_id' author_url 'http://rickbarrette.org'
TimeEntry.safe_attributes 'qbo_billed' settings :default => {'empty' => true}, :partial => 'qbo/settings'
Project.safe_attributes 'customer_id' requires_redmine :version_or_higher => '4.0.0'
Project.safe_attributes 'vehicle_id'
# We are playing in the sandbox
#Quickbooks.sandbox_mode = true
# set per_page globally
WillPaginate.per_page = 20
# Permissions for security # Add safe attributes for core models
permission :view_customers, :customers => :index, :public => false Issue.safe_attributes 'customer_id'
permission :add_customers, :customers => :new, :public => false Issue.safe_attributes 'qbo_item_id'
permission :view_payments, :payments => :index, :public => false Issue.safe_attributes 'qbo_estimate_id'
permission :add_payments, :payments => :new, :public => false Issue.safe_attributes 'qbo_invoice_id'
permission :view_vehicles, :payments => :new, :public => false Issue.safe_attributes 'vehicles_id'
User.safe_attributes 'qbo_employee_id'
# Register QBO top menu item TimeEntry.safe_attributes 'qbo_billed'
menu :top_menu, :customers, { :controller => :customers, :action => :index }, :caption => 'Customers', :if => Proc.new {User.current.logged?} Project.safe_attributes 'customer_id'
menu :top_menu, :vehicles, { :controller => :vehicles, :action => :index }, :caption => 'Vehicles', :if => Proc.new { User.current.logged? } Project.safe_attributes 'vehicle_id'
# We are playing in the sandbox
#Quickbooks.sandbox_mode = true
# set per_page globally
WillPaginate.per_page = 20
# Permissions for security
permission :view_customers, :customers => :index, :public => false
permission :add_customers, :customers => :new, :public => false
permission :view_payments, :payments => :index, :public => false
permission :add_payments, :payments => :new, :public => false
permission :view_vehicles, :payments => :new, :public => false
# Register top menu items
menu :top_menu, :customers, { :controller => :customers, :action => :index }, :caption => 'Customers', :if => Proc.new {User.current.logged?}
menu :top_menu, :vehicles, { :controller => :vehicles, :action => :index }, :caption => 'Vehicles', :if => Proc.new { User.current.logged? }
end end

View File

@@ -14,6 +14,6 @@ class HeaderFooterHookListener < Redmine::Hook::ViewListener
end end
def view_layouts_base_body_bottom(context = {}) def view_layouts_base_body_bottom(context = {})
return "<div id='qbo_footer' align='center'><b>Last Sync: </b> #{Qbo.last_sync if Qbo.exists?}</div>" return "<div id='qbo_footer' align='center'><b>#{I18n.translate(:label_last_sync)}: </b> #{Qbo.last_sync if Qbo.exists?}</div>"
end end
end end

View File

@@ -1,6 +1,6 @@
#The MIT License (MIT) #The MIT License (MIT)
# #
#Copyright (c) 2017 rick barrette #Copyright (c) 2022 rick barrette
# #
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: #Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
# #
@@ -18,7 +18,7 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener
end end
# Edit Issue Form # Edit Issue Form
# Show a dropdown for quickbooks contacts # TODO figure out how to do this with a view
def view_issues_form_details_bottom(context={}) def view_issues_form_details_bottom(context={})
f = context[:form] f = context[:form]
@@ -28,7 +28,7 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener
selected_vehicle = context[:project].vehicle ? context[:project].vehicle.id : nil selected_vehicle = context[:project].vehicle ? context[:project].vehicle.id : nil
end end
# Check to see if there is a quickbooks user attached to the issue # Check to see if there are things 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_estimate = context[:issue].qbo_estimate ? context[:issue].qbo_estimate.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 selected_vehicle = context[:issue].vehicles_id ? context[:issue].vehicles_id : nil
@@ -36,15 +36,22 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener
# Load customer information # Load customer information
customer = Customer.find_by_id(selected_customer) if selected_customer customer = Customer.find_by_id(selected_customer) if selected_customer
# Customer Name Text Box with database backed autocomplete
search_customer = f.autocomplete_field :customer, search_customer = f.autocomplete_field :customer,
autocomplete_customer_name_customers_path, autocomplete_customer_name_customers_path,
:selected => selected_customer, :selected => selected_customer,
:update_elements => { :id => '#issue_customer_id', :value => '#issue_customer' } :onchange => "updateIssueFrom('/issues/#{context[:issue].id}/edit.js', this)",
:update_elements => {
:id => '#issue_customer_id',
:value => '#issue_customer'
}
# Customer ID - Hidden Field
customer_id = f.hidden_field :customer_id, customer_id = f.hidden_field :customer_id,
:id => "issue_customer_id", :id => "issue_customer_id",
:onchange => "updateIssueFrom('/issues/#{context[:issue].id}/edit.js', this)" :onchange => "updateIssueFrom('/issues/#{context[:issue].id}/edit.js', this)"
# Load estimates & vehicles
if context[:issue].customer if context[:issue].customer
if customer.vehicles if customer.vehicles
vehicles = customer.vehicles.pluck(:name, :id) vehicles = customer.vehicles.pluck(:name, :id)
@@ -57,11 +64,10 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener
estimates = [nil].compact estimates = [nil].compact
end end
# Generate the drop down list of quickbooks extimates # Generate the drop down list of quickbooks extimates & vehicles
select_estimate = f.select :qbo_estimate_id, estimates, :selected => selected_estimate, include_blank: true select_estimate = f.select :qbo_estimate_id, estimates, :selected => selected_estimate, include_blank: true
vehicle = f.select :vehicles_id, vehicles, :selected => selected_vehicle, include_blank: true vehicle = f.select :vehicles_id, vehicles, :selected => selected_vehicle, include_blank: true
return "<p><label for=\"issue_customer\">Customer</label>#{search_customer} #{customer_id}</p> <p>#{select_estimate}</p> <p>#{vehicle}</p>" return "<p><label for=\"issue_customer\">Customer</label>#{search_customer} #{customer_id} #{link_to_function I18n.t(:label_load_customer), "updateIssueFrom('/issues/#{context[:issue].id}/edit.js', this)"}</p> <p>#{select_estimate}</p> <p>#{vehicle}</p>"
end end
end end

View File

@@ -32,7 +32,7 @@ class IssuesShowHookListener < Redmine::Hook::ViewListener
issue.qbo_invoice_ids.each do |i| issue.qbo_invoice_ids.each do |i|
invoice = QboInvoice.find i invoice = QboInvoice.find i
invoice_link = invoice_link + link_to( invoice.doc_number, "#{Redmine::Utils::relative_url_root}/qbo/invoice/#{i}", :target => "_blank").to_s + " " invoice_link = invoice_link + link_to( invoice.doc_number, "#{Redmine::Utils::relative_url_root}/qbo/invoice/#{i}", :target => "_blank").to_s + " "
invoice_link = invoice_link.html_safe invoice_link = invoice_link.html_safe
end end
end end
@@ -51,11 +51,11 @@ class IssuesShowHookListener < Redmine::Hook::ViewListener
:partial => 'qbo/issues_show_details', :partial => 'qbo/issues_show_details',
locals: { locals: {
customer: customer, customer: customer,
estimate_link: estimate_link, estimate_link: estimate_link,
invoice_link: invoice_link, invoice_link: invoice_link,
vehicle: vehicle, vehicle: vehicle,
split_vin: split_vin, split_vin: split_vin,
notes: notes notes: notes
} }
}) })
end end

View File

@@ -16,7 +16,7 @@ module QueryPatch
def available_columns def available_columns
unless @available_columns unless @available_columns
@available_columns = self.class.available_columns.dup @available_columns = self.class.available_columns.dup
@available_columns << QueryColumn.new(:customer, :sortable => "#{Customer.table_name}.name", :groupable => true, :caption => :field_customer) @available_columns << QueryColumn.new(:customer, :sortable => "#{Issue.table_name}.customer_id", :groupable => true, :caption => :field_customer)
@available_columns << QueryColumn.new(:qbo_billed, :sortable => "#{TimeEntry.table_name}.qbo_billed", :groupable => true, :caption => :field_qbo_billed) @available_columns << QueryColumn.new(:qbo_billed, :sortable => "#{TimeEntry.table_name}.qbo_billed", :groupable => true, :caption => :field_qbo_billed)
end end
super super
@@ -24,7 +24,7 @@ module QueryPatch
# Add customers to filters # Add customers to filters
def initialize_available_filters def initialize_available_filters
add_available_filter "customer", :type => :text #add_available_filter "customer", :type => :text
super super
end end

View File

@@ -1,3 +1,15 @@
#The MIT License (MIT)
#
#Copyright (c) 2022 rick barrette
#
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
#
#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.
class ViewHookListener < Redmine::Hook::ViewListener class ViewHookListener < Redmine::Hook::ViewListener
render_on :view_layouts_base_sidebar, :partial => "qbo/sidebar" render_on :view_layouts_base_sidebar, :partial => "qbo/sidebar"
end end