mirror of
https://github.com/rickbarrette/redmine_qbo.git
synced 2025-11-09 01:14:23 -05:00
Compare commits
178 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eb039368bb | |||
| 0dea5917a7 | |||
| a8ccde6c81 | |||
| 787ae1b8df | |||
| 276c89d4ac | |||
| 9a395ee25c | |||
| 475c86eabe | |||
| 259737a488 | |||
| 362cb77381 | |||
| 8cfab17136 | |||
| f0018ab87d | |||
| 8f87eb3e60 | |||
| 2b093903b3 | |||
| 05017dcc4f | |||
| 0e9b5fa17a | |||
| dd335aff71 | |||
| 0f61bf54ce | |||
| 14cb22d743 | |||
| 702ab5013e | |||
| 235e2c6e7b | |||
| 2e89a60d63 | |||
| 3d5ef2cd8a | |||
| de8eff9bd2 | |||
| a9561d1694 | |||
| aa33de00d2 | |||
| ffc589fe80 | |||
| 229e4e8d39 | |||
| d6dda2cdd6 | |||
| b8a101fddb | |||
| c8a875b301 | |||
| df8e3a7465 | |||
| d91a6e3939 | |||
| 48a2d683dd | |||
| 44bf42c548 | |||
| d34e6cb0fd | |||
| d8e7356ca3 | |||
| 60e6dbaa6f | |||
| 47e5a7d0e4 | |||
| 9fa2165907 | |||
| 7385d7018c | |||
| 6124c1b307 | |||
| b73535c6da | |||
| 1581023656 | |||
| 0d21e2967d | |||
| 0dc7d83fbe | |||
| cd18067384 | |||
| 6c99f7095c | |||
| eeaafce427 | |||
| b7cb27b5da | |||
| 3e6286da7c | |||
| 30ceea7fd5 | |||
| de9e973fd9 | |||
| 49a3bd5790 | |||
| f1745930b1 | |||
| d9beda8171 | |||
| 65f343fb74 | |||
| 892bd65fac | |||
| 0251191844 | |||
| 65f6f52252 | |||
| 4d94308bcc | |||
| 7dcd8b24d2 | |||
| 11da8e7a43 | |||
| 56c895388d | |||
| 8ec9567f15 | |||
| be3dd0d131 | |||
| 92f51d9884 | |||
| c4904a0ac2 | |||
| 0d87e5fb21 | |||
| d38e3e1702 | |||
| fec59a7495 | |||
| a3b5ad0cb0 | |||
| bf21451819 | |||
| c6d3d9673b | |||
| 3f5334a92d | |||
| bde7b83752 | |||
| c788e5724a | |||
| 295cd12f9d | |||
| 4a432481d9 | |||
| 4a37d83694 | |||
| 15a2a16379 | |||
| 18fc7a6c8c | |||
| 7aba8cdce3 | |||
| 382e6675f1 | |||
| 116d6896f4 | |||
| c9ced52112 | |||
| 01b4bb4e53 | |||
| a266da2cd7 | |||
| 578e7ba807 | |||
| b923e15d46 | |||
| 1310d1e63e | |||
| a8e1e8429c | |||
| 1b54b40f6c | |||
| 6d7530922d | |||
| 23698986b1 | |||
| 1b4c377940 | |||
| d33c0c9aa5 | |||
| 09d8c0024f | |||
| 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 |
@@ -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
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
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
|
## 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 |
@@ -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
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
class QboController < ApplicationController
|
class QboController < ApplicationController
|
||||||
unloadable
|
unloadable
|
||||||
|
|
||||||
|
require 'openssl'
|
||||||
|
|
||||||
include AuthHelper
|
include AuthHelper
|
||||||
|
|
||||||
before_filter :require_user, :except => :qbo_webhook
|
before_filter :require_user, :except => :qbo_webhook
|
||||||
@@ -34,7 +36,6 @@ class QboController < ApplicationController
|
|||||||
def authenticate
|
def authenticate
|
||||||
callback = qbo_oauth_callback_url
|
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)
|
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
|
||||||
@@ -43,8 +44,6 @@ 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])
|
|
||||||
# If Rails >= 4.1 you need to do this =>
|
|
||||||
at = Marshal.load(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...
|
||||||
@@ -62,42 +61,57 @@ class QboController < ApplicationController
|
|||||||
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
|
end
|
||||||
|
|
||||||
# Quickbooks Webhook Callback
|
# Quickbooks Webhook Callback
|
||||||
def qbo_webhook
|
def qbo_webhook
|
||||||
|
|
||||||
if request.headers['Content-Type'] == 'application/json'
|
# check the payload
|
||||||
data = JSON.parse(request.body.read)
|
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
|
else
|
||||||
# application/x-www-form-urlencoded
|
# application/x-www-form-urlencoded
|
||||||
data = params.as_json
|
data = params.as_json
|
||||||
end
|
end
|
||||||
|
# Process the information
|
||||||
entities = data['eventNotifications'][0]['dataChangeEvent']['entities']
|
entities = data['eventNotifications'][0]['dataChangeEvent']['entities']
|
||||||
|
|
||||||
entities.each do |entity|
|
entities.each do |entity|
|
||||||
if entity['name'].eql? "Customer"
|
id = entity['id'].to_i
|
||||||
Customer.sync_by_id(entity['id'].to_i)
|
name = entity['name']
|
||||||
end
|
|
||||||
|
|
||||||
if entity['name'].eql? "Invoice"
|
# TODO rename all other models!
|
||||||
QboInvoice.sync_by_id(entity['id'].to_i)
|
name.prepend("Qbo") if not name.eql? "Customer"
|
||||||
end
|
|
||||||
|
|
||||||
if entity['name'].eql? "Estimate"
|
# Magicly initialize the correct class
|
||||||
QboEstimate.sync_by_id(entity['id'].to_i)
|
obj = name.constantize
|
||||||
end
|
|
||||||
|
|
||||||
if entity['name'].eql? "Employee"
|
# for merge events
|
||||||
QboEmployee.sync_by_id(entity['id'].to_i)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
# The webhook doesn't require a response but let's make sure
|
# Record that last time we updated
|
||||||
# we don't send anything
|
Qbo.update_time_stamp
|
||||||
|
|
||||||
|
# The webhook doesn't require a response but let's make sure we don't send anything
|
||||||
render :nothing => true
|
render :nothing => true
|
||||||
|
else
|
||||||
|
render nothing: true, status: 400
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -110,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
|
||||||
|
|||||||
@@ -18,13 +18,29 @@ 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[:customer_id]
|
||||||
|
begin
|
||||||
|
@vehicles = Customer.find_by_id(params[:customer_id]).vehicles.paginate(:page => params[:page])
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
render_404
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
def new
|
def new
|
||||||
@vehicle = Vehicle.new
|
@vehicle = Vehicle.new
|
||||||
@customers = Customer.all.order(:name)
|
@customers = Customer.all.order(:name)
|
||||||
|
if params[:customer]
|
||||||
|
@customer = Customer.find_by_id(params[:customer])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# create a new vehicle
|
# create a new vehicle
|
||||||
@@ -87,4 +103,26 @@ class VehiclesController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# returns a dynamic list of vehicles owned by a customer
|
||||||
|
def update_vehicles
|
||||||
|
@vehicles = Customer.find_by_id(params[:customer_id].to_i).vehicles
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { render(:text => "not implemented") }
|
||||||
|
format.js
|
||||||
|
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
|
||||||
|
|||||||
@@ -140,6 +140,11 @@ 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
|
# proforms a bruteforce sync operation
|
||||||
# This needs to be simplified
|
# This needs to be simplified
|
||||||
def self.sync_by_id(id)
|
def self.sync_by_id(id)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ class QboEmployee < ActiveRecord::Base
|
|||||||
|
|
||||||
def self.sync_by_id(id)
|
def self.sync_by_id(id)
|
||||||
employee = get_base.service.fetch_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.name = employee.display_name
|
||||||
qbo_employee.id = employee.id
|
qbo_employee.id = employee.id
|
||||||
|
|||||||
@@ -46,11 +46,23 @@ class QboInvoice < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.sync_by_id(id)
|
def self.sync_by_id(id)
|
||||||
|
#update the information in the database
|
||||||
invoice = get_base.service.fetch_by_id(id)
|
invoice = get_base.service.fetch_by_id(id)
|
||||||
qbo_invoice = find_or_create_by(id: invoice.id)
|
qbo_invoice = find_or_create_by(id: invoice.id)
|
||||||
qbo_invoice.doc_number = invoice.doc_number
|
qbo_invoice.doc_number = invoice.doc_number
|
||||||
qbo_invoice.id = invoice.id
|
qbo_invoice.id = invoice.id
|
||||||
qbo_invoice.save!
|
qbo_invoice.save!
|
||||||
|
|
||||||
|
# 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|
|
||||||
|
i = Issue.find_by_id(issue.to_i)
|
||||||
|
i.qbo_invoice = QboInvoice.find_by_id(invoice.id.to_i)
|
||||||
|
i.save!
|
||||||
|
}
|
||||||
|
end
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.update(id)
|
def self.update(id)
|
||||||
|
|||||||
@@ -21,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
|
||||||
@@ -75,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
|
||||||
@@ -121,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
|
||||||
|
|||||||
@@ -31,7 +31,6 @@
|
|||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td/>
|
|
||||||
<td>
|
<td>
|
||||||
<%= button_to "Edit Customer", edit_customer_path(customer), method: :get%>
|
<%= button_to "Edit Customer", edit_customer_path(customer), method: :get%>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
<h1>Customer Detail</h1>
|
<h1>Customer #<%= @customer.id %></h1>
|
||||||
<br/>
|
<br/>
|
||||||
|
<h2>Details:</h2>
|
||||||
<%= render :partial => 'customers/details', locals: {customer: @customer} %>
|
<%= render :partial => 'customers/details', locals: {customer: @customer} %>
|
||||||
<br/>
|
<br/>
|
||||||
|
<h2>Vehicles:</h2>
|
||||||
<%= render :partial => 'vehicles/list' %>
|
<%= render :partial => 'vehicles/list' %>
|
||||||
|
<%= button_to "New Vehicle", new_customer_vehicle_path(@customer), method: :get %>
|
||||||
<br/>
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<h2>Issues:</h2>
|
||||||
<%= render :partial => 'issues/list_simple', locals: {issues: @issues} %>
|
<%= render :partial => 'issues/list_simple', locals: {issues: @issues} %>
|
||||||
|
|||||||
@@ -47,6 +47,15 @@ intuit.ipp.anywhere.setup({menuProxy: '/path/to/blue-dot', grantUrl: '<%= qbo_au
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</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>
|
<tr>
|
||||||
<th>Token Expires At</th>
|
<th>Token Expires At</th>
|
||||||
<td><%= if Qbo.exists? then Qbo.first.token_expires_at end %>
|
<td><%= if Qbo.exists? then Qbo.first.token_expires_at end %>
|
||||||
@@ -63,6 +72,36 @@ intuit.ipp.anywhere.setup({menuProxy: '/path/to/blue-dot', grantUrl: '<%= qbo_au
|
|||||||
<br/>
|
<br/>
|
||||||
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/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
<!-- this will display a button that the user clicks to start the flow -->
|
<!-- this will display a button that the user clicks to start the flow -->
|
||||||
<ipp:connectToIntuit></ipp:connectToIntuit>
|
<ipp:connectToIntuit></ipp:connectToIntuit>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<b>Customer Count:</b> <%= Customer.count%>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<b>Item Count:</b> <%= QboItem.count %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<b>Employee Count:</b> <%= QboEmployee.count %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<b>Invoice Count:</b> <%= QboInvoice.count %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<b>Estimate Count:</b> <%= QboEstimate.count %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<b>Last Sync: </b> <%= Qbo.last_sync %> <%= link_to " Sync Now", qbo_sync_path %>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>Customer</th>
|
<th>Customer</th>
|
||||||
<td><%= vehicle.customer.name %></td>
|
<td><%= link_to vehicle.customer.name, customer_path(vehicle.customer) %></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div class="clearfix">
|
<div class="clearfix">
|
||||||
Customer:
|
Customer:
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<%= f.collection_select :customer_id, @customers, :id, :name, include_blank: true, :selected => @customer, :required => true%>
|
<%= f.collection_select :customer_id, @customers, :id, :name, include_blank: true, :selected => params[:customer_id], :required => true%>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
|
<% if @vehicles.present? %>
|
||||||
|
|
||||||
<% @vehicles.each do |vehicle| %>
|
<% @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 %>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
<%= vehicle.vin %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br/>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<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 %>
|
||||||
|
|||||||
1
app/views/vehicles/_vehicle.html.erb
Normal file
1
app/views/vehicles/_vehicle.html.erb
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<option value="<%= vehicle.id %>"><%= vehicle.to_s.titleize %></option>
|
||||||
@@ -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' %>
|
||||||
|
|||||||
@@ -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%;">
|
||||||
|
|||||||
1
app/views/vehicles/update_vehicles.js.erb
Normal file
1
app/views/vehicles/update_vehicles.js.erb
Normal file
@@ -0,0 +1 @@
|
|||||||
|
$("#issue_vehicles_id").empty().append("<%= escape_javascript(render(:partial => @vehicles)) %>")
|
||||||
16
assets/javascripts/vehicles.js
Normal file
16
assets/javascripts/vehicles.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Place all the behaviors and hooks related to the matching controller here.
|
||||||
|
# All this logic will automatically be available in application.js.
|
||||||
|
# You can use CoffeeScript in this file: http://coffeescript.org/
|
||||||
|
|
||||||
|
$ ->
|
||||||
|
$(document).on 'change', '#issue_customer_id', (evt) ->
|
||||||
|
$.ajax 'update_vehicles',
|
||||||
|
type: 'GET'
|
||||||
|
dataType: 'script'
|
||||||
|
data: {
|
||||||
|
customer_id: $("#issue_customer_id option:selected").val()
|
||||||
|
}
|
||||||
|
error: (jqXHR, textStatus, errorThrown) ->
|
||||||
|
console.log("AJAX Error: #{textStatus}")
|
||||||
|
success: (data, textStatus, jqXHR) ->
|
||||||
|
console.log("Dynamic vehicle select OK!")
|
||||||
@@ -20,5 +20,11 @@ get 'qbo/invoice/:id', :to => 'invoice#show', as: :invoice
|
|||||||
|
|
||||||
post 'qbo/webhook', :to => 'qbo#qbo_webhook'
|
post 'qbo/webhook', :to => 'qbo#qbo_webhook'
|
||||||
|
|
||||||
|
#ajax
|
||||||
|
get "update_vehicles" => 'vehicles#update_vehicles', as: 'update_vehicles'
|
||||||
|
|
||||||
|
resources :customers do
|
||||||
|
resources :vehicles
|
||||||
|
end
|
||||||
|
|
||||||
resources :vehicles
|
resources :vehicles
|
||||||
resources :customers
|
|
||||||
|
|||||||
11
init.rb
11
init.rb
@@ -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.7'
|
version '0.2.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'
|
||||||
@@ -46,7 +46,12 @@ Redmine::Plugin.register :redmine_qbo do
|
|||||||
WillPaginate.per_page = 10
|
WillPaginate.per_page = 10
|
||||||
|
|
||||||
# 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, :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? }
|
||||||
|
menu :top_menu, :vehicles, { :controller => :vehicles, :action => :index }, :caption => 'Vehicles', :if => Proc.new { User.current.logged? }
|
||||||
|
|
||||||
|
menu :application_menu, :new_customer, { :controller => :customers, :action => :new }, :caption => 'New Customer', :if => Proc.new { User.current.logged? }
|
||||||
|
|
||||||
|
permission :customers, { :customers => [:index, :new] }, :public => false
|
||||||
|
menu :project_menu, :customers, { :controller => 'customers', :action => 'new' }, :caption => 'New Customer', :after => :activity, :param => :project_id
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,6 +10,11 @@
|
|||||||
|
|
||||||
class IssuesFormHookListener < Redmine::Hook::ViewListener
|
class IssuesFormHookListener < Redmine::Hook::ViewListener
|
||||||
|
|
||||||
|
# Load the javascript
|
||||||
|
def view_layouts_base_html_head(context = {})
|
||||||
|
javascript_include_tag 'vehicles', :plugin => 'redmine_qbo'
|
||||||
|
end
|
||||||
|
|
||||||
# Edit Issue Form
|
# Edit Issue Form
|
||||||
# Show a dropdown for quickbooks contacts
|
# Show a dropdown for quickbooks contacts
|
||||||
def view_issues_form_details_bottom(context={})
|
def view_issues_form_details_bottom(context={})
|
||||||
|
|||||||
@@ -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.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>
|
||||||
|
|
||||||
<div class=\"vehicle_notes attribute\">
|
<div class=\"vehicle_notes attribute\">
|
||||||
|
|||||||
Reference in New Issue
Block a user