148 Commits
0.0.6 ... 0.1.0

Author SHA1 Message Date
06e827fff8 Version bump 0.1.0 2016-08-01 22:30:47 -04:00
b1844689df Update qbo_controller.rb 2016-08-01 22:29:53 -04:00
a4263a92ca Update issues_show_hook_listener.rb 2016-08-01 22:20:57 -04:00
14cc251809 Update Gemfile 2016-08-01 21:56:17 -04:00
471e8f3398 Update qbo_controller.rb 2016-08-01 21:56:00 -04:00
dadbda62c6 Update qbo_controller.rb 2016-08-01 21:55:30 -04:00
df47efe816 Update qbo_controller.rb 2016-08-01 21:54:31 -04:00
03cc6943a3 Update Gemfile 2016-08-01 21:51:04 -04:00
6f0163ce7d Update qbo_controller.rb 2016-08-01 21:49:17 -04:00
91110adad5 Update qbo_controller.rb 2016-08-01 21:47:30 -04:00
c2f48d0277 Update qbo_controller.rb 2016-08-01 21:41:47 -04:00
06344b6498 Update qbo_controller.rb 2016-08-01 21:34:14 -04:00
4ff2b2bdc6 Update _settings.html.erb 2016-08-01 21:28:15 -04:00
a71dd310fe Update issues_show_hook_listener.rb 2016-08-01 21:24:22 -04:00
90da7a5d74 Update issues_show_hook_listener.rb 2016-08-01 21:23:15 -04:00
6505f54c7f Update issues_show_hook_listener.rb 2016-08-01 21:22:31 -04:00
c4a488e5a7 Update issues_show_hook_listener.rb 2016-08-01 21:21:41 -04:00
71817f5ca8 Update issues_show_hook_listener.rb 2016-08-01 21:18:31 -04:00
77c97ef2c1 Update vehicle.rb 2016-08-01 21:14:02 -04:00
875ec19e38 Update customer.rb 2016-08-01 21:09:39 -04:00
f47e77f816 Version bump 0.0.8 2016-08-01 21:08:49 -04:00
144a52f813 Add files via upload 2016-08-01 21:07:01 -04:00
f9a5269fd7 Delete plugin_issue_edit.png 2016-08-01 21:06:48 -04:00
bc1445f8bb Update qbo_employee.rb 2016-08-01 21:01:06 -04:00
01e5415074 Update qbo_controller.rb 2016-08-01 20:57:51 -04:00
697ff4f9d5 Update qbo_controller.rb 2016-08-01 20:54:42 -04:00
fe7cfc6b1d Update qbo_controller.rb 2016-08-01 20:52:24 -04:00
7a7e148719 Update README.md 2016-08-01 20:38:04 -04:00
95db8f9839 Update _list.html.erb 2016-08-01 20:31:48 -04:00
7fb91ae10b Update _list.html.erb 2016-08-01 20:29:58 -04:00
0c5c778c75 Update _list.html.erb 2016-08-01 20:28:18 -04:00
38865bd062 Update vehicle.rb 2016-08-01 19:34:28 -04:00
e201765f02 Update vehicles_controller.rb 2016-08-01 19:31:51 -04:00
d9ccffe3d6 Update vehicles_controller.rb 2016-08-01 19:30:25 -04:00
86e084574e Update _list.html.erb 2016-08-01 19:28:22 -04:00
756e60b865 Update index.html.erb 2016-08-01 19:09:41 -04:00
5c49094b40 Update index.html.erb 2016-08-01 19:08:49 -04:00
336d1c7c7b Update vehicles_controller.rb 2016-08-01 19:05:49 -04:00
632b788082 Update vehicle.rb 2016-08-01 19:04:46 -04:00
ddd00a3e9a Update show.html.erb 2016-08-01 19:02:49 -04:00
54e59fbd98 Update show.html.erb 2016-08-01 17:00:54 -04:00
3f29a024f9 Update show.html.erb 2016-08-01 17:00:25 -04:00
c5f03ed03c Update _details.html.erb 2016-08-01 16:59:35 -04:00
547880443c Update _details.html.erb 2016-08-01 16:58:51 -04:00
838733fdc3 Update README.md 2016-08-01 16:55:56 -04:00
4b068266a9 Add files via upload 2016-08-01 16:48:46 -04:00
57c78f27a7 Delete plugin_config.png 2016-08-01 16:48:31 -04:00
3af5caef4a Update index.html.erb 2016-08-01 16:41:59 -04:00
49425656ee Update index.html.erb 2016-08-01 16:40:50 -04:00
3e85216e66 Update qbo.rb 2016-08-01 16:39:58 -04:00
443d6fc47c Update customer.rb 2016-08-01 16:13:58 -04:00
4d7bc59bd3 Update customer.rb 2016-08-01 16:12:08 -04:00
4dbeee0aa1 Update index.html.erb 2016-08-01 16:09:30 -04:00
9b137fed69 Update index.html.erb 2016-08-01 16:02:05 -04:00
9c667c20da Update index.html.erb 2016-08-01 16:01:24 -04:00
e503c965c3 Update index.html.erb 2016-08-01 15:59:51 -04:00
933d1eb730 Update qbo_controller.rb 2016-08-01 15:54:40 -04:00
c99fe57074 Update qbo_controller.rb 2016-08-01 15:52:02 -04:00
77fc54dc31 Update customer.rb 2016-08-01 15:48:28 -04:00
37f6518a15 Update customer.rb 2016-08-01 15:47:10 -04:00
bcaf011166 Update customers_controller.rb 2016-08-01 15:46:37 -04:00
27807e963d Update customers_controller.rb 2016-08-01 15:45:57 -04:00
f0fabc5e10 Update customers_controller.rb 2016-08-01 15:45:10 -04:00
e7c85eac4d Update index.html.erb 2016-08-01 15:43:45 -04:00
01ea01fef6 Update index.html.erb 2016-08-01 15:42:42 -04:00
134fb776f9 Update index.html.erb 2016-08-01 15:40:17 -04:00
9cd143c5ef Update index.html.erb 2016-08-01 15:39:04 -04:00
ba18275ef8 Update index.html.erb 2016-08-01 15:37:32 -04:00
6e92648d8b Update customers_controller.rb 2016-08-01 15:36:10 -04:00
8ddc612bba Extended search 2016-08-01 15:34:17 -04:00
2324aadcd5 Extended search 2016-08-01 15:31:54 -04:00
baff3f5a1b Extended search 2016-08-01 15:30:00 -04:00
68b6ea7649 Update _list.html.erb 2016-07-28 23:33:15 -04:00
c46cab6a6f Update customers_controller.rb 2016-07-28 23:30:49 -04:00
74807c73b0 Jump to customer is there is only one result 2016-07-28 23:27:02 -04:00
a26214fef7 Search 2016-07-28 20:50:23 -04:00
ec77a004a2 Search 2016-07-28 20:49:38 -04:00
f33203b0e3 Search 2016-07-28 20:48:34 -04:00
297dd8ec4e Search 2016-07-28 20:46:49 -04:00
bbc2ae4750 Search 2016-07-28 20:45:29 -04:00
fb1a560751 Search 2016-07-28 20:44:15 -04:00
bee48c4f0f Version Bump 0.0.7! 2016-07-28 20:37:13 -04:00
0f8fbfb8df Update qbo.rb 2016-07-28 20:34:31 -04:00
d71e9a78a1 Update qbo_controller.rb 2016-07-28 20:06:06 -04:00
7ac778586c Update qbo.rb 2016-07-28 20:03:36 -04:00
2558cb69b1 Update qbo_controller.rb 2016-07-28 19:52:42 -04:00
55ac8d12a5 Update qbo_controller.rb 2016-07-28 19:51:48 -04:00
a5dc5ce921 Update qbo.rb 2016-07-28 19:42:09 -04:00
de65cc0926 Update qbo.rb 2016-07-28 19:41:02 -04:00
80d3eed224 Update qbo_controller.rb 2016-07-28 19:32:24 -04:00
76beccfb9f Update qbo_controller.rb 2016-07-28 19:31:24 -04:00
5579cd9255 Update _settings.html.erb 2016-07-28 19:27:00 -04:00
236e84f11a Update _settings.html.erb 2016-07-28 19:23:20 -04:00
ed61dc6bbf Update qbo_controller.rb 2016-07-28 19:21:40 -04:00
2b7ac05338 Update qbo.rb 2016-07-28 19:19:13 -04:00
36e63995aa Update qbo_controller.rb 2016-07-28 19:15:19 -04:00
58d16fbc7d Update qbo_controller.rb 2016-07-28 19:12:36 -04:00
aa78482c36 Update qbo_controller.rb 2016-07-28 19:11:09 -04:00
c35b6a3f6b Update qbo_controller.rb 2016-07-28 18:35:44 -04:00
8d52c46a53 Update qbo_controller.rb 2016-07-28 18:31:54 -04:00
325f124e4e Update qbo_controller.rb 2016-07-28 18:30:48 -04:00
18d71a69f8 Update qbo_controller.rb 2016-07-28 18:27:00 -04:00
7d5fd72297 Update qbo_controller.rb 2016-07-28 18:21:58 -04:00
a625f6d9fc Update qbo_controller.rb 2016-07-28 18:17:44 -04:00
ede89cc6cf Update qbo_controller.rb 2016-07-28 18:15:56 -04:00
c60f06e8ed Webhook 2016-07-28 09:18:50 -04:00
863a5efa38 Webhook 2016-07-28 09:17:21 -04:00
670b0aac67 Webhook 2016-07-28 09:16:13 -04:00
d261b156bd Webhook 2016-07-28 09:14:18 -04:00
c49bdb731a Webhook 2016-07-28 09:13:16 -04:00
dc2993bdea Webhook 2016-07-28 09:09:38 -04:00
09e1c0ad48 Webhook 2016-07-28 09:07:51 -04:00
370153bed9 Webhook 2016-07-28 09:04:17 -04:00
b115c4bf67 Webhook 2016-07-28 09:03:55 -04:00
90a7ac1267 Webhook 2016-07-28 09:02:29 -04:00
887d330ba9 Merge branch 'master' of github.com:rickbarrette/redmine_qbo 2016-07-28 09:02:04 -04:00
fe97a589d9 Update 2016-07-28 09:01:41 -04:00
37d0b2321f Webhook 2016-07-28 08:59:43 -04:00
47aa454895 Webhook 2016-07-28 08:57:28 -04:00
fecc4956b4 Webhook 2016-07-28 08:52:30 -04:00
0d5fb8d3e3 Update qbo_controller.rb 2016-07-28 08:47:39 -04:00
ca6e51911b Webhook 2016-07-28 08:41:23 -04:00
8159487631 Webhook 2016-07-28 08:33:26 -04:00
392b27563a Webhooks 2016-07-28 07:55:20 -04:00
60dced41db Webhook 2016-07-28 01:07:54 -04:00
8b02a80904 Webhooks 2016-07-28 01:04:00 -04:00
ff977cc364 Update pdf_patch.rb 2016-07-22 19:01:43 -04:00
3578908832 Update pdf_patch.rb 2016-07-22 19:00:24 -04:00
4fcde967f1 Update issues_show_hook_listener.rb 2016-07-22 19:00:03 -04:00
4e81c16617 Update pdf_patch.rb 2016-07-22 16:02:57 -04:00
8149f5ab9b Update issues_show_hook_listener.rb 2016-07-22 15:59:49 -04:00
7800e52299 Update users_show_hook_listener.rb 2016-07-13 16:33:04 -04:00
c6f8fd7561 Update issues_form_hook_listener.rb 2016-06-04 10:09:27 -04:00
e3d26cea23 Update issue_patch.rb 2016-06-02 09:47:42 -04:00
4fd35d4cb6 Update issues_form_hook_listener.rb 2016-06-02 09:45:54 -04:00
cf00331497 Update issues_form_hook_listener.rb 2016-06-02 09:45:28 -04:00
dc8ea82b61 Update issues_form_hook_listener.rb 2016-06-02 09:41:54 -04:00
aab99e3abe Update issues_form_hook_listener.rb 2016-06-02 09:41:14 -04:00
f240a5a6a4 Update issues_form_hook_listener.rb 2016-06-02 09:40:13 -04:00
05ce348d8a Update issues_form_hook_listener.rb 2016-06-02 09:36:34 -04:00
67afbff93d Update issues_form_hook_listener.rb 2016-06-02 09:35:41 -04:00
bd92ca8f2c Fixed Vehicle Drop down 2016-06-02 09:25:52 -04:00
7ef3e31465 New Issue Button 2016-06-02 09:20:44 -04:00
f59aa18be8 Fixed Formatting 2016-06-02 09:02:06 -04:00
2699b37e4f Edmunds API Key 2016-06-02 08:56:13 -04:00
22f8138422 Edmunds API Key Setting 2016-06-02 08:54:37 -04:00
0727257d72 Update issues_save_hook_listener.rb 2016-06-01 12:01:51 -04:00
f8a9ffbe15 Setter for email 2016-05-31 11:16:41 -04:00
28 changed files with 328 additions and 125 deletions

