81 Commits
0.0.7 ... 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
19 changed files with 194 additions and 104 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/
* 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
@@ -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)
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
![Alt plugin_top_menu](/Screenshots/plugin_top_menu.png)
Note: After the inital synchronization, this plugin will recieve push notifications via Intuit's webhook service.
## 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

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
default_search_scope :names
# display a list of all customers
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
def new
@@ -81,5 +88,18 @@ class CustomersController < ApplicationController
render_404
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

View File

@@ -11,6 +11,8 @@
class QboController < ApplicationController
unloadable
require 'openssl'
include AuthHelper
before_filter :require_user, :except => :qbo_webhook
@@ -34,7 +36,6 @@ class QboController < ApplicationController
def authenticate
callback = qbo_oauth_callback_url
token = Qbo.get_oauth_consumer.get_request_token(:oauth_callback => callback)
#session[:qb_request_token] = token
session[:qb_request_token] = Marshal.dump(token)
redirect_to("https://appcenter.intuit.com/Connect/Begin?oauth_token=#{token.token}") and return
end
@@ -43,8 +44,6 @@ class QboController < ApplicationController
# Called by QBO after authentication has been processed
#
def oauth_callback
#at = session[:qb_request_token].get_access_token(:oauth_verifier => params[:oauth_verifier])
# If Rails >= 4.1 you need to do this =>
at = Marshal.load(session[:qb_request_token]).get_access_token(:oauth_verifier => params[:oauth_verifier])
#There can only be one...
@@ -62,42 +61,57 @@ class QboController < ApplicationController
else
redirect_to plugin_settings_path(:redmine_qbo), :flash => { :error => "Error" }
end
end
# Quickbooks Webhook Callback
def qbo_webhook
if request.headers['Content-Type'] == 'application/json'
data = JSON.parse(request.body.read)
# 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
# application/x-www-form-urlencoded
data = params.as_json
render nothing: true, status: 400
end
entities = data['eventNotifications'][0]['dataChangeEvent']['entities']
entities.each do |entity|
if entity['name'].eql? "Customer"
Customer.sync_by_id(entity['id'].to_i)
end
if entity['name'].eql? "Invoice"
QboInvoice.sync_by_id(entity['id'].to_i)
end
if entity['name'].eql? "Estimate"
QboEstimate.sync_by_id(entity['id'].to_i)
end
if entity['name'].eql? "Employee"
QboEmployee.sync_by_id(entity['id'].to_i)
end
end
# The webhook doesn't require a response but let's make sure
# we don't send anything
render :nothing => true
end
#
@@ -110,7 +124,6 @@ class QboController < ApplicationController
QboEmployee.sync
QboEstimate.sync
QboInvoice.sync
#QboPurchase.sync
# Record the last sync time
Qbo.update_time_stamp

View File

@@ -18,7 +18,12 @@ class VehiclesController < ApplicationController
# display a list of all vehicles
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
# return an HTML form for creating a new vehicle
@@ -86,5 +91,18 @@ class VehiclesController < ApplicationController
render_404
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

View File

@@ -140,6 +140,11 @@ class Customer < ActiveRecord::Base
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)

View File

@@ -51,4 +51,8 @@ class Qbo < ActiveRecord::Base
qbo.last_sync = DateTime.now
qbo.save
end
def self.last_sync
format_time(Qbo.first.last_sync)
end
end

View File

@@ -34,8 +34,7 @@ class QboEmployee < ActiveRecord::Base
def self.sync_by_id(id)
employee = get_base.service.fetch_by_id(id)
qbo_employee = find_or_create_by(id: employee.id)
qbo_employee = find_or_create_by(id: employee.id)
qbo_employee.name = employee.display_name
qbo_employee.id = employee.id
qbo_employee.save!

View File

@@ -21,7 +21,7 @@ class Vehicle < ActiveRecord::Base
validates_presence_of :customer
validates :vin, uniqueness: true
validates :year, numericality: { only_integer: true }
#validates :year, numericality: { only_integer: true }
before_save :decode_vin
after_initialize :get_details
@@ -75,6 +75,11 @@ class Vehicle < ActiveRecord::Base
write_attribute(:vin, val.to_s.upcase)
end
# search for a vin
def self.search(search)
where("vin LIKE ?", "%#{search}%")
end
private
# init method to pull JSON details from Edmunds
@@ -121,4 +126,5 @@ class Vehicle < ActiveRecord::Base
v = self.vin[0,11]
return v.slice(0,8) + v.slice(9,11)
end
end

View File

@@ -1,19 +1,30 @@
<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/>
<% @customers.each do |c| %>
<div class="row">
<div class="span6 columns">
<fieldset>
<%= c.name %>
<%= button_to "Show", customer_path(c.id), method: :get %>
</fieldset>
<%= link_to c, customer_path(c.id) %>
</div>
</div>
<% end %>
<br/>
<div class="actions">
<%= 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>

View File

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

View File

@@ -46,6 +46,15 @@ intuit.ipp.anywhere.setup({menuProxy: '/path/to/blue-dot', grantUrl: '<%= qbo_au
name="settings[settingsOAuthConsumerSecret]" >
</td>
</tr>
<tr>
<th>Intuit QBO Webhook Token</th>
<td>
<input type="text" style="width:350px" id="settingsWebhookToken"
value="<%= settings['settingsWebhookToken'] %>"
name="settings[settingsWebhookToken]" >
</td>
</tr>
<tr>
<th>Token Expires At</th>

View File

@@ -12,44 +12,31 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
<body>
<h1> Redmine Quickbooks</h1>
<%= form_for @qbo do |f|%>
<div>
<%= f.label "Customer Count:"+@customer_count.to_s%>
<br/>
<%= f.select :customer_id, Customer.all.pluck(:name, :id).sort, :selected => @selected_customer, include_blank: true %>
</div>
<br/>
<div>
<%= f.label "Item Count: "+@qbo_item_count.to_s %>
<br/>
<%= f.select :qbo_item_id, QboItem.all.pluck(:name, :id).sort.reverse, :selected => @selected_item, include_blank: true %>
</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>
<div>
<b>Customer Count:</b> <%= @customer_count.to_s%>
</div>
<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>
<% end %>
<br/>
<br/>
<%= link_to "Sync", qbo_sync_path %>
<div>
<b>Last Sync: </b> <%= Qbo.last_sync %>
</div>
</body>

View File

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

View File

@@ -1,3 +1,10 @@
<h1>Customer Vehicles</h1>
<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' %>

View File

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

View File

@@ -25,7 +25,7 @@ Redmine::Plugin.register :redmine_qbo do
name 'Redmine Quickbooks Online plugin'
author 'Rick Barrette'
description 'This is a plugin for Redmine to intergrate with Quickbooks Online to allow for seamless intergration CRM and invoicing of completed issues'
version '0.0.7'
version '0.1.0'
url 'https://github.com/rickbarrette/redmine_qbo'
author_url 'http://rickbarrette.org'
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, :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? }
end

View File

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