Compare commits

...

186 Commits

Author SHA1 Message Date
ricky a34b6a07fc fixed typos 2017-03-23 05:56:26 -04:00
ricky 2ce811bbbf Update auth_helper.rb 2017-03-23 05:50:31 -04:00
ricky 02153de8b0 Added before filters add_customer, view_customer 2017-03-23 05:47:37 -04:00
ricky 68be20459b Added global_check_permission 2017-03-23 05:45:45 -04:00
ricky bbd03cc337 Update init.rb 2017-03-23 05:42:54 -04:00
ricky 4fc71a93f2 Update init.rb 2017-03-23 05:42:09 -04:00
ricky 8e7e1908e4 Update customers_controller.rb 2017-03-23 05:39:55 -04:00
ricky 89fba883ef Update customers_controller.rb 2017-03-23 05:38:06 -04:00
ricky 15f317fba1 Update customers_controller.rb 2017-03-23 05:36:51 -04:00
ricky 894ee9abfd added check_permission 2017-03-23 05:33:58 -04:00
ricky ca17807117 Update payments_controller.rb 2017-03-23 05:29:54 -04:00
ricky a70ba2f164 Update payments_controller.rb 2017-03-23 05:27:38 -04:00
ricky 78ac97298c Update payments_controller.rb 2017-03-23 05:25:57 -04:00
ricky 72cd349c1b Update payments_controller.rb 2017-03-23 05:23:44 -04:00
ricky 6fc1d27dca Update auth_helper.rb 2017-03-23 05:21:56 -04:00
ricky 525c6b99d6 Update auth_helper.rb 2017-03-23 05:19:13 -04:00
ricky 3eaff0ab30 Update auth_helper.rb 2017-03-23 05:14:47 -04:00
ricky 85b40bc9cf Update payments_controller.rb 2017-03-23 05:11:15 -04:00
ricky 37a2b95447 Update payments_controller.rb 2017-03-23 05:10:05 -04:00
ricky 33feb91713 added permission_checker 2017-03-23 05:08:33 -04:00
ricky f7357f30ce Update payments_controller.rb 2017-03-23 05:03:58 -04:00
ricky c0ae01018b Update payments_controller.rb 2017-03-23 05:01:01 -04:00
ricky 4353e910c8 Update payments_controller.rb 2017-03-23 04:57:22 -04:00
ricky bef9774c4e Update payments_controller.rb 2017-03-23 04:52:19 -04:00
ricky 863437b1b7 Added before filter to check permissions 2017-03-23 04:50:17 -04:00
ricky 7cfa15910a Update init.rb 2017-03-23 04:41:31 -04:00
ricky 2154a3d001 Update init.rb 2017-03-22 23:09:05 -04:00
ricky fdab090a3d Update init.rb 2017-03-22 23:06:12 -04:00
ricky 3f32b7fef1 Update init.rb 2017-03-22 22:53:21 -04:00
ricky 14422bc549 Update init.rb 2017-03-22 22:52:24 -04:00
ricky 6bb66597e8 Added some permissions
view_customers, add_customers, view_payments, add_payments
2017-03-22 22:44:09 -04:00
ricky 32b750b545 Version 0.4.2 2017-03-22 22:38:42 -04:00
ricky 5fd3141746 Merge pull request #7 from rickbarrette/dev
Removed un-needed js files
2017-03-22 22:26:04 -04:00
ricky 2c38361234 Removed un-needed js files 2017-03-22 22:20:12 -04:00
ricky 81b7b1492d Merge pull request #6 from rickbarrette/filter_vehicles_by_customer
Filter vehicles by customer
2017-03-22 22:07:22 -04:00
ricky 57ef1ac5a1 Fixed Typo 2017-03-22 22:05:33 -04:00
ricky 6597c5a13c Update application.js 2017-03-22 22:03:25 -04:00
ricky 8af97072fb Fixed filter_vehicles_by_customer method 2017-03-22 21:55:07 -04:00
ricky 48b6df0cef Update application.js 2017-03-22 21:34:04 -04:00
ricky 853b7ad804 Rename filter_vehicles_by_customerjs.erb to filter_vehicles_by_customer.js.erb 2017-03-22 21:27:15 -04:00
ricky 6a74baff5e Update application.js 2017-03-22 18:56:44 -04:00
ricky 8b21b0ff75 Update application.js 2017-03-22 18:54:47 -04:00
ricky d22a6303cd Update application.js 2017-03-22 18:53:47 -04:00
ricky 807d6643f4 Update application.js 2017-03-22 18:52:38 -04:00
ricky c725c2774c Update application.js 2017-03-22 18:50:21 -04:00
ricky ae0abae333 Update application.js 2017-03-22 18:48:52 -04:00
ricky fed2282212 Added debug 2017-03-22 18:37:06 -04:00
ricky 545960e676 Call autocomplete instead 2017-03-22 18:28:53 -04:00
ricky 66781f0625 Update application.js 2017-03-22 13:12:29 -04:00
ricky 504b9b93e4 Removed { 2017-03-22 13:11:19 -04:00
ricky b71bba473c Provide empty list 2017-03-22 13:09:15 -04:00
ricky aef3c453c4 Do not list all vehicles without customer 2017-03-22 13:04:50 -04:00
ricky 6de3ed94dc Update and rename app/views/filter_vehicles_by_customerjs.erb to app/views/customers/filter_vehicles_by_customerjs.erb
Also changed ID to select#issue_vehicles_id
2017-03-22 12:59:36 -04:00
ricky daada08ea7 Changed select to input 2017-03-22 12:48:43 -04:00
ricky fa37c98500 Removed ID 2017-03-22 12:47:08 -04:00
ricky 0ee59704b3 Fixed Javascript includes to += 2017-03-22 12:43:25 -04:00
ricky a22cbb9520 Added Javascript include for application.js 2017-03-22 11:09:19 -04:00
ricky c0d3f64d82 Create application.js 2017-03-22 11:08:14 -04:00
ricky 66d2bf4aa4 Create filter_vehicles_by_customerjs.erb 2017-03-22 11:04:48 -04:00
ricky 2b90c953ba Added filter_vehicles_by_customer method 2017-03-22 10:29:04 -04:00
ricky 0d5e5d679e Added route for filter_vehicles_by_customer 2017-03-22 10:24:16 -04:00
ricky 4da891cc07 Merge branch 'master' of git://github.com/sempervictus/redmine_qbo into sempervictus-master 2017-02-22 11:10:46 -05:00
ricky d3475a9b53 Delete vehicles.js 2017-02-14 14:42:13 -05:00
ricky eb583f78ec Delete update_vehicles.js.coffee 2017-02-14 14:41:55 -05:00
ricky 47bebf0a1a Delete application.js 2017-02-14 14:41:34 -05:00
ricky f9bd149f21 Commented out un-needed JS 2017-02-14 14:39:58 -05:00
ricky 5371b0f193 Update application.js 2017-01-29 22:40:48 -05:00
ricky 95497e5514 Update issues_form_hook_listener.rb 2017-01-29 22:40:02 -05:00
ricky 1a74abe76c Update issues_form_hook_listener.rb 2017-01-29 22:38:30 -05:00
ricky 7f11d3cdc4 Update issues_form_hook_listener.rb 2017-01-29 22:36:32 -05:00
ricky da01d79325 Update customers_controller.rb 2017-01-29 22:29:15 -05:00
ricky 82807cfede Update customers_controller.rb 2017-01-29 22:26:53 -05:00
ricky a268d10819 Update customers_controller.rb 2017-01-29 22:25:27 -05:00
ricky ba4bdd9ecb Update routes.rb 2017-01-29 22:19:12 -05:00
ricky 0a72e05e03 Update issues_form_hook_listener.rb 2017-01-29 22:05:29 -05:00
ricky 8d143ff06d Update customers_controller.rb 2017-01-29 22:00:19 -05:00
ricky 3ddb585a58 Update customers_controller.rb 2017-01-29 21:58:49 -05:00
ricky 1606ceb743 Update issues_form_hook_listener.rb 2017-01-29 21:49:57 -05:00
ricky 97578f8380 Update issues_form_hook_listener.rb 2017-01-29 21:47:09 -05:00
ricky 32beae70ef Update issues_form_hook_listener.rb 2017-01-29 21:45:13 -05:00
ricky 13fbc9e14f Update issues_form_hook_listener.rb 2017-01-29 21:44:00 -05:00
ricky c57a45c85e Update issues_form_hook_listener.rb 2017-01-29 21:43:25 -05:00
ricky 3711a9ca43 Update issues_form_hook_listener.rb 2017-01-29 21:38:18 -05:00
ricky d0c1693f38 Create autocomplete-rails.js 2017-01-29 21:37:27 -05:00
ricky 36534ee129 Update issues_form_hook_listener.rb 2017-01-29 21:34:28 -05:00
ricky 188c460054 Update issues_form_hook_listener.rb 2017-01-29 21:15:29 -05:00
ricky d9e3cb096b Update application.js 2017-01-29 21:10:41 -05:00
ricky b57b19493f Add files via upload 2017-01-29 21:08:29 -05:00
ricky 8c569db541 Update issues_form_hook_listener.rb 2017-01-29 20:56:30 -05:00
ricky 997257f42d Update issues_form_hook_listener.rb 2017-01-29 20:54:52 -05:00
ricky f9f1af17bc Update issues_form_hook_listener.rb 2017-01-29 20:53:09 -05:00
ricky f1f44d0048 Update issues_form_hook_listener.rb 2017-01-29 20:46:39 -05:00
ricky 7e8511090d Update issues_form_hook_listener.rb 2017-01-29 20:42:18 -05:00
ricky 4ca6a3138b Update issues_form_hook_listener.rb 2017-01-29 20:41:38 -05:00
ricky 21e1132e0e Update issues_form_hook_listener.rb 2017-01-29 20:36:19 -05:00
ricky dc15424014 Update issues_form_hook_listener.rb 2017-01-29 20:30:34 -05:00
ricky f90cdcd86b Update customers_controller.rb 2017-01-29 20:28:17 -05:00
ricky 5e53c18098 Update issues_form_hook_listener.rb 2017-01-29 20:24:06 -05:00
ricky 52d13ea7bc Update customers_controller.rb 2017-01-29 20:18:21 -05:00
ricky 5e24c5084e Update customers_controller.rb 2017-01-29 20:12:38 -05:00
ricky abdb61cc41 Update Gemfile 2017-01-29 20:07:08 -05:00
ricky 03556cc670 Update Gemfile 2017-01-29 20:03:53 -05:00
ricky 7f6cd99aba Update Gemfile 2017-01-29 19:58:11 -05:00
ricky ba513fb950 Update issues_form_hook_listener.rb 2017-01-29 19:47:24 -05:00
ricky 837ddd722c Update issues_form_hook_listener.rb 2017-01-29 19:46:21 -05:00
ricky f2b0cd3748 Update issues_form_hook_listener.rb 2017-01-29 19:45:31 -05:00
ricky f2a8878af4 Update issues_form_hook_listener.rb 2017-01-29 19:40:51 -05:00
ricky 13fccec54b Update routes.rb 2017-01-29 19:36:58 -05:00
ricky eca2b986a9 Update customers_controller.rb 2017-01-29 19:35:45 -05:00
ricky a06599b7f9 Update issues_form_hook_listener.rb 2017-01-29 19:34:31 -05:00
ricky 7fda4dc577 Create application.js 2017-01-29 19:32:59 -05:00
ricky 9e47152e12 Update Gemfile 2017-01-29 19:31:31 -05:00
ricky 83d21da41a Update issues_form_hook_listener.rb 2017-01-29 19:27:23 -05:00
ricky a692f03bfa Update init.rb 2017-01-29 19:17:37 -05:00
ricky 994cdf908f Update init.rb 2017-01-29 19:16:55 -05:00
ricky b022d17fc0 Update init.rb 2017-01-29 19:16:05 -05:00
ricky 644899c0b5 Update email_worker.rb 2017-01-29 19:15:14 -05:00
ricky be3a3b920d Update email_worker.rb 2017-01-27 12:04:58 -05:00
ricky d546eb026f Update email_worker.rb 2017-01-27 12:01:53 -05:00
ricky fdc59feb13 Create email_worker.rb 2017-01-27 11:47:30 -05:00
ricky 186b726a7b Added nil check 2017-01-27 09:38:00 -05:00
RageLtMan fa362bad55 Merge pull request #3 from sempervictus/bug-dont_call_string_method_on_nil_token
Do not permit OAUTH_CONSUMER_SECRET to be nil
2017-01-19 04:09:34 -05:00
RageLtMan fcf55bb504 Do not permit OAUTH_CONSUMER_SECRET to be nil
When QBO plugin is not configured, OAUTH_CONSUMER_SECRET can be
nil, and any codepath hitting the model raises a stack trace.

Set a "safe-ish" value here to allow execution in conditions where
QBO plugin is installed, but not yet configured.
2017-01-19 04:02:59 -05:00
RageLtMan 2185667665 Merge pull request #2 from sempervictus/local
Merge initial changes 20170103
2017-01-03 04:34:58 -05:00
RageLtMan 772483817e Prevent billing if issue has no customer assigned 2017-01-03 04:32:04 -05:00
RageLtMan 178ddd32c7 Remove will_paginate version constraint 2017-01-02 05:01:41 -05:00
ricky 08e047c90e Added done_ratio to partial billing 2016-10-12 10:31:05 -04:00
ricky b3c3314385 Remove Breaks 2016-09-26 16:41:41 -04:00
ricky 9fd5e01bb4 Update qbo_invoice.rb 2016-09-26 16:39:12 -04:00
ricky cd62f65fcd Update qbo_invoice.rb 2016-09-26 16:34:56 -04:00
ricky fb40833abd Update qbo_invoice.rb 2016-09-26 16:33:56 -04:00
ricky 6aae155933 Update qbo_invoice.rb 2016-09-21 19:51:22 -04:00
ricky f9e0ae8fef Update qbo_invoice.rb 2016-09-21 19:48:26 -04:00
ricky 489e335ca4 Update qbo_invoice.rb 2016-09-21 19:46:55 -04:00
ricky 874d0b4db9 Update qbo_invoice.rb 2016-09-21 19:43:40 -04:00
ricky 49e8f70b46 Update qbo_invoice.rb 2016-09-21 16:17:07 -04:00
ricky 77ea20171e Don't tie invoice to issue if customer is diffrent 2016-09-21 16:15:43 -04:00
ricky 11d4034c37 Update query_patch.rb 2016-09-19 23:19:53 -04:00
ricky 64369470de Update auth_helper.rb 2016-09-19 23:01:58 -04:00
ricky 7b483f3290 Update invoice_controller.rb 2016-09-19 23:01:27 -04:00
ricky 32bec79c28 Update invoice_controller.rb 2016-09-19 22:59:44 -04:00
ricky dfd9622ab7 Update invoice_controller.rb 2016-09-19 22:59:02 -04:00
ricky 334d3c930b Update auth_helper.rb 2016-09-19 22:57:19 -04:00
ricky 8cf2f370bf Update invoice_controller.rb 2016-09-19 22:54:58 -04:00
ricky 3965bed6c4 Update invoice_controller.rb 2016-09-19 22:50:18 -04:00
ricky 52396eb384 Update invoice_controller.rb 2016-09-19 22:49:06 -04:00
ricky 9cfab7bea1 Update invoice_controller.rb 2016-09-19 22:44:52 -04:00
ricky c8ef3bbd4e Update invoice_controller.rb 2016-09-19 22:38:48 -04:00
ricky 39e7d3c062 Update invoice_controller.rb 2016-09-19 22:37:53 -04:00
ricky 6fa96e11df Update invoice_controller.rb 2016-09-19 22:35:51 -04:00
ricky ecde64193a Update invoice_controller.rb 2016-09-19 22:32:30 -04:00
ricky f701af9a4d Update auth_helper.rb 2016-09-19 22:30:14 -04:00
ricky 6d99702a11 Update customers_controller.rb 2016-09-19 22:27:03 -04:00
ricky 138f8f2c2f Update auth_helper.rb 2016-09-19 22:21:37 -04:00
ricky 61ddf7378d Update auth_helper.rb 2016-09-19 22:18:19 -04:00
ricky f5b72f30be Update customers_controller.rb 2016-09-19 22:17:15 -04:00
ricky 1863b33955 Update view.html.erb 2016-09-19 22:05:48 -04:00
ricky a4573fce1c Update issue_patch.rb 2016-09-19 22:01:32 -04:00
ricky 0461801ee0 Update issues_form_hook_listener.rb 2016-09-19 21:29:28 -04:00
ricky 4c2eaac013 Update routes.rb 2016-09-19 21:26:40 -04:00
ricky 7ca56ccd2e Update routes.rb 2016-09-19 20:27:26 -04:00
ricky 915a59afa4 Update issues_form_hook_listener.rb 2016-09-19 20:17:00 -04:00
ricky ac61950d48 Update routes.rb 2016-09-19 20:13:57 -04:00
ricky b257fef563 Update customers_controller.rb 2016-09-19 20:10:11 -04:00
ricky 504c27c197 Update qbo_invoice.rb 2016-09-19 11:10:26 -04:00
ricky a7a5e2c731 Update qbo_invoice.rb 2016-09-19 11:07:33 -04:00
ricky 21d72dcc33 Update qbo_invoice.rb 2016-09-19 11:06:35 -04:00
ricky da7ba40e61 Added Private Note Scanning
Also removed redundant checks
2016-09-19 10:59:20 -04:00
ricky b4d6fc55ea Update customers_controller.rb 2016-09-19 07:57:49 -04:00
ricky 515b8feff7 Update attachments_controller_patch.rb 2016-09-19 07:44:03 -04:00
ricky 8bc05db033 Update init.rb 2016-09-19 07:30:07 -04:00
ricky 34cd6b08dc Create attachments_controller_patch.rb 2016-09-19 07:29:16 -04:00
ricky 41195dc095 Update show.html.erb 2016-09-18 22:55:12 -04:00
ricky 33a83c8f76 Update _details.html.erb 2016-09-18 22:53:54 -04:00
ricky 4f613d3fe1 Update show.html.erb 2016-09-18 22:52:48 -04:00
ricky 1c7cdec600 Update show.html.erb 2016-09-18 22:41:24 -04:00
ricky 7ae60c0e62 Update show.html.erb 2016-09-18 22:38:26 -04:00
ricky 5dd04925e0 Update show.html.erb 2016-09-18 22:37:23 -04:00
ricky 92eedbd4d3 Update show.html.erb 2016-09-18 22:32:45 -04:00
ricky 5545d72adf Update view.html.erb 2016-09-16 23:11:59 -04:00
ricky 226d44cd28 Update customers_controller.rb 2016-09-16 23:10:21 -04:00
ricky b7152d6124 Update view.html.erb 2016-09-16 23:07:05 -04:00
ricky f3e9b58c87 Update customers_controller.rb 2016-09-16 23:06:20 -04:00
ricky 5209315236 Update view.html.erb 2016-09-16 23:05:23 -04:00
ricky f38a9e1ff0 Update view.html.erb 2016-09-16 23:04:47 -04:00
ricky 80fb296e24 Update customers_controller.rb 2016-09-16 23:03:07 -04:00
24 changed files with 299 additions and 145 deletions
+4 -1
View File
@@ -6,7 +6,10 @@ gem 'oauth-plugin'
gem 'oauth' gem 'oauth'
gem 'roxml' gem 'roxml'
gem 'edmunds_vin' gem 'edmunds_vin'
gem 'will_paginate', '~> 3.1.0' gem 'will_paginate'
gem 'rails-jquery-autocomplete'
gem 'jquery-rails', '~> 3.1.4'
gem 'jquery-ui-rails'
group :assets do group :assets do
gem 'coffee-rails' gem 'coffee-rails'
+24 -4
View File
@@ -27,11 +27,18 @@ class CustomersController < ApplicationController
include SortHelper include SortHelper
helper :timelog helper :timelog
before_filter :require_user, :except => :view before_filter :add_customer, :only => :new
before_filter :view_customer, :except => :new
skip_before_filter :verify_authenticity_token, :check_if_login_required, :only => [:view] skip_before_filter :verify_authenticity_token, :check_if_login_required, :only => [:view]
default_search_scope :names default_search_scope :names
autocomplete :customer, :name, :full => false, :extra_data => [:id]
def filter_vehicles_by_customer
@filtered_vehicles = Vehicle.all.where(customer_id: params[:selected_customer])
end
# display a list of all customers # display a list of all customers
def index def index
if params[:search] if params[:search]
@@ -105,9 +112,14 @@ class CustomersController < ApplicationController
# Customer view for an issue # Customer view for an issue
def view def view
token = CustomerToken.where("token = ? and expires_at > ?", params[:token], Time.now)
if token.first User.current = User.find_by lastname: 'Anonymous'
@issue = Issue.find token.first.issue_id
@token = CustomerToken.where("token = ? and expires_at > ?", params[:token], Time.now)
@token = @token.first
if @token
session[:token] = @token.token
@issue = Issue.find @token.issue_id
@journals = @issue.journals. @journals = @issue.journals.
preload(:details). preload(:details).
preload(:user => :email_address). preload(:user => :email_address).
@@ -133,6 +145,14 @@ class CustomersController < ApplicationController
private private
def add_customer
global_check_permission(:add_customers)
end
def view_customer
global_check_permission(:view_customers)
end
def only_one_non_zero?( array ) def only_one_non_zero?( array )
found_non_zero = false found_non_zero = false
array.each do |val| array.each do |val|
+2 -1
View File
@@ -12,7 +12,8 @@ class InvoiceController < ApplicationController
include AuthHelper include AuthHelper
before_filter :require_user before_filter :require_user, :unless => proc {|c| session[:token].nil? }
skip_before_filter :verify_authenticity_token, :check_if_login_required, :unless => proc {|c| session[:token].nil? }
# #
# Downloads and forwards the invoice pdf # Downloads and forwards the invoice pdf
+7 -1
View File
@@ -12,7 +12,7 @@ class PaymentsController < ApplicationController
include AuthHelper include AuthHelper
before_filter :require_user before_filter :check_permissions
def new def new
@payment = Payment.new @payment = Payment.new
@@ -37,6 +37,12 @@ end
private private
def check_permissions
if !allowed_to?(:add_payments)
render :file => "public/401.html.erb", :status => :unauthorized, :layout =>true
end
end
def only_one_non_zero?( array ) def only_one_non_zero?( array )
found_non_zero = false found_non_zero = false
array.each do |val| array.each do |val|
+4
View File
@@ -66,8 +66,12 @@ class QboController < ApplicationController
# Manual Billing # Manual Billing
def bill def bill
i = Issue.find_by_id params[:id] i = Issue.find_by_id params[:id]
if i.customer
i.bill_time i.bill_time
redirect_to i, :flash => { :notice => "Successfully Billed #{i.customer.name}" } redirect_to i, :flash => { :notice => "Successfully Billed #{i.customer.name}" }
else
redirect_to i, :flash => { :error => "Cannot bill without a customer assigned" }
end
end end
# Quickbooks Webhook Callback # Quickbooks Webhook Callback
+35
View File
@@ -11,8 +11,43 @@
module AuthHelper module AuthHelper
def require_user def require_user
return unless session[:token].nil?
if !User.current.logged? if !User.current.logged?
render :file => "public/401.html.erb", :status => :unauthorized, :layout =>true render :file => "public/401.html.erb", :status => :unauthorized, :layout =>true
end end
end end
def allowed_to?(action)
return false if User.current.nil?
project = Project.find(params[:project_id])
return false if project.nil?
return true if User.current.allowed_to?(action, project)
false
end
def check_permission(permission)
if !allowed_to?(permission)
render :file => "public/401.html.erb", :status => :unauthorized, :layout =>true
end
end
def global_check_permission(permission)
if !globaly_allowed_to?(permission)
render :file => "public/401.html.erb", :status => :unauthorized, :layout =>true
end
end
def globaly_allowed_to?( action)
return false if User.current.nil?
projects = Project.all
projects.each { |p|
if User.current.allowed_to?(action, p)
return true
end
}
false
end
end end
+1 -1
View File
@@ -15,7 +15,7 @@ class CustomerToken < ActiveRecord::Base
validates_presence_of :expires_at, :issue_id validates_presence_of :expires_at, :issue_id
before_create :generate_token before_create :generate_token
OAUTH_CONSUMER_SECRET = Setting.plugin_redmine_qbo['settingsOAuthConsumerSecret'] OAUTH_CONSUMER_SECRET = Setting.plugin_redmine_qbo['settingsOAuthConsumerSecret'] || 'CONFIGURE_QBO__' + SecureRandom.uuid
def generate_token def generate_token
self.token = SecureRandom.base64(15).tr('+/=lIO0', OAUTH_CONSUMER_SECRET) self.token = SecureRandom.base64(15).tr('+/=lIO0', OAUTH_CONSUMER_SECRET)
+40 -20
View File
@@ -46,16 +46,14 @@ class QboInvoice < ActiveRecord::Base
process_invoice invoice process_invoice invoice
end end
# processes the invoice into the system private
def self.process_invoice(invoice)
is_changed = false # Attach the invoice to the issue
def self.attach_to_issue(issue, invoice)
return if issue.nil?
# Scan the line items for hashtags and attach to the applicable issues # skip this issue if the issue customer is not the same as the invoice customer
invoice.line_items.each { |line| return if issue.customer_id != invoice.customer_ref.value.to_i
if line.description
line.description.scan(/#(\w+)/).flatten.each { |issue|
i = Issue.find_by_id(issue.to_i)
# 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)
@@ -63,24 +61,49 @@ class QboInvoice < ActiveRecord::Base
qbo_invoice.id = invoice.id qbo_invoice.id = invoice.id
qbo_invoice.save! qbo_invoice.save!
# Attach the invoice to the issue unless issue.qbo_invoices.include?(qbo_invoice)
unless i.qbo_invoices.include?(qbo_invoice) issue.qbo_invoices << qbo_invoice
i.qbo_invoices << qbo_invoice issue.save!
i.save!
end end
compare_custom_fields(issue, invoice)
end
# processes the invoice into the system
def self.process_invoice(invoice)
# Check the private notes
if not invoice.private_note.nil?
invoice.private_note.scan(/#(\w+)/).flatten.each { |issue|
attach_to_issue(Issue.find_by_id(issue.to_i), invoice)
}
end
# Scan the line items for hashtags and attach to the applicable issues
invoice.line_items.each { |line|
if line.description
line.description.scan(/#(\w+)/).flatten.each { |issue|
attach_to_issue(Issue.find_by_id(issue.to_i), invoice)
}
end
}
end
def self.compare_custom_fields(issue, invoice)
is_changed = false
# update the invoive custom fields with infomation from the work ticket if available # update the invoive custom fields with infomation from the work ticket if available
invoice.custom_fields.each { |cf| invoice.custom_fields.each { |cf|
# TODO Add some hooks here
# VIN from the attached vehicle # VIN from the attached vehicle
begin begin
if cf.name.eql? "VIN" if cf.name.eql? "VIN"
vin = Vehicle.find(i.vehicles_id).vin vin = Vehicle.find(issue.vehicles_id).vin
break if vin.nil? break if vin.nil?
if not cf.string_value.to_s.eql? vin if not cf.string_value.to_s.eql? vin
cf.string_value = vin.to_s cf.string_value = vin.to_s
is_changed = true is_changed = true
break
end end
end end
rescue rescue
@@ -89,7 +112,7 @@ class QboInvoice < ActiveRecord::Base
# Custom Values # Custom Values
begin begin
value = i.custom_values.find_by(custom_field_id: CustomField.find_by_name(cf.name).id) value = issue.custom_values.find_by(custom_field_id: CustomField.find_by_name(cf.name).id)
# Check to see if the value is blank... # Check to see if the value is blank...
if not value.value.to_s.blank? if not value.value.to_s.blank?
@@ -102,7 +125,6 @@ class QboInvoice < ActiveRecord::Base
cf.string_value = value.value.to_s cf.string_value = value.value.to_s
is_changed = true is_changed = true
end end
break
end end
# Use the max milage # Use the max milage
@@ -111,7 +133,6 @@ class QboInvoice < ActiveRecord::Base
cf.string_value = value.value.to_s cf.string_value = value.value.to_s
is_changed = true is_changed = true
end end
break
end end
# Everything else # Everything else
@@ -123,9 +144,8 @@ class QboInvoice < ActiveRecord::Base
# Nothing to do here, there is no match # Nothing to do here, there is no match
end end
} }
}
end # TODO Add some hooks here
}
# Push updates # Push updates
get_base.update(invoice) if is_changed get_base.update(invoice) if is_changed
-5
View File
@@ -1,10 +1,5 @@
<table> <table>
<tbody> <tbody>
<tr>
<th>Customer</th>
<td><%= customer.name %></td>
</tr>
<tr> <tr>
<th>Email</th> <th>Email</th>
<td><%= customer.email %></td> <td><%= customer.email %></td>
@@ -0,0 +1 @@
$('select#issue_vehicles_id').html('<%= j options_from_collection_for_select(@filtered_vehicles, :id, :to_s) %>');
+20 -5
View File
@@ -1,12 +1,27 @@
<h1>Customer #<%= @customer.id %></h1> <div id="content">
<h2>Customer #<%= @customer.id %></h2>
<br/> <br/>
<h2>Details:</h2>
<div class="subject">
<div><h3><%= @customer.name %></h3></div>
</div>
<div class="attributes">
<div class="splitcontent">
<div class="splitcontentleft">
<h4>Details:</h4>
<%= render :partial => 'customers/details', locals: {customer: @customer} %> <%= render :partial => 'customers/details', locals: {customer: @customer} %>
<br/> </div>
<h2>Vehicles:</h2> <div class="splitcontentleft">
<h4>Vehicles:</h4>
<%= render :partial => 'vehicles/list' %> <%= render :partial => 'vehicles/list' %>
<%= button_to "New Vehicle", new_customer_vehicle_path(@customer), method: :get %> <%= button_to "New Vehicle", new_customer_vehicle_path(@customer), method: :get %>
<br/> </div>
</div>
<br/> <br/>
<h2>Issues:</h2> <h2>Issues:</h2>
<%= render :partial => 'issues/list_simple', locals: {issues: @issues} %> <%= render :partial => 'issues/list_simple', locals: {issues: @issues} %>
</div>
</div>
+3 -2
View File
@@ -6,6 +6,7 @@
<div class="subject"> <div class="subject">
<%= render_issue_subject_with_tree(@issue) %> <%= render_issue_subject_with_tree(@issue) %>
This customer link expires in <%= distance_of_time_in_words(Time.now, @token.expires_at) %>
</div> </div>
<p class="author"> <p class="author">
<%= authoring @issue.created_on, @issue.author %>. <%= authoring @issue.created_on, @issue.author %>.
@@ -41,11 +42,11 @@
rows.right l(:field_estimated_hours), issue_estimated_hours_details(@issue), :class => 'estimated-hours' rows.right l(:field_estimated_hours), issue_estimated_hours_details(@issue), :class => 'estimated-hours'
end end
end end
if User.current.allowed_to_view_all_time_entries?(@project) #if User.current.allowed_to_view_all_time_entries?(@project)
if @issue.total_spent_hours > 0 if @issue.total_spent_hours > 0
rows.right l(:label_spent_time), issue_spent_hours_details(@issue), :class => 'spent-time' rows.right l(:label_spent_time), issue_spent_hours_details(@issue), :class => 'spent-time'
end end
end #end
end %> end %>
<%= render_custom_fields_rows(@issue) %> <%= render_custom_fields_rows(@issue) %>
<%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %> <%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
+18
View File
@@ -0,0 +1,18 @@
#The MIT License (MIT)
#
#Copyright (c) 2017 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.
# This sidekiq worker class will handle emailing weekly time reports
class EmailWorker
include Sidekiq::Worker
def perform()
# email something
end
end
+9
View File
@@ -0,0 +1,9 @@
$(function() {
$("input#issue_customer_id").on("change", function() {
$.ajax({
url: "/filter_vehicles_by_customer",
type: "GET",
data: { selected_customer: $("input#issue_customer_id").val() }
});
});
});
+1
View File
@@ -0,0 +1 @@
!function(t){t.fn.railsAutocomplete=function(e){var a=function(){this.railsAutoCompleter||(this.railsAutoCompleter=new t.railsAutocomplete(this))};if(void 0!==t.fn.on){if(!e)return;return t(document).on("focus",e,a)}return this.live("focus",a)},t.railsAutocomplete=function(t){var e=t;this.init(e)},t.railsAutocomplete.options={showNoMatches:!0,noMatchesLabel:"no existing match"},t.railsAutocomplete.fn=t.railsAutocomplete.prototype={railsAutocomplete:"0.0.1"},t.railsAutocomplete.fn.extend=t.railsAutocomplete.extend=t.extend,t.railsAutocomplete.fn.extend({init:function(e){function a(t){return t.split(e.delimiter)}function i(t){return a(t).pop().replace(/^\s+/,"")}e.delimiter=t(e).attr("data-delimiter")||null,e.min_length=t(e).attr("data-min-length")||t(e).attr("min-length")||2,e.append_to=t(e).attr("data-append-to")||null,e.autoFocus=t(e).attr("data-auto-focus")||!1,t(e).autocomplete({appendTo:e.append_to,autoFocus:e.autoFocus,delay:t(e).attr("delay")||0,source:function(a,r){var n=this.element[0],o={term:i(a.term)};t(e).attr("data-autocomplete-fields")&&t.each(t.parseJSON(t(e).attr("data-autocomplete-fields")),function(e,a){o[e]=t(a).val()}),t.getJSON(t(e).attr("data-autocomplete"),o,function(){var a={};t.extend(a,t.railsAutocomplete.options),t.each(a,function(i,r){if(a.hasOwnProperty(i)){var n=t(e).attr("data-"+i);a[i]=n?n:r}}),0==arguments[0].length&&t.inArray(a.showNoMatches,[!0,"true"])>=0&&(arguments[0]=[],arguments[0][0]={id:"",label:a.noMatchesLabel}),t(arguments[0]).each(function(a,i){var r={};r[i.id]=i,t(e).data(r)}),r.apply(null,arguments),t(n).trigger("railsAutocomplete.source",arguments)})},change:function(e,a){if(t(this).is("[data-id-element]")&&""!==t(t(this).attr("data-id-element")).val()&&(t(t(this).attr("data-id-element")).val(a.item?a.item.id:"").trigger("change"),t(this).attr("data-update-elements"))){var i=t.parseJSON(t(this).attr("data-update-elements")),r=a.item?t(this).data(a.item.id.toString()):{};if(i&&""===t(i.id).val())return;for(var n in i){var o=t(i[n]);o.is(":checkbox")?null!=r[n]&&o.prop("checked",r[n]):o.val(a.item?r[n]:"").trigger("change")}}},search:function(){var t=i(this.value);return t.length<e.min_length?!1:void 0},focus:function(){return!1},select:function(i,r){if(r.item.value=r.item.value.toString(),-1!=r.item.value.toLowerCase().indexOf("no match")||-1!=r.item.value.toLowerCase().indexOf("too many results"))return t(this).trigger("railsAutocomplete.noMatch",r),!1;var n=a(this.value);if(n.pop(),n.push(r.item.value),null!=e.delimiter)n.push(""),this.value=n.join(e.delimiter);else if(this.value=n.join(""),t(this).attr("data-id-element")&&t(t(this).attr("data-id-element")).val(r.item.id).trigger("change"),t(this).attr("data-update-elements")){var o=r.item,l=-1!=r.item.value.indexOf("Create New")?!0:!1,u=t.parseJSON(t(this).attr("data-update-elements"));for(var s in u)"checkbox"===t(u[s]).attr("type")?o[s]===!0||1===o[s]?t(u[s]).attr("checked","checked"):t(u[s]).removeAttr("checked"):l&&o[s]&&-1==o[s].indexOf("Create New")||!l?t(u[s]).val(o[s]).trigger("change"):t(u[s]).val("").trigger("change")}var c=this.value;return t(this).bind("keyup.clearId",function(){t.trim(t(this).val())!=t.trim(c)&&(t(t(this).attr("data-id-element")).val("").trigger("change"),t(this).unbind("keyup.clearId"))}),t(e).trigger("railsAutocomplete.select",r),!1}}),t(e).trigger("railsAutocomplete.init")}}),t(document).ready(function(){t("input[data-autocomplete]").railsAutocomplete("input[data-autocomplete]")})}(jQuery);
@@ -1,2 +0,0 @@
$("#issue_vehicles_id").empty()
.append("<%= escape_javascript(render(:partial => @vehicles)) %>")
-10
View File
@@ -1,10 +0,0 @@
function customerSelected() {
customer_id = $('issue_customer_id').getValue();
console.log(customer_id);
}
document.observe('dom:loaded', function() {
customerSelected();
$('issue_customer_id').observe('change', customerSelected);
});
+3 -1
View File
@@ -35,11 +35,13 @@ resources :payments
post 'qbo/webhook', :to => 'qbo#qbo_webhook' post 'qbo/webhook', :to => 'qbo#qbo_webhook'
#ajax #ajax
get "update_vehicles" => 'vehicles#update_vehicles', as: 'update_vehicles' get 'filter_vehicles_by_customer' => 'customers#filter_vehicles_by_customer'
# Nest Vehicles under customers # Nest Vehicles under customers
resources :customers do resources :customers do
resources :vehicles resources :vehicles
get :autocomplete_customer_name, :on => :collection
get :autocomplete_customer_vehicles, :on => :collection
end end
#allow for just vehicles too #allow for just vehicles too
+11 -11
View File
@@ -23,15 +23,12 @@ Redmine::Plugin.register :redmine_qbo do
require_dependency 'query_patch' require_dependency 'query_patch'
require_dependency 'time_entry_query_patch' require_dependency 'time_entry_query_patch'
require_dependency 'pdf_patch' require_dependency 'pdf_patch'
require_dependency 'attachments_controller_patch'
Rails.configuration.to_prepare do
Redmine::Search.available_search_types << 'customers'
end
name 'Redmine Quickbooks Online plugin' name 'Redmine Quickbooks Online plugin'
author 'Rick Barrette' 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' 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.4.1' version '0.4.2'
url 'https://github.com/rickbarrette/redmine_qbo' url 'https://github.com/rickbarrette/redmine_qbo'
author_url 'http://rickbarrette.org' author_url 'http://rickbarrette.org'
settings :default => {'empty' => true}, :partial => 'qbo/settings' settings :default => {'empty' => true}, :partial => 'qbo/settings'
@@ -51,17 +48,20 @@ Redmine::Plugin.register :redmine_qbo do
# set per_page globally # set per_page globally
WillPaginate.per_page = 10 WillPaginate.per_page = 10
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 QBO top menu item # Register QBO top menu item
#menu :top_menu, :qbo, { :controller => :qbo, :action => :index }, :caption => 'Quickbooks', :if => Proc.new { User.current.admin? } #menu :top_menu, :qbo, { :controller => :qbo, :action => :index }, :caption => 'Quickbooks', :if => Proc.new { User.current.admin? }
menu :top_menu, :customers, { :controller => :customers, :action => :index }, :caption => 'Customers', :if => Proc.new {User.current.logged?} 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? } menu :top_menu, :vehicles, { :controller => :vehicles, :action => :index }, :caption => 'Vehicles', :if => Proc.new { User.current.allowed_to?(:view_vehicles, @project) }
menu :application_menu, :new_customer, { :controller => :customers, :action => :new }, :caption => 'New Customer', :if => Proc.new { User.current.logged? } menu :application_menu, :new_customer, { :controller => :customers, :action => :new }, :caption => 'New Customer', :if => Proc.new { User.current.allowed_to?(:add_customers, @project) }
menu :application_menu, :new_payment, { :controller => :payments, :action => :new }, :caption => 'New Payment', :if => Proc.new { User.current.logged? } menu :application_menu, :new_payment, { :controller => :payments, :action => :new }, :caption => 'New Payment', :if => Proc.new { User.current.allowed_to?(:add_payments, @project)}
permission :customers, { :customers => [:index, :new] }, :public => false
menu :project_menu, :customers, { :controller => 'customers', :action => 'new' }, :caption => 'New Customer', :after => :new_issue, :param => :project_id menu :project_menu, :customers, { :controller => 'customers', :action => 'new' }, :caption => 'New Customer', :after => :new_issue, :param => :project_id
permission :payments, { :payments => [:index, :new] }, :public => false
menu :project_menu, :payments, { :controller => 'payments', :action => 'new' }, :caption => 'New Payment', :after => :customers, :param => :project_id menu :project_menu, :payments, { :controller => 'payments', :action => 'new' }, :caption => 'New Payment', :after => :customers, :param => :project_id
end end
+38
View File
@@ -0,0 +1,38 @@
#The MIT License (MIT)
#
#Copyright (c) 2016 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.
require_dependency 'attachments_controller'
module AttachmentsControllerPatch
def self.included(base) # :nodoc:
base.extend(ClassMethods)
base.send(:include, InstanceMethods)
# Same as typing in the class
base.class_eval do
unloadable # Send unloadable so it will not be unloaded in development
skip_before_action :read_authorize
end
end
module ClassMethods
end
module InstanceMethods
end
end
# Add module to AttachmentsController
AttachmentsController.send(:include, AttachmentsControllerPatch)
+2 -2
View File
@@ -72,7 +72,7 @@ module IssuePatch
item = item_service.query("SELECT * FROM Item WHERE Name = '#{key}' ").first item = item_service.query("SELECT * FROM Item WHERE Name = '#{key}' ").first
next if item.nil? next if item.nil?
time_entry.description = "#{tracker} ##{id}: #{subject} #{"(Partial)" if not closed?}" time_entry.description = "#{tracker} ##{id}: #{subject} #{"(Partial @ #{done_ratio}%)" if not closed?}"
# TODO entry.user.qbo_employee.id # TODO entry.user.qbo_employee.id
time_entry.employee_id = assigned_to.qbo_employee_id time_entry.employee_id = assigned_to.qbo_employee_id
time_entry.customer_id = customer_id time_entry.customer_id = customer_id
@@ -93,7 +93,7 @@ module IssuePatch
# Create a shareable link for a customer # Create a shareable link for a customer
def share_token def share_token
CustomerToken.create(:expires_at => Time.now + 24.hours, :issue_id => id) CustomerToken.create(:expires_at => Time.now + 1.month, :issue_id => id)
end end
end end
+7 -10
View File
@@ -12,8 +12,8 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener
# Load the javascript # Load the javascript
def view_layouts_base_html_head(context = {}) def view_layouts_base_html_head(context = {})
js = javascript_include_tag 'vehicles', :plugin => 'redmine_qbo' js = javascript_include_tag 'application', :plugin => 'redmine_qbo'
#js += javascript_include_tag 'update_vehicles', :plugin => 'redmine_qbo' js += javascript_include_tag 'autocomplete-rails', :plugin => 'redmine_qbo'
return js return js
end end
@@ -27,13 +27,10 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener
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
# Load customer information without callbacks # Load customer information
customer = Customer.find_by_id(selected_customer) if selected_customer customer = Customer.find_by_id(selected_customer) if selected_customer
select_customer = f.select :customer_id, search_customer = f.autocomplete_field :customer, autocomplete_customer_name_customers_path, :selected => selected_customer, :update_elements => {:id => '#issue_customer_id', :value => '#issue_customer'}
Customer.all.pluck(:name, :id).sort, customer_id = f.hidden_field :customer_id, :id => "issue_customer_id"
:selected => selected_customer,
include_blank: true,
onchange: "$customerSelected;"
# Generate the drop down list of quickbooks extimates # 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 select_estimate = f.select :qbo_estimate_id, QboEstimate.all.pluck(:doc_number, :id).sort! {|x, y| y <=> x}, :selected => selected_estimate, include_blank: true
@@ -41,11 +38,11 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener
if context[:issue].customer if context[:issue].customer
vehicles = customer.vehicles.pluck(:name, :id).sort! vehicles = customer.vehicles.pluck(:name, :id).sort!
else else
vehicles = Vehicle.all.order(:name).pluck(:name, :id) vehicles = [nil].compact
end end
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>#{select_customer}</p> <p>#{select_estimate}</p> <p>#{vehicle}</p>" return "<p><label for=\"issue_customer\">Customer</label>#{search_customer} #{customer_id}</p> <p>#{select_estimate}</p> <p>#{vehicle}</p>"
end end
end end
+1 -1
View File
@@ -51,7 +51,7 @@ module IssuesPdfHelperPatch
vin = v ? v.vin : nil vin = v ? v.vin : nil
notes = v ? v.notes : nil notes = v ? v.notes : nil
left << [l(:field_vehicles), vehicle] left << [l(:field_vehicles), vehicle]
left << [l(:field_vin), vin.gsub(/(.{9})/, '\1 ')] left << [l(:field_vin), vin ? vin.gsub(/(.{9})/, '\1 ') : nil]
#left << [l(:field_notes), notes] #left << [l(:field_notes), notes]
right = [] right = []
+1 -1
View File
@@ -36,7 +36,7 @@ module QueryPatch
unless @available_columns unless @available_columns
@available_columns = available_columns_without_qbo @available_columns = available_columns_without_qbo
@available_columns << QueryColumn.new(:customer, :sortable => "#{Customer.table_name}.name", :groupable => true, :caption => :field_customer) @available_columns << QueryColumn.new(:customer, :sortable => "#{Customer.table_name}.name", :groupable => true, :caption => :field_customer)
@available_columns << QueryColumn.new(:qbo_billed, :sortable => "#{TimeEntry.table_name}.name", :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
@available_columns @available_columns
end end