View File

@@ -20,6 +20,8 @@ The goal of this project is to allow redmine to connect with Quickbooks Online t
* Sign up to become a developer for Intuit https://developer.intuit.com/ * Sign up to become a developer for Intuit https://developer.intuit.com/
* Create your own aplication to obtain your API keys * Create your own aplication to obtain your API keys
* Set up webhook service to https://redmine.yourdomain.com/qbo/webhook
- See https://developer.intuit.com/docs/0100_accounting/0300_developer_guides/webhooks
##The Install ##The Install
@@ -47,9 +49,7 @@ The goal of this project is to allow redmine to connect with Quickbooks Online t
![Alt plugin_issue-edit](/Screenshots/plugin_issue_edit.png) ![Alt plugin_issue-edit](/Screenshots/plugin_issue_edit.png)
Note: Customers, Employees, and Service Items with automaticly update during normal usage of redmine i.e. a page refresh. You can also manualy force redmine to sync its database with QBO clicking the sync link in the Quickbooks top menu page Note: After the inital synchronization, this plugin will recieve push notifications via Intuit's webhook service.
![Alt plugin_top_menu](/Screenshots/plugin_top_menu.png)
## TODO ## TODO
* Abiltiy to add line items to a ticket in a dynamic table so they can be added to the invoice upon closing of the issue * Abiltiy to add line items to a ticket in a dynamic table so they can be added to the invoice upon closing of the issue

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

