mirror of
https://github.com/rickbarrette/redmine_qbo.git
synced 2025-11-08 08:54:23 -05:00
Compare commits
148 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 06e827fff8 | |||
| b1844689df | |||
| a4263a92ca | |||
| 14cc251809 | |||
| 471e8f3398 | |||
| dadbda62c6 | |||
| df47efe816 | |||
| 03cc6943a3 | |||
| 6f0163ce7d | |||
| 91110adad5 | |||
| c2f48d0277 | |||
| 06344b6498 | |||
| 4ff2b2bdc6 | |||
| a71dd310fe | |||
| 90da7a5d74 | |||
| 6505f54c7f | |||
| c4a488e5a7 | |||
| 71817f5ca8 | |||
| 77c97ef2c1 | |||
| 875ec19e38 | |||
| f47e77f816 | |||
| 144a52f813 | |||
| f9a5269fd7 | |||
| bc1445f8bb | |||
| 01e5415074 | |||
| 697ff4f9d5 | |||
| fe7cfc6b1d | |||
| 7a7e148719 | |||
| 95db8f9839 | |||
| 7fb91ae10b | |||
| 0c5c778c75 | |||
| 38865bd062 | |||
| e201765f02 | |||
| d9ccffe3d6 | |||
| 86e084574e | |||
| 756e60b865 | |||
| 5c49094b40 | |||
| 336d1c7c7b | |||
| 632b788082 | |||
| ddd00a3e9a | |||
| 54e59fbd98 | |||
| 3f29a024f9 | |||
| c5f03ed03c | |||
| 547880443c | |||
| 838733fdc3 | |||
| 4b068266a9 | |||
| 57c78f27a7 | |||
| 3af5caef4a | |||
| 49425656ee | |||
| 3e85216e66 | |||
| 443d6fc47c | |||
| 4d7bc59bd3 | |||
| 4dbeee0aa1 | |||
| 9b137fed69 | |||
| 9c667c20da | |||
| e503c965c3 | |||
| 933d1eb730 | |||
| c99fe57074 | |||
| 77fc54dc31 | |||
| 37f6518a15 | |||
| bcaf011166 | |||
| 27807e963d | |||
| f0fabc5e10 | |||
| e7c85eac4d | |||
| 01ea01fef6 | |||
| 134fb776f9 | |||
| 9cd143c5ef | |||
| ba18275ef8 | |||
| 6e92648d8b | |||
| 8ddc612bba | |||
| 2324aadcd5 | |||
| baff3f5a1b | |||
| 68b6ea7649 | |||
| c46cab6a6f | |||
| 74807c73b0 | |||
| a26214fef7 | |||
| ec77a004a2 | |||
| f33203b0e3 | |||
| 297dd8ec4e | |||
| bbc2ae4750 | |||
| fb1a560751 | |||
| bee48c4f0f | |||
| 0f8fbfb8df | |||
| d71e9a78a1 | |||
| 7ac778586c | |||
| 2558cb69b1 | |||
| 55ac8d12a5 | |||
| a5dc5ce921 | |||
| de65cc0926 | |||
| 80d3eed224 | |||
| 76beccfb9f | |||
| 5579cd9255 | |||
| 236e84f11a | |||
| ed61dc6bbf | |||
| 2b7ac05338 | |||
| 36e63995aa | |||
| 58d16fbc7d | |||
| aa78482c36 | |||
| c35b6a3f6b | |||
| 8d52c46a53 | |||
| 325f124e4e | |||
| 18d71a69f8 | |||
| 7d5fd72297 | |||
| a625f6d9fc | |||
| ede89cc6cf | |||
| c60f06e8ed | |||
| 863a5efa38 | |||
| 670b0aac67 | |||
| d261b156bd | |||
| c49bdb731a | |||
| dc2993bdea | |||
| 09e1c0ad48 | |||
| 370153bed9 | |||
| b115c4bf67 | |||
| 90a7ac1267 | |||
| 887d330ba9 | |||
| fe97a589d9 | |||
| 37d0b2321f | |||
| 47aa454895 | |||
| fecc4956b4 | |||
| 0d5fb8d3e3 | |||
| ca6e51911b | |||
| 8159487631 | |||
| 392b27563a | |||
| 60dced41db | |||
| 8b02a80904 | |||
| ff977cc364 | |||
| 3578908832 | |||
| 4fcde967f1 | |||
| 4e81c16617 | |||
| 8149f5ab9b | |||
| 7800e52299 | |||
| c6f8fd7561 | |||
| e3d26cea23 | |||
| 4fd35d4cb6 | |||
| cf00331497 | |||
| dc8ea82b61 | |||
| aab99e3abe | |||
| f240a5a6a4 | |||
| 05ce348d8a | |||
| 67afbff93d | |||
| bd92ca8f2c | |||
| 7ef3e31465 | |||
| f59aa18be8 | |||
| 2699b37e4f | |||
| 22f8138422 | |||
| 0727257d72 | |||
| f8a9ffbe15 |
@@ -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
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
## 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 |
@@ -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
|
||||
|
||||
@@ -11,9 +11,12 @@
|
||||
class QboController < ApplicationController
|
||||
unloadable
|
||||
|
||||
require 'openssl'
|
||||
|
||||
include AuthHelper
|
||||
|
||||
before_filter :require_user
|
||||
before_filter :require_user, :except => :qbo_webhook
|
||||
skip_before_filter :verify_authenticity_token, :check_if_login_required
|
||||
|
||||
#
|
||||
# Called when the QBO Top Menu us shown
|
||||
@@ -31,9 +34,9 @@ class QboController < ApplicationController
|
||||
# Called when the user requests that Redmine to connect to QBO
|
||||
#
|
||||
def authenticate
|
||||
callback = request.base_url + qbo_oauth_callback_path
|
||||
callback = qbo_oauth_callback_url
|
||||
token = Qbo.get_oauth_consumer.get_request_token(:oauth_callback => callback)
|
||||
session[:qb_request_token] = token
|
||||
session[:qb_request_token] = Marshal.dump(token)
|
||||
redirect_to("https://appcenter.intuit.com/Connect/Begin?oauth_token=#{token.token}") and return
|
||||
end
|
||||
|
||||
@@ -41,7 +44,7 @@ class QboController < ApplicationController
|
||||
# Called by QBO after authentication has been processed
|
||||
#
|
||||
def oauth_callback
|
||||
at = session[:qb_request_token].get_access_token(:oauth_verifier => params[:oauth_verifier])
|
||||
at = Marshal.load(session[:qb_request_token]).get_access_token(:oauth_verifier => params[:oauth_verifier])
|
||||
|
||||
#There can only be one...
|
||||
Qbo.destroy_all
|
||||
@@ -54,11 +57,61 @@ class QboController < ApplicationController
|
||||
qbo.reconnect_token_at = 5.months.from_now.utc
|
||||
qbo.company_id = params['realmId']
|
||||
if qbo.save!
|
||||
redirect_to qbo_sync_path, :flash => { :notice => "Successfully connected to Quickbooks" }
|
||||
redirect_to qbo_path, :flash => { :notice => "Successfully connected to Quickbooks" }
|
||||
else
|
||||
redirect_to plugin_settings_path(:redmine_qbo), :flash => { :error => "Error" }
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# Quickbooks Webhook Callback
|
||||
def qbo_webhook
|
||||
|
||||
# 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
|
||||
|
||||
#
|
||||
@@ -71,7 +124,6 @@ class QboController < ApplicationController
|
||||
QboEmployee.sync
|
||||
QboEstimate.sync
|
||||
QboInvoice.sync
|
||||
#QboPurchase.sync
|
||||
|
||||
# Record the last sync time
|
||||
Qbo.update_time_stamp
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -36,6 +36,13 @@ class Customer < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
# Convenience Method
|
||||
# Sets the email
|
||||
def email=(s)
|
||||
pull unless @details
|
||||
@details.email_address = s
|
||||
end
|
||||
|
||||
# Convenience Method
|
||||
# returns the customer's primary phone
|
||||
def primary_phone
|
||||
@@ -108,14 +115,14 @@ class Customer < ActiveRecord::Base
|
||||
service = Qbo.get_base(:customer).service
|
||||
|
||||
# Sync ALL customers if the database is empty
|
||||
if count == 0
|
||||
#if count == 0
|
||||
customers = service.all
|
||||
else
|
||||
last = Qbo.first.last_sync
|
||||
query = "Select Id, DisplayName From Customer"
|
||||
query << " Where Metadata.LastUpdatedTime >= '#{last.iso8601}' " if last
|
||||
customers = service.query(query)
|
||||
end
|
||||
#else
|
||||
# last = Qbo.first.last_sync
|
||||
# query = "Select Id, DisplayName From Customer"
|
||||
# query << " Where Metadata.LastUpdatedTime >= '#{last.iso8601}' " if last
|
||||
# customers = service.query(query)
|
||||
#end
|
||||
|
||||
customers.each do |customer|
|
||||
qbo_customer = Customer.find_or_create_by(id: customer.id)
|
||||
@@ -133,6 +140,31 @@ 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)
|
||||
service = Qbo.get_base(:customer).service
|
||||
|
||||
customer = service.fetch_by_id(id)
|
||||
qbo_customer = Customer.find_or_create_by(id: customer.id)
|
||||
if customer.active?
|
||||
if not qbo_customer.name.eql? customer.display_name
|
||||
qbo_customer.name = customer.display_name
|
||||
qbo_customer.id = customer.id
|
||||
qbo_customer.save_without_push
|
||||
end
|
||||
else
|
||||
if not qbo_customer.new_record?
|
||||
qbo_customer.delete
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Push the updates
|
||||
def save_with_push
|
||||
begin
|
||||
|
||||
@@ -11,18 +11,17 @@
|
||||
class Qbo < ActiveRecord::Base
|
||||
unloadable
|
||||
validates_presence_of :qb_token, :qb_secret, :company_id, :token_expires_at, :reconnect_token_at
|
||||
|
||||
QB_KEY = Setting.plugin_redmine_qbo['settingsOAuthConsumerKey']
|
||||
QB_SECRET = Setting.plugin_redmine_qbo['settingsOAuthConsumerSecret']
|
||||
|
||||
# Quickbooks Config Info
|
||||
$qb_oauth_consumer = OAuth::Consumer.new(QB_KEY, QB_SECRET, {
|
||||
:site => "https://oauth.intuit.com",
|
||||
:request_token_path => "/oauth/v1/get_request_token",
|
||||
:authorize_url => "https://appcenter.intuit.com/Connect/Begin",
|
||||
:access_token_path => "/oauth/v1/get_access_token"
|
||||
})
|
||||
|
||||
|
||||
OAUTH_CONSUMER_KEY = Setting.plugin_redmine_qbo['settingsOAuthConsumerKey']
|
||||
OAUTH_CONSUMER_SECRET = Setting.plugin_redmine_qbo['settingsOAuthConsumerSecret']
|
||||
|
||||
$qb_oauth_consumer = OAuth::Consumer.new(OAUTH_CONSUMER_KEY, OAUTH_CONSUMER_SECRET, {
|
||||
:site => "https://oauth.intuit.com",
|
||||
:request_token_path => "/oauth/v1/get_request_token",
|
||||
:authorize_url => "https://appcenter.intuit.com/Connect/Begin",
|
||||
:access_token_path => "/oauth/v1/get_access_token"
|
||||
})
|
||||
|
||||
# Configure quickbooks-ruby-base to access our database
|
||||
Quickbooks::Base.configure do |c|
|
||||
c.persistent_token = 'qb_token'
|
||||
@@ -31,6 +30,7 @@ class Qbo < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def self.get_oauth_consumer
|
||||
# Quickbooks Config Info
|
||||
return $qb_oauth_consumer
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -30,8 +30,13 @@ class QboEmployee < ActiveRecord::Base
|
||||
qbo_employee.save!
|
||||
}
|
||||
end
|
||||
|
||||
#remove deleted employees
|
||||
where.not(employees.map(&:id)).destroy_all
|
||||
end
|
||||
|
||||
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.name = employee.display_name
|
||||
qbo_employee.id = employee.id
|
||||
qbo_employee.save!
|
||||
end
|
||||
end
|
||||
|
||||
@@ -35,6 +35,14 @@ class QboEstimate < ActiveRecord::Base
|
||||
where.not(estimates.map(&:id)).destroy_all
|
||||
end
|
||||
|
||||
def self.sync_by_id(id)
|
||||
estimate = get_base.service.fetch_by_id(id)
|
||||
qbo_estimate = QboEstimate.find_or_create_by(id: estimate.id)
|
||||
qbo_estimate.doc_number = estimate.doc_number
|
||||
qbo_estimate.id = estimate.id
|
||||
qbo_estimate.save!
|
||||
end
|
||||
|
||||
def self.update(id)
|
||||
# Update the item table
|
||||
estimate = get_base.service.fetch_by_id(id)
|
||||
|
||||
@@ -45,6 +45,14 @@ class QboInvoice < ActiveRecord::Base
|
||||
#where.not(invoices.map(&:id)).destroy_all
|
||||
end
|
||||
|
||||
def self.sync_by_id(id)
|
||||
invoice = get_base.service.fetch_by_id(id)
|
||||
qbo_invoice = find_or_create_by(id: invoice.id)
|
||||
qbo_invoice.doc_number = invoice.doc_number
|
||||
qbo_invoice.id = invoice.id
|
||||
qbo_invoice.save!
|
||||
end
|
||||
|
||||
def self.update(id)
|
||||
# Update the item table
|
||||
invoice = get_base.service.fetch_by_id(id)
|
||||
|
||||
@@ -12,6 +12,8 @@ class Vehicle < ActiveRecord::Base
|
||||
|
||||
unloadable
|
||||
|
||||
API_KEY = Setting.plugin_redmine_qbo['settingsEdmundsAPIKey']
|
||||
|
||||
belongs_to :customer
|
||||
has_many :issues, :foreign_key => 'vehicles_id'
|
||||
|
||||
@@ -19,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
|
||||
@@ -73,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
|
||||
@@ -91,7 +98,7 @@ class Vehicle < ActiveRecord::Base
|
||||
# returns the Edmunds decoder service
|
||||
def get_decoder
|
||||
#TODO API Code via Settings
|
||||
return decoder = Edmunds::Vin.new('2dheutzvhxs28dzukx5tgu47')
|
||||
return decoder = Edmunds::Vin.new(API_KEY)
|
||||
end
|
||||
|
||||
# decodes a vin and updates self
|
||||
@@ -119,4 +126,5 @@ class Vehicle < ActiveRecord::Base
|
||||
v = self.vin[0,11]
|
||||
return v.slice(0,8) + v.slice(9,11)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<h1>Customer Detail</h1>
|
||||
<h1>Customer #<%= @customer.id %></h1>
|
||||
<br/>
|
||||
<%= render :partial => 'customers/details', locals: {customer: @customer} %>
|
||||
<br/>
|
||||
|
||||
@@ -10,10 +10,27 @@ The above copyright notice and this permission notice shall be included in all c
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
-->
|
||||
|
||||
<!-- somewhere in your document include the Javascript -->
|
||||
<script type="text/javascript" src="https://appcenter.intuit.com/Content/IA/intuit.ipp.anywhere.js"></script>
|
||||
|
||||
<!-- configure the Intuit object: 'grantUrl' is a URL in your application which kicks off the flow, see below -->
|
||||
<script>
|
||||
intuit.ipp.anywhere.setup({menuProxy: '/path/to/blue-dot', grantUrl: '<%= qbo_authenticate_url %>'});
|
||||
</script>
|
||||
|
||||
<table >
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>OAuth Consumer Key</th>
|
||||
<th>Edmunds API Key</th>
|
||||
<td>
|
||||
<input type="text" style="width:350px" id="settingsEdmundsAPIKey"
|
||||
value="<%= settings['settingsEdmundsAPIKey'] %>"
|
||||
name="settings[settingsEdmundsAPIKey]" >
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Intuit QBO OAuth Consumer Key</th>
|
||||
<td>
|
||||
<input type="text" style="width:350px" id="settingsOAuthConsumerKey"
|
||||
value="<%= settings['settingsOAuthConsumerKey'] %>"
|
||||
@@ -22,21 +39,22 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>OAuth Consumer Secret</th>
|
||||
<th>Intuit QBO OAuth Consumer Secret</th>
|
||||
<td>
|
||||
<input type="text" style="width:350px" id="settingsOAuthConsumerSecret"
|
||||
value="<%= settings['settingsOAuthConsumerSecret'] %>"
|
||||
name="settings[settingsOAuthConsumerSecret]" >
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br/>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
|
||||
<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>
|
||||
@@ -54,5 +72,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
<br/>
|
||||
Note: You need to authenticate after saving your key and secret above
|
||||
<br/>
|
||||
|
||||
<%= link_to "Authenticate", qbo_authenticate_path, :method => :get %>
|
||||
|
||||
<!-- this will display a button that the user clicks to start the flow -->
|
||||
<ipp:connectToIntuit></ipp:connectToIntuit>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -44,6 +44,8 @@
|
||||
<tr>
|
||||
<td/>
|
||||
<td>
|
||||
|
||||
<%= button_to "New Issue", new_issue_path(:vehicle_id => vehicle.id, :customer_id => vehicle.customer.id), method: :get%>
|
||||
<%= button_to "Edit", edit_vehicle_path(vehicle), method: :get%>
|
||||
<%= button_to "Delete", vehicle, method: :delete, data: {confirm: "You sure?"} %>
|
||||
</td>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' %>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<h1>Customer Vehicle</h1>
|
||||
<h1>Vehicle #<%=@vehicle.id%> </h1>
|
||||
<br/>
|
||||
|
||||
<div style="text-align: left; width:90%;">
|
||||
|
||||
@@ -17,5 +17,8 @@ get 'qbo/oauth_callback', :to => 'qbo#oauth_callback'
|
||||
get 'qbo/sync', :to => 'qbo#sync'
|
||||
get 'qbo/estimate/:id', :to => 'estimate#show', as: :estimate
|
||||
get 'qbo/invoice/:id', :to => 'invoice#show', as: :invoice
|
||||
|
||||
post 'qbo/webhook', :to => 'qbo#qbo_webhook'
|
||||
|
||||
resources :vehicles
|
||||
resources :customers
|
||||
|
||||
3
init.rb
3
init.rb
@@ -25,7 +25,7 @@ Redmine::Plugin.register :redmine_qbo do
|
||||
name 'Redmine Quickbooks Online plugin'
|
||||
author 'Rick Barrette'
|
||||
description 'This is a plugin for Redmine to intergrate with Quickbooks Online to allow for seamless intergration CRM and invoicing of completed issues'
|
||||
version '0.0.6'
|
||||
version '0.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
|
||||
|
||||
@@ -26,7 +26,7 @@ module IssuePatch
|
||||
belongs_to :qbo_item, primary_key: :id
|
||||
belongs_to :qbo_estimate, primary_key: :id
|
||||
belongs_to :qbo_invoice, primary_key: :id
|
||||
belongs_to :vehicle
|
||||
belongs_to :vehicle, primary_key: :id
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -16,14 +16,15 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener
|
||||
f = context[:form]
|
||||
|
||||
# Check to see if there is a quickbooks user attached to the issue
|
||||
@selected_customer = context[:issue].customer ? context[:issue].customer.id : nil
|
||||
selected_customer = context[:issue].customer ? context[:issue].customer.id : nil
|
||||
selected_item = context[:issue].qbo_item ? context[:issue].qbo_item.id : nil
|
||||
selected_invoice = context[:issue].qbo_invoice ? context[:issue].qbo_invoice.id : nil
|
||||
selected_estimate = context[:issue].qbo_estimate ? context[:issue].qbo_estimate.id : nil
|
||||
selected_vehicle = context[:issue].vehicles_id ? context[:issue].vehicles_id : nil
|
||||
|
||||
# Load customer information without callbacks
|
||||
@customer = Customer.find_by_id(@selected_customer) if @selected_customer
|
||||
@select_customer = f.select :customer_id, Customer.all.pluck(:name, :id).sort, :selected => @selected_customer, include_blank: true
|
||||
customer = Customer.find_by_id(selected_customer) if selected_customer
|
||||
select_customer = f.select :customer_id, Customer.all.pluck(:name, :id).sort, :selected => selected_customer, include_blank: true
|
||||
|
||||
# Generate the drop down list of quickbooks items
|
||||
select_item = f.select :qbo_item_id, QboItem.all.pluck(:name, :id).sort, :selected => selected_item, include_blank: true
|
||||
@@ -34,10 +35,14 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener
|
||||
# Generate the drop down list of quickbooks extimates
|
||||
select_estimate = f.select :qbo_estimate_id, QboEstimate.all.pluck(:doc_number, :id).sort! {|x, y| y <=> x}, :selected => selected_estimate, include_blank: true
|
||||
|
||||
vehicles = @customer.vehicles.pluck(:name, :id).sort! if context[:issue].customer
|
||||
vehicles = Vehicle.all.order(:name) if not vehicles
|
||||
vehicle = f.select :vehicles_id, vehicles, include_blank: true, :selected => vehicle
|
||||
if context[:issue].customer
|
||||
vehicles = customer.vehicles.pluck(:name, :id).sort!
|
||||
else
|
||||
vehicles = Vehicle.all.order(:name).pluck(:name, :id)
|
||||
end
|
||||
|
||||
return "<p>#{@select_customer}</p> <p>#{select_item}</p> <p>#{select_invoice}</p> <p>#{select_estimate}</p> <p>#{vehicle}</p>"
|
||||
vehicle = f.select :vehicles_id, vehicles, :selected => selected_vehicle, include_blank: true
|
||||
|
||||
return "<p>#{select_customer}</p> <p>#{select_item}</p> <p>#{select_invoice}</p> <p>#{select_estimate}</p> <p>#{vehicle}</p>"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -55,11 +55,14 @@ class IssuesSaveHookListener < Redmine::Hook::ViewListener
|
||||
# Called After Issue Saved
|
||||
def controller_issues_edit_after_save(context={})
|
||||
issue = context[:issue]
|
||||
employee_id = issue.assigned_to.qbo_employee_id
|
||||
|
||||
if issue.assigned_to
|
||||
employee_id = issue.assigned_to.qbo_employee_id
|
||||
|
||||
# Check to see if we have registered with QBO and if the issue is closed.
|
||||
# If so then we need to create a new billable time activity for the customer
|
||||
bill_time(issue, employee_id) if Qbo.first && issue.customer && issue.qbo_item && employee_id && issue.status.is_closed?
|
||||
# Check to see if we have registered with QBO and if the issue is closed.
|
||||
# If so then we need to create a new billable time activity for the customer
|
||||
bill_time(issue, employee_id) if Qbo.first && issue.customer && issue.qbo_item && employee_id && issue.status.is_closed?
|
||||
end
|
||||
end
|
||||
|
||||
# Create billable time entries
|
||||
|
||||
@@ -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}</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\">
|
||||
|
||||
@@ -51,7 +51,7 @@ module IssuesPdfHelperPatch
|
||||
vin = v ? v.vin : nil
|
||||
notes = v ? v.notes : nil
|
||||
left << [l(:field_vehicles), vehicle]
|
||||
left << [l(:field_vin), vin]
|
||||
left << [l(:field_vin), vin.gsub(/(.{9})/, '\1 ')]
|
||||
#left << [l(:field_notes), notes]
|
||||
|
||||
right = []
|
||||
|
||||
@@ -14,7 +14,7 @@ class UsersShowHookListener < Redmine::Hook::ViewListener
|
||||
def view_users_form(context={})
|
||||
|
||||
# Update the users
|
||||
QboEmployee.update_all
|
||||
#QboEmployee.update_all
|
||||
|
||||
# Check to see if there is a quickbooks user attached to the issue
|
||||
@selected = context[:user].qbo_employee.id if context[:user].qbo_employee
|
||||
|
||||
Reference in New Issue
Block a user