mirror of
https://github.com/rickbarrette/redmine_qbo.git
synced 2025-11-08 08:54:23 -05:00
Compare commits
245 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 | |||
| 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,13 +18,29 @@ class VehiclesController < ApplicationController
|
||||
|
||||
# display a list of all vehicles
|
||||
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
|
||||
|
||||
# return an HTML form for creating a new vehicle
|
||||
def new
|
||||
@vehicle = Vehicle.new
|
||||
@customers = Customer.all.order(:name)
|
||||
if params[:customer]
|
||||
@customer = Customer.find_by_id(params[:customer])
|
||||
end
|
||||
end
|
||||
|
||||
# create a new vehicle
|
||||
@@ -86,5 +102,27 @@ class VehiclesController < ApplicationController
|
||||
render_404
|
||||
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
|
||||
|
||||
@@ -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,26 @@ class QboInvoice < ActiveRecord::Base
|
||||
#where.not(invoices.map(&:id)).destroy_all
|
||||
end
|
||||
|
||||
def self.sync_by_id(id)
|
||||
#update the information in the database
|
||||
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!
|
||||
|
||||
# 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
|
||||
|
||||
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
|
||||
|
||||
@@ -30,8 +30,7 @@
|
||||
<td><%= customer.issues.count %></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td/>
|
||||
<tr>
|
||||
<td>
|
||||
<%= button_to "Edit Customer", edit_customer_path(customer), method: :get%>
|
||||
</td>
|
||||
|
||||
@@ -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,7 +1,12 @@
|
||||
<h1>Customer Detail</h1>
|
||||
<h1>Customer #<%= @customer.id %></h1>
|
||||
<br/>
|
||||
<h2>Details:</h2>
|
||||
<%= render :partial => 'customers/details', locals: {customer: @customer} %>
|
||||
<br/>
|
||||
<h2>Vehicles:</h2>
|
||||
<%= render :partial => 'vehicles/list' %>
|
||||
<%= button_to "New Vehicle", new_customer_vehicle_path(@customer), method: :get %>
|
||||
<br/>
|
||||
<br/>
|
||||
<h2>Issues:</h2>
|
||||
<%= render :partial => 'issues/list_simple', locals: {issues: @issues} %>
|
||||
|
||||
@@ -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,36 @@ 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 %>
|
||||
<br/>
|
||||
|
||||
<!-- this will display a button that the user clicks to start the flow -->
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<tr>
|
||||
<th>Customer</th>
|
||||
<td><%= vehicle.customer.name %></td>
|
||||
<td><%= link_to vehicle.customer.name, customer_path(vehicle.customer) %></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
@@ -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>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="clearfix">
|
||||
Customer:
|
||||
<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>
|
||||
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
<% @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 %>
|
||||
|
||||
<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
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>
|
||||
<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%;">
|
||||
|
||||
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!")
|
||||
@@ -17,5 +17,14 @@ 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'
|
||||
|
||||
#ajax
|
||||
get "update_vehicles" => 'vehicles#update_vehicles', as: 'update_vehicles'
|
||||
|
||||
resources :customers do
|
||||
resources :vehicles
|
||||
end
|
||||
|
||||
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'
|
||||
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.2.0'
|
||||
url 'https://github.com/rickbarrette/redmine_qbo'
|
||||
author_url 'http://rickbarrette.org'
|
||||
settings :default => {'empty' => true}, :partial => 'qbo/settings'
|
||||
@@ -46,7 +46,12 @@ Redmine::Plugin.register :redmine_qbo do
|
||||
WillPaginate.per_page = 10
|
||||
|
||||
# Register QBO top menu item
|
||||
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, :qbo, { :controller => :qbo, :action => :index }, :caption => 'Quickbooks', :if => Proc.new { User.current.admin? }
|
||||
menu :top_menu, :customers, { :controller => :customers, :action => :index }, :caption => 'Customers', :if => Proc.new { User.current.logged? }
|
||||
menu :top_menu, :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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,20 +10,26 @@
|
||||
|
||||
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
|
||||
# Show a dropdown for quickbooks contacts
|
||||
def view_issues_form_details_bottom(context={})
|
||||
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 +40,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