After

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -16,9 +16,16 @@ class CustomersController < ApplicationController
before_filter :require_user before_filter :require_user
default_search_scope :names
# display a list of all customers # display a list of all customers
def index def index
@customers = Customer.paginate(:page => params[:page]) if params[:search]
@customers = Customer.search(params[:search]).paginate(:page => params[:page])
if only_one_non_zero?(@customers)
redirect_to @customers.first
end
end
end end
def new def new
@@ -82,4 +89,17 @@ class CustomersController < ApplicationController
end end
end end
private
def only_one_non_zero?( array )
found_non_zero = false
array.each do |val|
if val!=0
return false if found_non_zero
found_non_zero = true
end
end
found_non_zero
end
end end

View File

@@ -11,9 +11,12 @@
class QboController < ApplicationController class QboController < ApplicationController
unloadable unloadable
require 'openssl'
include AuthHelper 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 # Called when the QBO Top Menu us shown
@@ -31,9 +34,9 @@ class QboController < ApplicationController
# Called when the user requests that Redmine to connect to QBO # Called when the user requests that Redmine to connect to QBO
# #
def authenticate 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) token = Qbo.get_oauth_consumer.get_request_token(:oauth_callback => callback)
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 redirect_to("https://appcenter.intuit.com/Connect/Begin?oauth_token=#{token.token}") and return
end end
@@ -41,7 +44,7 @@ class QboController < ApplicationController
# Called by QBO after authentication has been processed # Called by QBO after authentication has been processed
# #
def oauth_callback def oauth_callback
at = session[:qb_request_token].get_access_token(:oauth_verifier => params[:oauth_verifier]) at = Marshal.load(session[:qb_request_token]).get_access_token(:oauth_verifier => params[:oauth_verifier])
#There can only be one... #There can only be one...
Qbo.destroy_all Qbo.destroy_all
@@ -54,11 +57,61 @@ class QboController < ApplicationController
qbo.reconnect_token_at = 5.months.from_now.utc qbo.reconnect_token_at = 5.months.from_now.utc
qbo.company_id = params['realmId'] qbo.company_id = params['realmId']
if qbo.save! 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 else
redirect_to plugin_settings_path(:redmine_qbo), :flash => { :error => "Error" } redirect_to plugin_settings_path(:redmine_qbo), :flash => { :error => "Error" }
end end
end
# Quickbooks Webhook Callback
def qbo_webhook
# check the payload
signature = request.headers['intuit-signature']
key = Setting.plugin_redmine_qbo['settingsWebhookToken']
data = request.body.read
hash = Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha256'), key, data)).strip()
# proceed if the request is good
if hash.eql? signature
if request.headers['content-type'] == 'application/json'
data = JSON.parse(data)
else
# application/x-www-form-urlencoded
data = params.as_json
end
# Process the information
entities = data['eventNotifications'][0]['dataChangeEvent']['entities']
entities.each do |entity|
id = entity['id'].to_i
name = entity['name']
# TODO rename all other models!
name.prepend("Qbo") if not name.eql? "Customer"
# Magicly initialize the correct class
obj = name.constantize
# for merge events
obj.destroy(entity['deletedId']) if entity['deletedId']
#Check to see if we are deleting a record
if entity['operation'].eql? "Delete"
obj.destroy(id)
#if not then update!
else
obj.sync_by_id(id)
end
end
# Record that last time we updated
Qbo.update_time_stamp
# The webhook doesn't require a response but let's make sure we don't send anything
render :nothing => true
else
render nothing: true, status: 400
end
end end
# #
@@ -71,7 +124,6 @@ class QboController < ApplicationController
QboEmployee.sync QboEmployee.sync
QboEstimate.sync QboEstimate.sync
QboInvoice.sync QboInvoice.sync
#QboPurchase.sync
# Record the last sync time # Record the last sync time
Qbo.update_time_stamp Qbo.update_time_stamp

View File

@@ -18,7 +18,12 @@ class VehiclesController < ApplicationController
# display a list of all vehicles # display a list of all vehicles
def index def index
@vehicles = Vehicle.paginate(:page => params[:page]) if params[:search]
@vehicles = Vehicle.search(params[:search]).paginate(:page => params[:page])
if only_one_non_zero?(@vehicles)
redirect_to @vehicles.first
end
end
end end
# return an HTML form for creating a new vehicle # return an HTML form for creating a new vehicle
@@ -87,4 +92,17 @@ class VehiclesController < ApplicationController
end end
end end
private
def only_one_non_zero?( array )
found_non_zero = false
array.each do |val|
if val!=0
return false if found_non_zero
found_non_zero = true
end
end
found_non_zero
end
end end

View File

@@ -36,6 +36,13 @@ class Customer < ActiveRecord::Base
end end
end end
# Convenience Method
# Sets the email
def email=(s)
pull unless @details
@details.email_address = s
end
# Convenience Method # Convenience Method
# returns the customer's primary phone # returns the customer's primary phone
def primary_phone def primary_phone
@@ -108,14 +115,14 @@ class Customer < ActiveRecord::Base
service = Qbo.get_base(:customer).service service = Qbo.get_base(:customer).service
# Sync ALL customers if the database is empty # Sync ALL customers if the database is empty
if count == 0 #if count == 0
customers = service.all customers = service.all
else #else
last = Qbo.first.last_sync # last = Qbo.first.last_sync
query = "Select Id, DisplayName From Customer" # query = "Select Id, DisplayName From Customer"
query << " Where Metadata.LastUpdatedTime >= '#{last.iso8601}' " if last # query << " Where Metadata.LastUpdatedTime >= '#{last.iso8601}' " if last
customers = service.query(query) # customers = service.query(query)
end #end
customers.each do |customer| customers.each do |customer|
qbo_customer = Customer.find_or_create_by(id: customer.id) qbo_customer = Customer.find_or_create_by(id: customer.id)
@@ -133,6 +140,31 @@ class Customer < ActiveRecord::Base
end end
end end
# Searchs the database for a customer by name
def self.search(search)
where("name LIKE ?", "%#{search}%").order(:name)
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 # Push the updates
def save_with_push def save_with_push
begin begin

View File

@@ -12,11 +12,10 @@ class Qbo < ActiveRecord::Base
unloadable unloadable
validates_presence_of :qb_token, :qb_secret, :company_id, :token_expires_at, :reconnect_token_at validates_presence_of :qb_token, :qb_secret, :company_id, :token_expires_at, :reconnect_token_at
QB_KEY = Setting.plugin_redmine_qbo['settingsOAuthConsumerKey'] OAUTH_CONSUMER_KEY = Setting.plugin_redmine_qbo['settingsOAuthConsumerKey']
QB_SECRET = Setting.plugin_redmine_qbo['settingsOAuthConsumerSecret'] OAUTH_CONSUMER_SECRET = Setting.plugin_redmine_qbo['settingsOAuthConsumerSecret']
# Quickbooks Config Info $qb_oauth_consumer = OAuth::Consumer.new(OAUTH_CONSUMER_KEY, OAUTH_CONSUMER_SECRET, {
$qb_oauth_consumer = OAuth::Consumer.new(QB_KEY, QB_SECRET, {
:site => "https://oauth.intuit.com", :site => "https://oauth.intuit.com",
:request_token_path => "/oauth/v1/get_request_token", :request_token_path => "/oauth/v1/get_request_token",
:authorize_url => "https://appcenter.intuit.com/Connect/Begin", :authorize_url => "https://appcenter.intuit.com/Connect/Begin",
@@ -31,6 +30,7 @@ class Qbo < ActiveRecord::Base
end end
def self.get_oauth_consumer def self.get_oauth_consumer
# Quickbooks Config Info
return $qb_oauth_consumer return $qb_oauth_consumer
end end
@@ -51,4 +51,8 @@ class Qbo < ActiveRecord::Base
qbo.last_sync = DateTime.now qbo.last_sync = DateTime.now
qbo.save qbo.save
end end
def self.last_sync
format_time(Qbo.first.last_sync)
end
end end

View File

@@ -30,8 +30,13 @@ class QboEmployee < ActiveRecord::Base
qbo_employee.save! qbo_employee.save!
} }
end end
end
#remove deleted employees def self.sync_by_id(id)
where.not(employees.map(&:id)).destroy_all employee = get_base.service.fetch_by_id(id)
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
end end

View File

@@ -35,6 +35,14 @@ class QboEstimate < ActiveRecord::Base
where.not(estimates.map(&:id)).destroy_all where.not(estimates.map(&:id)).destroy_all
end 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) def self.update(id)
# Update the item table # Update the item table
estimate = get_base.service.fetch_by_id(id) estimate = get_base.service.fetch_by_id(id)

View File

@@ -45,6 +45,14 @@ class QboInvoice < ActiveRecord::Base
#where.not(invoices.map(&:id)).destroy_all #where.not(invoices.map(&:id)).destroy_all
end 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) def self.update(id)
# Update the item table # Update the item table
invoice = get_base.service.fetch_by_id(id) invoice = get_base.service.fetch_by_id(id)

View File

@@ -12,6 +12,8 @@ class Vehicle < ActiveRecord::Base
unloadable unloadable
API_KEY = Setting.plugin_redmine_qbo['settingsEdmundsAPIKey']
belongs_to :customer belongs_to :customer
has_many :issues, :foreign_key => 'vehicles_id' has_many :issues, :foreign_key => 'vehicles_id'
@@ -19,7 +21,7 @@ class Vehicle < ActiveRecord::Base
validates_presence_of :customer validates_presence_of :customer
validates :vin, uniqueness: true validates :vin, uniqueness: true
validates :year, numericality: { only_integer: true } #validates :year, numericality: { only_integer: true }
before_save :decode_vin before_save :decode_vin
after_initialize :get_details after_initialize :get_details
@@ -73,6 +75,11 @@ class Vehicle < ActiveRecord::Base
write_attribute(:vin, val.to_s.upcase) write_attribute(:vin, val.to_s.upcase)
end end
# search for a vin
def self.search(search)
where("vin LIKE ?", "%#{search}%")
end
private private
# init method to pull JSON details from Edmunds # init method to pull JSON details from Edmunds
@@ -91,7 +98,7 @@ class Vehicle < ActiveRecord::Base
# returns the Edmunds decoder service # returns the Edmunds decoder service
def get_decoder def get_decoder
#TODO API Code via Settings #TODO API Code via Settings
return decoder = Edmunds::Vin.new('2dheutzvhxs28dzukx5tgu47') return decoder = Edmunds::Vin.new(API_KEY)
end end
# decodes a vin and updates self # decodes a vin and updates self
@@ -119,4 +126,5 @@ class Vehicle < ActiveRecord::Base
v = self.vin[0,11] v = self.vin[0,11]
return v.slice(0,8) + v.slice(9,11) return v.slice(0,8) + v.slice(9,11)
end end
end end

View File

@@ -1,19 +1,30 @@
<h1>Customers</h1> <h1>Customers</h1>
<br/>
<%= form_tag(customers_path, :method => "get", id: "search-form") do %>
<%= text_field_tag :search, params[:search], placeholder: "Search Customers" %>
<%= submit_tag "Search" %>
<%= button_to "New Customer", new_customer_path, method: :get %>
<% end %>
<br/>
<% if @customers.present? %>
<br/> <br/>
<% @customers.each do |c| %> <% @customers.each do |c| %>
<div class="row"> <div class="row">
<div class="span6 columns"> <div class="span6 columns">
<fieldset> <%= link_to c, customer_path(c.id) %>
<%= c.name %>
<%= button_to "Show", customer_path(c.id), method: :get %>
</fieldset>
</div> </div>
</div> </div>
<% end %> <% end %>
<br/>
<div class="actions"> <div class="actions">
<%= will_paginate @customers %> <%= will_paginate @customers %>
<%= button_to "New", new_customer_path, method: :get %> </div>
<% else %>
<p>There are no customers containing the term(s) <%= params[:search] %>.</p>
<% end %>
<div>
<b>Last Sync: </b> <%= Qbo.last_sync %>
</div> </div>

View File

@@ -1,4 +1,4 @@
<h1>Customer Detail</h1> <h1>Customer #<%= @customer.id %></h1>
<br/> <br/>
<%= render :partial => 'customers/details', locals: {customer: @customer} %> <%= render :partial => 'customers/details', locals: {customer: @customer} %>
<br/> <br/>

View File

@@ -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. 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 > <table >
<tbody> <tbody>
<tr> <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> <td>
<input type="text" style="width:350px" id="settingsOAuthConsumerKey" <input type="text" style="width:350px" id="settingsOAuthConsumerKey"
value="<%= settings['settingsOAuthConsumerKey'] %>" value="<%= settings['settingsOAuthConsumerKey'] %>"
@@ -22,7 +39,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
</tr> </tr>
<tr> <tr>
<th>OAuth Consumer Secret</th> <th>Intuit QBO OAuth Consumer Secret</th>
<td> <td>
<input type="text" style="width:350px" id="settingsOAuthConsumerSecret" <input type="text" style="width:350px" id="settingsOAuthConsumerSecret"
value="<%= settings['settingsOAuthConsumerSecret'] %>" value="<%= settings['settingsOAuthConsumerSecret'] %>"
@@ -30,13 +47,14 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
</td> </td>
</tr> </tr>
</tbody> <tr>
</table> <th>Intuit QBO Webhook Token</th>
<td>
<br/> <input type="text" style="width:350px" id="settingsWebhookToken"
value="<%= settings['settingsWebhookToken'] %>"
<table> name="settings[settingsWebhookToken]" >
<tbody> </td>
</tr>
<tr> <tr>
<th>Token Expires At</th> <th>Token Expires At</th>
@@ -55,4 +73,5 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
Note: You need to authenticate after saving your key and secret above Note: You need to authenticate after saving your key and secret above
<br/> <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>

View File

@@ -12,44 +12,31 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
<body> <body>
<h1> Redmine Quickbooks</h1> <h1> Redmine Quickbooks</h1>
<%= form_for @qbo do |f|%>
<div> <div>
<%= f.label "Customer Count:"+@customer_count.to_s%> <b>Customer Count:</b> <%= @customer_count.to_s%>
<br/> </div>
<%= f.select :customer_id, Customer.all.pluck(:name, :id).sort, :selected => @selected_customer, include_blank: true %>
<div>
<b>Item Count:</b> <%= @qbo_item_count.to_s %>
</div>
<div>
<b>Employee Count:</b> <%= @qbo_employee_count.to_s %>
</div>
<div>
<b>Invoice Count:</b> <%= @qbo_invoice_count.to_s %>
</div>
<div>
<b>Estimate Count:</b> <%= @qbo_estimate_count.to_s %>
</div> </div>
<br/> <br/>
<div> <div>
<%= f.label "Item Count: "+@qbo_item_count.to_s %> <b>Last Sync: </b> <%= Qbo.last_sync %>
<br/>
<%= f.select :qbo_item_id, QboItem.all.pluck(:name, :id).sort.reverse, :selected => @selected_item, include_blank: true %>
</div> </div>
<br/>
<div>
<%= f.label "Employee Count: "+@qbo_employee_count.to_s %>
<br/>
<%= f.select :qbo_employee_id, QboEmployee.all.pluck(:name, :id).sort, :selected => @selected_employee, include_blank: true %>
</div>
<p>
<%= f.label "Invoice Count: "+@qbo_invoice_count.to_s %>
<br/>
<%=f.select :qbo_invoice_id, QboInvoice.all.pluck(:doc_number, :id).sort! {|x, y| y <=> x}, :selected => @selected_invoice, include_blank: true%>
</p>
<p>
<%= f.label "Estimate Count: "+@qbo_estimate_count.to_s %>
<br/>
<%=f.select :qbo_estimate_id, QboEstimate.all.pluck(:doc_number, :id).sort! {|x, y| y <=> x}, :selected => @selected_estimate, include_blank: true%>
</p>
<% end %>
<br/>
<br/>
<%= link_to "Sync", qbo_sync_path %>
</body> </body>

View File

@@ -44,6 +44,8 @@
<tr> <tr>
<td/> <td/>
<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 "Edit", edit_vehicle_path(vehicle), method: :get%>
<%= button_to "Delete", vehicle, method: :delete, data: {confirm: "You sure?"} %> <%= button_to "Delete", vehicle, method: :delete, data: {confirm: "You sure?"} %>
</td> </td>

View File

@@ -1,20 +1,28 @@
<% @vehicles.each do |vehicle| %> <% if @vehicles.present? %>
<% @vehicles.each do |vehicle| %>
<div class="row"> <div class="row">
<div class="span6 columns"> <div>
<fieldset> <b><%= link_to "##{vehicle.id}", vehicle_path(vehicle) %> </b>
</div>
<div>
<%= vehicle.to_s %> <%= vehicle.to_s %>
<br/> <br/>
<div style="float: right;" > <%= vehicle.customer %>
<%= button_to "More", vehicle_path(vehicle), method: :get %> <br/>
</div> <%= vehicle.vin %>
</fieldset>
</div> </div>
</div> </div>
<% end %> <br/>
<% end %>
<br/> <br/>
<div class="actions"> <div class="actions">
<%= will_paginate @vehicles %> <%= will_paginate @vehicles %>
<%= button_to "New Vehicle", new_vehicle_path, method: :get %> </div>
</div>
<% else %>
<p>There are no vehicles containing the term(s) <%= params[:search] %>.</p>
<% end %>

View File

@@ -1,3 +1,10 @@
<h1>Customer Vehicles</h1> <h1>Customer Vehicles</h1>
<br/> <br/>
<%= form_tag(vehicles_path, :method => "get", id: "search-form") do %>
<%= text_field_tag :search, params[:search], placeholder: "Search Vehicles by VIN" %>
<%= submit_tag "Search" %>
<%= button_to "New Vehicle", new_vehicle_path, method: :get %>
<% end %>
<%= render :partial => 'vehicles/list' %> <%= render :partial => 'vehicles/list' %>

View File

@@ -1,4 +1,4 @@
<h1>Customer Vehicle</h1> <h1>Vehicle #<%=@vehicle.id%> </h1>
<br/> <br/>
<div style="text-align: left; width:90%;"> <div style="text-align: left; width:90%;">

View File

@@ -17,5 +17,8 @@ get 'qbo/oauth_callback', :to => 'qbo#oauth_callback'
get 'qbo/sync', :to => 'qbo#sync' get 'qbo/sync', :to => 'qbo#sync'
get 'qbo/estimate/:id', :to => 'estimate#show', as: :estimate get 'qbo/estimate/:id', :to => 'estimate#show', as: :estimate
get 'qbo/invoice/:id', :to => 'invoice#show', as: :invoice get 'qbo/invoice/:id', :to => 'invoice#show', as: :invoice
post 'qbo/webhook', :to => 'qbo#qbo_webhook'
resources :vehicles resources :vehicles
resources :customers resources :customers

View File

@@ -25,7 +25,7 @@ Redmine::Plugin.register :redmine_qbo do
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.0.6' version '0.1.0'
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'
@@ -49,4 +49,5 @@ Redmine::Plugin.register :redmine_qbo do
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, :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.logged? }
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? }
end end

View File

@@ -26,7 +26,7 @@ module IssuePatch
belongs_to :qbo_item, primary_key: :id belongs_to :qbo_item, primary_key: :id
belongs_to :qbo_estimate, primary_key: :id belongs_to :qbo_estimate, primary_key: :id
belongs_to :qbo_invoice, primary_key: :id belongs_to :qbo_invoice, primary_key: :id
belongs_to :vehicle belongs_to :vehicle, primary_key: :id
end end
end end

View File

@@ -16,14 +16,15 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener
f = context[:form] f = context[:form]
# Check to see if there is a quickbooks user attached to the issue # 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_item = context[:issue].qbo_item ? context[:issue].qbo_item.id : nil
selected_invoice = context[:issue].qbo_invoice ? context[:issue].qbo_invoice.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_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 # Load customer information without callbacks
@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, Customer.all.pluck(:name, :id).sort, :selected => @selected_customer, include_blank: true 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 # 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 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 # 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
vehicles = @customer.vehicles.pluck(:name, :id).sort! if context[:issue].customer if context[:issue].customer
vehicles = Vehicle.all.order(:name) if not vehicles vehicles = customer.vehicles.pluck(:name, :id).sort!
vehicle = f.select :vehicles_id, vehicles, include_blank: true, :selected => vehicle 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
end end

View File

@@ -55,12 +55,15 @@ class IssuesSaveHookListener < Redmine::Hook::ViewListener
# Called After Issue Saved # Called After Issue Saved
def controller_issues_edit_after_save(context={}) def controller_issues_edit_after_save(context={})
issue = context[:issue] issue = context[:issue]
if issue.assigned_to
employee_id = issue.assigned_to.qbo_employee_id employee_id = issue.assigned_to.qbo_employee_id
# Check to see if we have registered with QBO and if the issue 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 # 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? bill_time(issue, employee_id) if Qbo.first && issue.customer && issue.qbo_item && employee_id && issue.status.is_closed?
end end
end
# Create billable time entries # Create billable time entries
def bill_time(issue, employee_id) def bill_time(issue, employee_id)

View File

@@ -50,6 +50,8 @@ class IssuesShowHookListener < Redmine::Hook::ViewListener
#do nothing #do nothing
end end
split_vin = vin.scan(/.{1,9}/) if vin
return " return "
<div class=\"attributes\"> <div class=\"attributes\">
@@ -82,7 +84,7 @@ class IssuesShowHookListener < Redmine::Hook::ViewListener
<div class=\"vehicle_vin attribute\"> <div class=\"vehicle_vin attribute\">
<div class=\"label\"><span>VIN</span>:</div> <div class=\"label\"><span>VIN</span>:</div>
<div class=\"value\">#{vin}</div> <div class=\"value\">#{split_vin[0] if split_vin}<b>#{split_vin[1] if split_vin}</b></div>
</div> </div>
<div class=\"vehicle_notes attribute\"> <div class=\"vehicle_notes attribute\">

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] left << [l(:field_vin), vin.gsub(/(.{9})/, '\1 ')]
#left << [l(:field_notes), notes] #left << [l(:field_notes), notes]
right = [] right = []

View File

@@ -14,7 +14,7 @@ class UsersShowHookListener < Redmine::Hook::ViewListener
def view_users_form(context={}) def view_users_form(context={})
# Update the users # Update the users
QboEmployee.update_all #QboEmployee.update_all
# Check to see if there is a quickbooks user attached to the issue # Check to see if there is a quickbooks user attached to the issue
@selected = context[:user].qbo_employee.id if context[:user].qbo_employee @selected = context[:user].qbo_employee.id if context[:user].qbo_employee