mirror of
https://github.com/rickbarrette/redmine_qbo.git
synced 2025-11-08 08:54:23 -05:00
Compare commits
374 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fee0548899 | |||
| 5ddb45ba24 | |||
| 98c965c607 | |||
| 6e1d23af4e | |||
| 8da45bd348 | |||
| 620c4b395e | |||
| d75208a75a | |||
| 4f08825fb1 | |||
| 865470fc11 | |||
| d827936c85 | |||
| fcc614ff54 | |||
| b7abe2610e | |||
| d916464423 | |||
| 32164157c2 | |||
| ce4b957c8e | |||
| 3735629073 | |||
| d41d618be5 | |||
| d97f3cb2a3 | |||
| dcf31116b4 | |||
| 219141eeee | |||
| 765b5b6024 | |||
| 4efca93d03 | |||
| e2a4908420 | |||
| 183b8d17e6 | |||
| ed2b84c697 | |||
| 59410e6d77 | |||
| a134d1b601 | |||
| 56161f12d0 | |||
| 146dbb137c | |||
| 4f23439dac | |||
| 8b33aa6f6a | |||
| 4f72a8e5ad | |||
| fae4782ef0 | |||
| 37ea01de8c | |||
| 2c53155207 | |||
| dbe585ca2a | |||
| 6434092306 | |||
| 8720176b57 | |||
| 5bdf313fa5 | |||
| 4527e74d29 | |||
| 8e6e543c5b | |||
| dbbd4a2593 | |||
| 6b55f92454 | |||
| fba9645932 | |||
| 0cc867b410 | |||
| 40f738d976 | |||
| 82ecaae156 | |||
| 8d4ac896fa | |||
| 7c6246a539 | |||
| ce88cdd258 | |||
| f179b04af1 | |||
| 0070264d51 | |||
| 22db89a6d9 | |||
| 78dfad9875 | |||
| 6a55138f7c | |||
| f95ee10290 | |||
| adb864c9ca | |||
| f3f92e48e0 | |||
| 87a9d978c2 | |||
| 9981b9ef70 | |||
| 94f10dc9cd | |||
| e3fdc070df | |||
| 4cae63d02b | |||
| d856ceeec7 | |||
| d461570b14 | |||
| 924e0d7bc9 | |||
| 6b3280edaf | |||
| 722d66f130 | |||
| 16083a6f30 | |||
| 1b6fe073dc | |||
| 9f6103ad89 | |||
| 8a67cdf37c | |||
| 9b444d638b | |||
| abab81158f | |||
| 26c0716d35 | |||
| 7f7c724ef1 | |||
| f083e8257a | |||
| 4cb588e992 | |||
| 245d2b49a2 | |||
| e888bd0d38 | |||
| 6cfd56ed01 | |||
| 647af6f87a | |||
| 0e2f9b1031 | |||
| 86ee8908b3 | |||
| a0618b51ba | |||
| 272369ba4c | |||
| 6319c24b5c | |||
| a3180a318c | |||
| aeb890cbed | |||
| 4a94ca1d17 | |||
| 63d845ed97 | |||
| cb67cab974 | |||
| df94564d9b | |||
| 540e008f68 | |||
| c85d3ba8d5 | |||
| 1df335ed16 | |||
| 7ca5076477 | |||
| 6605946e62 | |||
| ba45c776ae | |||
| dcf17052b6 | |||
| 8b9acccb8a | |||
| 2afed176f0 | |||
| 577788110e | |||
| d251ea066f | |||
| 609e65b7cd | |||
| 6c2dd29a57 | |||
| 98896ac0a6 | |||
| 19ba3abade | |||
| 3347490b82 | |||
| d1457b09be | |||
| 1b71439f19 | |||
| 55c09d7e9d | |||
| 8429c29c30 | |||
| 10f8a7e124 | |||
| e8763ea923 | |||
| e5601030b1 | |||
| ad8d15203e | |||
| 3b4e55727c | |||
| 5dc2921d40 | |||
| 0c4ef8abe9 | |||
| 8165523acf | |||
| 7d1e9bb838 | |||
| 0d9140958f | |||
| 16ca8177e9 | |||
| a0e9061a8f | |||
| a56c01fe6d | |||
| 1cb9639f03 | |||
| 7af89db442 | |||
| fae815fd7f | |||
| 1b533d6dd8 | |||
| bc38361348 | |||
| a0a365c10e | |||
| 162c76471b | |||
| 328a50be47 | |||
| 7cc84277c6 | |||
| fbac6b6d77 | |||
| 33b5ac8c87 | |||
| 74f179d64b | |||
| 3cef188ff3 | |||
| 1b767f78d2 | |||
| f380969082 | |||
| df6acde327 | |||
| 0318ffaa10 | |||
| 51c1b38197 | |||
| d96bd1a3f4 | |||
| b6e43b5837 | |||
| 62fa98a656 | |||
| bb5a080f25 | |||
| 2afa9e4166 | |||
| b489a2771f | |||
| 0495ac1bc5 | |||
| e3b49358bb | |||
| 08b365e69e | |||
| 5d4c49c85d | |||
| 5bc9ca34a4 | |||
| 630a1d144b | |||
| 491684f7df | |||
| 9a28247b7f | |||
| 5a91e21d45 | |||
| f6f1ca4c04 | |||
| 8daa10888f | |||
| 82449642d3 | |||
| 06ad2d6971 | |||
| 4c4ca67be8 | |||
| b994f7c142 | |||
| 97b483031d | |||
| c624c20354 | |||
| 695e3bd24c | |||
| ce4883cd4c | |||
| 49e19cb73f | |||
| 31bb242a61 | |||
| 2e533e8798 | |||
| e70d0c8d17 | |||
| d96ecd2b66 | |||
| a588ac19a6 | |||
| 2bc8ec4f56 | |||
| 578a5a1228 | |||
| 416595ffea | |||
| d3be59fbc5 | |||
| 3c3b4da313 | |||
| 501834419b | |||
| 75b25a9e44 | |||
| 15912b2197 | |||
| b093f6136e | |||
| 33db0a53ba | |||
| 7237d2e643 | |||
| 77b1c1dbef | |||
| f3090bd1a4 | |||
| 89a131018c | |||
| de17fb80d1 | |||
| a5f1d15156 | |||
| d3463ce41b | |||
| 4503150b02 | |||
| 36cd00822e | |||
| d285344a61 | |||
| 8418dfc0b5 | |||
| 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 |
1
Gemfile
1
Gemfile
@@ -7,3 +7,4 @@ gem 'oauth'
|
||||
gem 'roxml'
|
||||
gem 'edmunds_vin'
|
||||
gem 'will_paginate', '~> 3.1.0'
|
||||
gem 'rails4-autocomplete'
|
||||
|
||||
53
README.md
53
README.md
@@ -1,25 +1,39 @@
|
||||
#Redmine Quickbooks Online
|
||||
|
||||
A simple plugin for Redmine to connect to Quickbooks Online
|
||||
A plugin for Redmine to connect to Quickbooks Online
|
||||
|
||||
The goal of this project is to allow redmine to connect with Quickbooks Online to create time activity entries for completed work when an issue is closed.
|
||||
The goal of this project is to allow Redmine to connect with Quickbooks Online to create `Time Activity Entries` for completed work when an Issue is closed.
|
||||
|
||||
`Note: This project is under heavy development. Currently the initial functionality goal has been meet, however I am still working on adding other features. Tags should be stable`
|
||||
`Note: Although the core functionality is complete, this project is still under heavy development. I am still working on refining everthing and adding other features. Tags should be stable`
|
||||
|
||||
####How it works
|
||||
* Issues can be assigned to a QBO Customer and QBO Service Item via drop down in issues form
|
||||
- The `QBO Employee` for the issue is assigned via the assigned redmine user
|
||||
- IF an `Issue` has been assined a `QBO Customer`, `QBO Service Item` & `QBO Employee` when an `Issue` is closed the following will happen:
|
||||
- A new `QBO Time Activity` agaist the `QBO Customer` will be created using the total spent hours logged agaist an `Issue`.
|
||||
- The rate will be the set via the `QBO Service Item` price
|
||||
* `Issues` with the Tracker `Quote` will generate an estimate based on the estimated hours and `QBO Service Item` cost.
|
||||
- Needs to have a `QBO Customer` & `QBO Service Item` Assiged
|
||||
* Users will be assigned a `QBO Employee` via a drop down in the user admistration page.
|
||||
####Features
|
||||
* Issues can be assigned to a `Customer` via drop down in the edit Issue form
|
||||
* The `Employee` for the Issue is assigned via the assigned Redmine User
|
||||
- This is set via a drop down in the user admistration page.
|
||||
* IF an `Issue` has been assined a `Customer` when an Issue is closed the following will happen:
|
||||
- A new `Time Activity` will be created for the `Customer` assinged to the issue for each Redmine Time Entery.
|
||||
+ Time Entries will be totalled up by Activity name. This will allow billing for diffrent activities without having to create seperate Issues.
|
||||
+ The Time Activity names are used to lookup `Items` in Quickbooks.
|
||||
+ IF there isn'tany Items that match the Activity name it will be skipped, and will not be billed to the `Customer`
|
||||
- Labor Rates are set by the `Item` in Quickbooks
|
||||
* `Issues` with the Tracker `Quote` will generate an estimate based on the estimated hours and `Item` rates.
|
||||
- Needs to have a `Customer` Assiged
|
||||
* `Payments` Can be created via the Redmine application menu
|
||||
* `Customers` Can be created via the Redmine application menu
|
||||
* `Customers` can be searched
|
||||
* Basic information for the `Customer` can be viewed via the customer page
|
||||
* `Custmoer` information can be update
|
||||
* Webhook Support
|
||||
- `Invoices` are automaticly attached to an Issue if a line item has a hashtag number in a `Line Item`
|
||||
+ `Invoice` Custom Fields are matched Issue Custom Fileds and are automaticly updated in Quickbooks. For example, this is usefull for extracting the Mileage In / Out from the Issue and updating the Invoice with the information.
|
||||
- `Customers` are automaticly updated in local database
|
||||
|
||||
##Prerequisites
|
||||
|
||||
* 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
|
||||
|
||||
@@ -43,20 +57,25 @@ The goal of this project is to allow redmine to connect with Quickbooks Online t
|
||||
|
||||
## Usage
|
||||
|
||||
To enable automatic `QBO Time Activity` entries for an `Issue` , you need only to assign a `QBO Customer` and `QBO Item` to an `Issue` via drop downs in the creation/update form.
|
||||
To enable automatic `Time Activity` entries for an Issue , you need only to assign a `Customer` to an Issue via drop downs in the issue creation/update form.
|
||||
|
||||

|
||||
|
||||
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
|
||||
* Customer ~~Creation~~, ~~Update~~, Deletion
|
||||
* Customer Deletion
|
||||
* Email Customer updates, provding a link that would: bypass the login page, go directly to the issue directing them to, and allow them to view only that issue.
|
||||
* Add a rake file to create required Trackers or statuses required
|
||||
* Add Setting for Sandbox Mode
|
||||
* Refactor Models prefixed with Qbo...
|
||||
* Allow multiple Invoices to be attached to an Issue
|
||||
* Seperate Vehicles into a seperate plugin
|
||||
* Make HTML Pretty
|
||||
* Intergrate Customer Search into Redmine Search
|
||||
* Fix Issue sort by Customer
|
||||
* MORE Stuff...
|
||||
|
||||
##License
|
||||
|
||||
|
||||
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
|
||||
|
||||
51
app/controllers/payments_controller.rb
Normal file
51
app/controllers/payments_controller.rb
Normal file
@@ -0,0 +1,51 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2016 rick barrette
|
||||
#
|
||||
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
#
|
||||
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
#
|
||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
class PaymentsController < ApplicationController
|
||||
unloadable
|
||||
|
||||
include AuthHelper
|
||||
|
||||
before_filter :require_user
|
||||
|
||||
def new
|
||||
@payment = Payment.new
|
||||
|
||||
@customers = Customer.all.sort_by &:name
|
||||
|
||||
@accounts = Qbo.get_base(:account).service.query("SELECT Id, Name FROM Account WHERE AccountType = 'Bank' Order By Name")
|
||||
|
||||
@payment_methods = Qbo.get_base(:payment_method).service.all
|
||||
end
|
||||
|
||||
def create
|
||||
@payment = Payment.new(params[:payment])
|
||||
if @payment.save
|
||||
flash[:notice] = "Payment Saved"
|
||||
redirect_to Customer.find_by_id(@payment.customer_id)
|
||||
else
|
||||
flash[:error] = @payment.errors.full_messages.to_sentence
|
||||
redirect_to new_customer_path
|
||||
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,6 +11,8 @@
|
||||
class QboController < ApplicationController
|
||||
unloadable
|
||||
|
||||
require 'openssl'
|
||||
|
||||
include AuthHelper
|
||||
|
||||
before_filter :require_user, :except => :qbo_webhook
|
||||
@@ -34,7 +36,6 @@ class QboController < ApplicationController
|
||||
def authenticate
|
||||
callback = qbo_oauth_callback_url
|
||||
token = Qbo.get_oauth_consumer.get_request_token(:oauth_callback => callback)
|
||||
#session[:qb_request_token] = token
|
||||
session[:qb_request_token] = Marshal.dump(token)
|
||||
redirect_to("https://appcenter.intuit.com/Connect/Begin?oauth_token=#{token.token}") and return
|
||||
end
|
||||
@@ -43,8 +44,6 @@ class QboController < ApplicationController
|
||||
# Called by QBO after authentication has been processed
|
||||
#
|
||||
def oauth_callback
|
||||
#at = session[:qb_request_token].get_access_token(:oauth_verifier => params[:oauth_verifier])
|
||||
# If Rails >= 4.1 you need to do this =>
|
||||
at = Marshal.load(session[:qb_request_token]).get_access_token(:oauth_verifier => params[:oauth_verifier])
|
||||
|
||||
#There can only be one...
|
||||
@@ -58,62 +57,87 @@ class QboController < ApplicationController
|
||||
qbo.reconnect_token_at = 5.months.from_now.utc
|
||||
qbo.company_id = params['realmId']
|
||||
if qbo.save!
|
||||
redirect_to qbo_path, :flash => { :notice => "Successfully connected to Quickbooks" }
|
||||
redirect_to qbo_sync_path, :flash => { :notice => "Successfully connected to Quickbooks" }
|
||||
else
|
||||
redirect_to plugin_settings_path(:redmine_qbo), :flash => { :error => "Error" }
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# Manual Billing
|
||||
def bill
|
||||
i = Issue.find_by_id params[:id]
|
||||
i.bill_time
|
||||
redirect_to i, :flash => { :notice => "Successfully Billed #{i.customer.name}" }
|
||||
end
|
||||
|
||||
# Quickbooks Webhook Callback
|
||||
def qbo_webhook
|
||||
|
||||
if request.headers['Content-Type'] == 'application/json'
|
||||
data = JSON.parse(request.body.read)
|
||||
# check the payload
|
||||
signature = request.headers['intuit-signature']
|
||||
key = Setting.plugin_redmine_qbo['settingsWebhookToken']
|
||||
data = request.body.read
|
||||
hash = Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha256'), key, data)).strip()
|
||||
|
||||
# proceed if the request is good
|
||||
if hash.eql? signature
|
||||
if request.headers['content-type'] == 'application/json'
|
||||
data = JSON.parse(data)
|
||||
else
|
||||
# application/x-www-form-urlencoded
|
||||
data = params.as_json
|
||||
end
|
||||
# Process the information
|
||||
entities = data['eventNotifications'][0]['dataChangeEvent']['entities']
|
||||
entities.each do |entity|
|
||||
id = entity['id'].to_i
|
||||
name = entity['name']
|
||||
|
||||
# TODO rename all other models!
|
||||
name.prepend("Qbo") if not name.eql? "Customer"
|
||||
|
||||
# Magicly initialize the correct class
|
||||
obj = name.constantize
|
||||
|
||||
# for merge events
|
||||
obj.destroy(entity['deletedId']) if entity['deletedId']
|
||||
|
||||
#Check to see if we are deleting a record
|
||||
if entity['operation'].eql? "Delete"
|
||||
obj.destroy(id)
|
||||
#if not then update!
|
||||
else
|
||||
obj.sync_by_id(id)
|
||||
end
|
||||
end
|
||||
|
||||
# Record that last time we updated
|
||||
Qbo.update_time_stamp
|
||||
|
||||
# The webhook doesn't require a response but let's make sure we don't send anything
|
||||
render :nothing => true
|
||||
else
|
||||
# application/x-www-form-urlencoded
|
||||
data = params.as_json
|
||||
render nothing: true, status: 400
|
||||
end
|
||||
|
||||
entities = data['eventNotifications'][0]['dataChangeEvent']['entities']
|
||||
|
||||
entities.each do |entity|
|
||||
if entity['name'].eql? "Customer"
|
||||
Customer.sync_by_id(entity['id'].to_i)
|
||||
end
|
||||
|
||||
if entity['name'].eql? "Invoice"
|
||||
QboInvoice.sync_by_id(entity['id'].to_i)
|
||||
end
|
||||
|
||||
if entity['name'].eql? "Estimate"
|
||||
QboEstimate.sync_by_id(entity['id'].to_i)
|
||||
end
|
||||
|
||||
if entity['name'].eql? "Employee"
|
||||
QboEmployee.sync_by_id(entity['id'].to_i)
|
||||
end
|
||||
end
|
||||
|
||||
# The webhook doesn't require a response but let's make sure
|
||||
# we don't send anything
|
||||
render :nothing => true
|
||||
end
|
||||
|
||||
#
|
||||
# Synchronizes the QboCustomer table with QBO
|
||||
#
|
||||
def sync
|
||||
if Qbo.exists?
|
||||
Customer.sync
|
||||
QboItem.sync
|
||||
QboEmployee.sync
|
||||
QboEstimate.sync
|
||||
QboInvoice.sync
|
||||
#QboPurchase.sync
|
||||
|
||||
# Record the last sync time
|
||||
Qbo.update_time_stamp
|
||||
# Update info in background
|
||||
Thread.new do
|
||||
if Qbo.exists?
|
||||
Customer.sync
|
||||
QboItem.sync
|
||||
QboEmployee.sync
|
||||
QboEstimate.sync
|
||||
QboInvoice.sync
|
||||
|
||||
# Record the last sync time
|
||||
Qbo.update_time_stamp
|
||||
end
|
||||
ActiveRecord::Base.connection.close
|
||||
end
|
||||
|
||||
redirect_to qbo_path(:redmine_qbo), :flash => { :notice => "Successfully synced to Quickbooks" }
|
||||
|
||||
@@ -18,13 +18,27 @@ 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)
|
||||
@customer = params[:customer_id] if params[:customer_id]
|
||||
end
|
||||
|
||||
# create a new vehicle
|
||||
@@ -86,5 +100,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
|
||||
|
||||
@@ -140,6 +140,22 @@ class Customer < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
# Searchs the database for a customer by name
|
||||
def self.search(search)
|
||||
customers = where("name LIKE ?", "%#{search}%")
|
||||
|
||||
#if customers.empty?
|
||||
# service = Qbo.get_base(:customer).service
|
||||
# results = service.query("Select Id From Customer Where PrimaryPhone LIKE '%#{search}%' AND Mobile LIKE '%#{search}%'")
|
||||
|
||||
# results.each do |customer|
|
||||
# customers << Customer.find_by_id(customer.id)
|
||||
# end
|
||||
#end
|
||||
|
||||
return customers.order(:name)
|
||||
end
|
||||
|
||||
# proforms a bruteforce sync operation
|
||||
# This needs to be simplified
|
||||
def self.sync_by_id(id)
|
||||
|
||||
37
app/models/payment.rb
Normal file
37
app/models/payment.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2016 rick barrette
|
||||
#
|
||||
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
#
|
||||
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
#
|
||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
class Payment
|
||||
unloadable
|
||||
|
||||
include ActiveModel::Model
|
||||
|
||||
attr_accessor :errors, :customer_id, :account_id, :payment_method_id, :total_amount
|
||||
validates_presence_of :customer_id, :account_id, :payment_method_id, :total_amount
|
||||
validates :total_amount, numericality: true
|
||||
|
||||
def save
|
||||
payment = Quickbooks::Model::Payment.new
|
||||
payment.customer_id = @customer_id.to_i
|
||||
payment.deposit_to_account_id = @account_id.to_i
|
||||
payment.payment_method_id = @payment_method_id.to_i
|
||||
payment.total = @total_amount
|
||||
Qbo.get_base(:payment).service.update(payment)
|
||||
end
|
||||
|
||||
def save!
|
||||
save
|
||||
end
|
||||
|
||||
# Dummy stub to make validtions happy.
|
||||
def update_attribute
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
@@ -34,8 +34,7 @@ class QboEmployee < ActiveRecord::Base
|
||||
|
||||
def self.sync_by_id(id)
|
||||
employee = get_base.service.fetch_by_id(id)
|
||||
|
||||
qbo_employee = find_or_create_by(id: employee.id)
|
||||
qbo_employee = find_or_create_by(id: employee.id)
|
||||
qbo_employee.name = employee.display_name
|
||||
qbo_employee.id = employee.id
|
||||
qbo_employee.save!
|
||||
|
||||
@@ -10,18 +10,17 @@
|
||||
|
||||
class QboInvoice < ActiveRecord::Base
|
||||
unloadable
|
||||
has_many :issues
|
||||
|
||||
has_and_belongs_to_many :issues
|
||||
attr_accessible :doc_number
|
||||
validates_presence_of :id, :doc_number
|
||||
validates_presence_of :doc_number
|
||||
self.primary_key = :id
|
||||
|
||||
def self.get_base
|
||||
Qbo.get_base(:invoice)
|
||||
end
|
||||
|
||||
def self.sync
|
||||
#Pull the invoices from the quickbooks server
|
||||
#invoices = get_base.service.all
|
||||
|
||||
last = Qbo.first.last_sync
|
||||
|
||||
query = "SELECT Id, DocNumber FROM Invoice"
|
||||
@@ -35,29 +34,70 @@ class QboInvoice < ActiveRecord::Base
|
||||
|
||||
# Update the invoice table
|
||||
invoices.each { | invoice |
|
||||
qbo_invoice = find_or_create_by(id: invoice.id)
|
||||
qbo_invoice.doc_number = invoice.doc_number
|
||||
qbo_invoice.id = invoice.id
|
||||
qbo_invoice.save!
|
||||
sync_by_id invoice.id
|
||||
}
|
||||
|
||||
#remove deleted invoices
|
||||
#where.not(invoices.map(&:id)).destroy_all
|
||||
end
|
||||
|
||||
def self.sync_by_id(id)
|
||||
invoice = get_base.service.fetch_by_id(id)
|
||||
#update the information in the database
|
||||
invoice = Qbo.get_base(:invoice).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!
|
||||
|
||||
is_changed = false
|
||||
|
||||
# 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)
|
||||
begin
|
||||
i.qbo_invoices << QboInvoice.find_by_id(invoice.id.to_i)
|
||||
i.save!
|
||||
rescue
|
||||
# do nothing, the reccord exists
|
||||
end
|
||||
|
||||
# update the invoive custom fields with infomation from the work ticket if available
|
||||
invoice.custom_fields.each { |cf|
|
||||
# VIN
|
||||
begin
|
||||
if cf.name.eql? "VIN"
|
||||
vin = Vehicle.find(i.vehicles_id).vin
|
||||
break if vin.blank?
|
||||
cf.string_value = vin if not cf.string_value.to_s.eql? vin
|
||||
break
|
||||
end
|
||||
rescue
|
||||
#do nothing
|
||||
end
|
||||
|
||||
# Custom Values
|
||||
begin
|
||||
value = i.custom_values.find_by(custom_field_id: CustomField.find_by_name(cf.name).id)
|
||||
if not value.value.to_s.blank?
|
||||
if not cf.string_value.to_s.eql? value.value.to_s
|
||||
cf.string_value = value.value.to_s
|
||||
is_changed = true
|
||||
end
|
||||
end
|
||||
rescue
|
||||
# Nothing to do here, there is no match
|
||||
end
|
||||
}
|
||||
# Push updates
|
||||
Qbo.get_base(:invoice).service.update(invoice) if is_changed
|
||||
}
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
def self.update(id)
|
||||
# Update the item table
|
||||
invoice = get_base.service.fetch_by_id(id)
|
||||
qbo_invoice = find_or_create_by(id: id)
|
||||
qbo_invoice.doc_number = invoice.doc_number
|
||||
qbo_invoice.save!
|
||||
qbo_invoice = find_or_create_by(id: id)
|
||||
qbo_invoice.doc_number = invoice.doc_number
|
||||
qbo_invoice.save!
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21,7 +21,7 @@ class Vehicle < ActiveRecord::Base
|
||||
|
||||
validates_presence_of :customer
|
||||
validates :vin, uniqueness: true
|
||||
validates :year, numericality: { only_integer: true }
|
||||
#validates :year, numericality: { only_integer: true }
|
||||
|
||||
before_save :decode_vin
|
||||
after_initialize :get_details
|
||||
@@ -72,7 +72,12 @@ class Vehicle < ActiveRecord::Base
|
||||
# Force Upper Case for VIN numbers
|
||||
def vin=(val)
|
||||
# The to_s is in case you get nil/non-string
|
||||
write_attribute(:vin, val.to_s.upcase)
|
||||
write_attribute(:vin, val.to_s.scan(/^[A-Za-z0-9]+$/).join.upcase)
|
||||
end
|
||||
|
||||
# search for a vin
|
||||
def self.search(search)
|
||||
where("vin LIKE ?", "%#{search}%")
|
||||
end
|
||||
|
||||
private
|
||||
@@ -121,4 +126,5 @@ class Vehicle < ActiveRecord::Base
|
||||
v = self.vin[0,11]
|
||||
return v.slice(0,8) + v.slice(9,11)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -12,12 +12,37 @@
|
||||
|
||||
<tr>
|
||||
<th>Primary Phone</th>
|
||||
<td><%= customer.primary_phone %></td>
|
||||
<td><%= number_to_phone(customer.primary_phone.gsub(/[^\d]/, '').to_i, area_code: true) if customer.primary_phone %></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Mobile Phone</th>
|
||||
<td><%= customer.mobile_phone %></td>
|
||||
<td><%= number_to_phone(customer.mobile_phone.gsub(/[^\d]/, '').to_i, area_code: true) if customer.mobile_phone %></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Bill Address</th>
|
||||
<td><%= customer.billing_address %></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Shipping Address</th>
|
||||
<td><%= customer.shipping_address %></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Issues</th>
|
||||
<td><%= customer.issues.count %></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Account Balance</th>
|
||||
<td>$<%= customer.balance %></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Balance With Jobs</th>
|
||||
<td>$<%= customer.balance_with_jobs %></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
@@ -26,12 +51,6 @@
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Issues</th>
|
||||
<td><%= customer.issues.count %></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td/>
|
||||
<td>
|
||||
<%= button_to "Edit Customer", edit_customer_path(customer), method: :get%>
|
||||
</td>
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
<h1>Customers</h1>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<%= form_tag(customers_path, :method => "get", id: "search-form") do %>
|
||||
<%= text_field_tag :search, params[:search], placeholder: "Search Customers" %>
|
||||
<%= submit_tag "Search" %>
|
||||
<% end %>
|
||||
|
||||
<br/>
|
||||
<% if @customers.present? %>
|
||||
<br/>
|
||||
<% @customers.each do |c| %>
|
||||
<div class="row">
|
||||
<div class="span6 columns">
|
||||
<%= link_to c, customer_path(c.id) %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<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>
|
||||
<%= Customer.count %> Customers - <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} %>
|
||||
|
||||
42
app/views/payments/_form.html.erb
Normal file
42
app/views/payments/_form.html.erb
Normal file
@@ -0,0 +1,42 @@
|
||||
<div class="row">
|
||||
<div class="span6 columns">
|
||||
<fieldset>
|
||||
|
||||
<%= form_for @payment do |f| %>
|
||||
|
||||
<div class="clearfix">
|
||||
Customer:
|
||||
<div class="input">
|
||||
<%= f.collection_select :customer_id, @customers, :id, :name, include_blank: true, :selected => @customer, :required => true%>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix">
|
||||
Deposit to Account:
|
||||
<div class="input">
|
||||
<%= f.collection_select :account_id, @accounts, :id, :name, include_blank: true, :selected => @account, :required => true%>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix">
|
||||
Payment Method:
|
||||
<div class="input">
|
||||
<%= f.collection_select :payment_method_id, @payment_methods, :id, :name, include_blank: true, :selected => @payment_method, :required => true%>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix">
|
||||
Amount:
|
||||
<div class="input">
|
||||
<%= f.number_field :total_amount %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
3
app/views/payments/new.html.erb
Normal file
3
app/views/payments/new.html.erb
Normal file
@@ -0,0 +1,3 @@
|
||||
<h1>New Payment</h1>
|
||||
<br/>
|
||||
<%= render :partial => 'payments/form' %>
|
||||
@@ -46,6 +46,15 @@ intuit.ipp.anywhere.setup({menuProxy: '/path/to/blue-dot', grantUrl: '<%= qbo_au
|
||||
name="settings[settingsOAuthConsumerSecret]" >
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Intuit QBO Webhook Token</th>
|
||||
<td>
|
||||
<input type="text" style="width:350px" id="settingsWebhookToken"
|
||||
value="<%= settings['settingsWebhookToken'] %>"
|
||||
name="settings[settingsWebhookToken]" >
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Token Expires At</th>
|
||||
@@ -63,6 +72,36 @@ intuit.ipp.anywhere.setup({menuProxy: '/path/to/blue-dot', grantUrl: '<%= qbo_au
|
||||
<br/>
|
||||
Note: You need to authenticate after saving your key and secret above
|
||||
<br/>
|
||||
<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>
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
<%= @f.collection_select :vehicle_id, @customer.vehicles.order(:year), :id, :vin, include_blank: true, :selected => @vehicle%>
|
||||
Partial Test
|
||||
@@ -41,18 +41,14 @@
|
||||
<div class="clearfix">
|
||||
Notes:
|
||||
<div class="input">
|
||||
<p>
|
||||
<%= link_to_function content_tag(:span, l(:button_edit), :class => 'icon icon-edit'), '$(this).hide(); $("#issue_description_and_toolbar").show()' unless @vehicle.new_record? %>
|
||||
<p>
|
||||
<%= content_tag 'span', :id => "issue_description_and_toolbar", :style => (@vehicle.new_record? ? nil : 'display:none') do %>
|
||||
<%= f.text_area :notes,
|
||||
<%= f.text_area :notes,
|
||||
:cols => 60,
|
||||
:rows => 10,
|
||||
:accesskey => accesskey(:edit),
|
||||
:class => 'wiki-edit',
|
||||
:no_label => true %>
|
||||
<% end %>
|
||||
</p>
|
||||
<%= wikitoolbar_for 'issue_description' %>
|
||||
</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,9 @@
|
||||
<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" %>
|
||||
<% 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%;">
|
||||
|
||||
4
assets/javascripts/autocomplete.js
Normal file
4
assets/javascripts/autocomplete.js
Normal file
@@ -0,0 +1,4 @@
|
||||
//= require jquery
|
||||
//= require jquery_ujs
|
||||
//= require jquery-ui
|
||||
//= require autocomplete-rails
|
||||
@@ -8,17 +8,36 @@
|
||||
#
|
||||
#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.
|
||||
|
||||
# Plugin's routes
|
||||
# See: http://guides.rubyonrails.org/routing.html
|
||||
#
|
||||
# Main Quickbooks landing page
|
||||
get 'qbo', :to=> 'qbo#index'
|
||||
|
||||
#authentication
|
||||
get 'qbo/authenticate', :to => 'qbo#authenticate'
|
||||
get 'qbo/oauth_callback', :to => 'qbo#oauth_callback'
|
||||
|
||||
#manual sync
|
||||
get 'qbo/sync', :to => 'qbo#sync'
|
||||
|
||||
# Estimate & Invoice PDF
|
||||
get 'qbo/estimate/:id', :to => 'estimate#show', as: :estimate
|
||||
get 'qbo/invoice/:id', :to => 'invoice#show', as: :invoice
|
||||
|
||||
#manual billing
|
||||
get 'qbo/bill/:id', :to => 'qbo#bill', as: :bill
|
||||
|
||||
#payments
|
||||
resources :payments
|
||||
|
||||
#webhook
|
||||
post 'qbo/webhook', :to => 'qbo#qbo_webhook'
|
||||
|
||||
#ajax
|
||||
get "update_vehicles" => 'vehicles#update_vehicles', as: 'update_vehicles'
|
||||
|
||||
# Nest Vehicles under customers
|
||||
resources :customers do
|
||||
resources :vehicles
|
||||
end
|
||||
|
||||
#allow for just vehicles too
|
||||
resources :vehicles
|
||||
resources :customers
|
||||
|
||||
27
db/migrate/21_add_issues_qbo_invoices.rb
Normal file
27
db/migrate/21_add_issues_qbo_invoices.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2016 rick barrette
|
||||
#
|
||||
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
#
|
||||
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
#
|
||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
class AddIssuesQboInvoices < ActiveRecord::Migration
|
||||
def self.up
|
||||
create_table :issues_qbo_invoices, :id => false do |t|
|
||||
t.references :issue
|
||||
t.references :qbo_invoice
|
||||
end
|
||||
|
||||
add_index :issues_qbo_invoices, [:issue_id, :qbo_invoice_id], :unique => true
|
||||
|
||||
# Now populate it with a SQL one-liner!
|
||||
execute "insert into issues_qbo_invoices(issue_id, qbo_invoice_id) select id, qbo_invoice_id from issues"
|
||||
end
|
||||
|
||||
def self.down
|
||||
drop_table :issues_qbo_invoices
|
||||
end
|
||||
end
|
||||
20
init.rb
20
init.rb
@@ -15,17 +15,22 @@ Redmine::Plugin.register :redmine_qbo do
|
||||
require_dependency 'issues_save_hook_listener'
|
||||
require_dependency 'issues_show_hook_listener'
|
||||
require_dependency 'users_show_hook_listener'
|
||||
require_dependency 'header_footer_hook_listener.rb'
|
||||
|
||||
# Patches to the Redmine core. Will not work in development mode
|
||||
require_dependency 'issue_patch'
|
||||
require_dependency 'user_patch'
|
||||
require_dependency 'query_patch'
|
||||
require_dependency 'pdf_patch'
|
||||
|
||||
Rails.configuration.to_prepare do
|
||||
Redmine::Search.available_search_types << 'customers'
|
||||
end
|
||||
|
||||
name 'Redmine Quickbooks Online plugin'
|
||||
author 'Rick Barrette'
|
||||
description 'This is a plugin for Redmine to intergrate with Quickbooks Online to allow for seamless intergration CRM and invoicing of completed issues'
|
||||
version '0.0.7'
|
||||
version '0.4.0'
|
||||
url 'https://github.com/rickbarrette/redmine_qbo'
|
||||
author_url 'http://rickbarrette.org'
|
||||
settings :default => {'empty' => true}, :partial => 'qbo/settings'
|
||||
@@ -46,7 +51,16 @@ 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? }
|
||||
menu :application_menu, :new_payment, { :controller => :payments, :action => :new }, :caption => 'New Payment', :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 => :new_issue, :param => :project_id
|
||||
|
||||
permission :payments, { :payments => [:index, :new] }, :public => false
|
||||
menu :project_menu, :payments, { :controller => 'payments', :action => 'new' }, :caption => 'New Payment', :after => :customers, :param => :project_id
|
||||
end
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
#
|
||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
class HeaderFooterHookListener < Redmine::Hook::ViewListener
|
||||
|
||||
module ActiveSupport::Callbacks::ClassMethods
|
||||
def without_callback(*args, &block)
|
||||
skip_callback(*args)
|
||||
yield
|
||||
set_callback(*args)
|
||||
def view_layouts_base_html_head(context = {})
|
||||
#nothing
|
||||
end
|
||||
|
||||
def view_layouts_base_body_bottom(context = {})
|
||||
return "<div class='footer' align='center'><b>Last Sync: </b> #{Qbo.last_sync}</div>"
|
||||
end
|
||||
end
|
||||
@@ -23,9 +23,10 @@ module IssuePatch
|
||||
base.class_eval do
|
||||
unloadable # Send unloadable so it will not be unloaded in development
|
||||
belongs_to :customer, primary_key: :id
|
||||
belongs_to :qbo_item, primary_key: :id
|
||||
belongs_to :qbo_estimate, primary_key: :id
|
||||
belongs_to :qbo_invoice, primary_key: :id
|
||||
has_and_belongs_to_many :qbo_invoices
|
||||
#, :association_foreign_key => 'issue_id', :class_name => 'Issue', :join_table => 'issues_qbo_invoices'
|
||||
|
||||
belongs_to :vehicle, primary_key: :id
|
||||
end
|
||||
|
||||
@@ -36,7 +37,57 @@ module IssuePatch
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
|
||||
# Create billable time entries
|
||||
def bill_time
|
||||
|
||||
# Get unbilled time entries
|
||||
spent_time = time_entries.where(qbo_billed: [false, nil])
|
||||
spent_hours ||= spent_time.sum(:hours) || 0
|
||||
|
||||
if spent_hours > 0 then
|
||||
|
||||
# Prepare to create a new Time Activity
|
||||
time_service = Qbo.get_base(:time_activity).service
|
||||
item_service = Qbo.get_base(:item).service
|
||||
time_entry = Quickbooks::Model::TimeActivity.new
|
||||
|
||||
h = Hash.new(0)
|
||||
spent_time.each do |entry|
|
||||
# Lets tottal up each activity
|
||||
h[entry.activity.name] += entry.hours
|
||||
# update time entries billed status
|
||||
entry.qbo_billed = true
|
||||
entry.save
|
||||
end
|
||||
|
||||
h.each do |key, val|
|
||||
|
||||
# Convert float spent time to hours and minutes
|
||||
hours = val.to_i
|
||||
minutesDecimal = (( val - hours) * 60)
|
||||
minutes = minutesDecimal.to_i
|
||||
|
||||
item = item_service.query("SELECT * FROM Item WHERE Name = '#{key}' ").first
|
||||
next if item.nil?
|
||||
|
||||
time_entry.description = "#{tracker} ##{id}: #{subject} #{"(Partial)" if not closed?}"
|
||||
# TODO entry.user.qbo_employee.id
|
||||
time_entry.employee_id = assigned_to.qbo_employee_id
|
||||
time_entry.customer_id = customer_id
|
||||
time_entry.billable_status = "Billable"
|
||||
time_entry.hours = hours
|
||||
time_entry.minutes = minutes
|
||||
time_entry.name_of = "Employee"
|
||||
time_entry.txn_date = Date.today
|
||||
time_entry.hourly_rate = item.unit_price
|
||||
time_entry.item_id = item.id
|
||||
time_entry.start_time = start_date
|
||||
time_entry.end_time = Time.now
|
||||
time_service.create(time_entry)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
|
||||
class IssuesFormHookListener < Redmine::Hook::ViewListener
|
||||
|
||||
# Load the javascript
|
||||
def view_layouts_base_html_head(context = {})
|
||||
javascript_include_tag 'vehicles', :plugin => 'redmine_qbo'
|
||||
javascript_include_tag 'autocomplete', :plugin => 'redmine_qbo'
|
||||
end
|
||||
|
||||
# Edit Issue Form
|
||||
# Show a dropdown for quickbooks contacts
|
||||
def view_issues_form_details_bottom(context={})
|
||||
@@ -17,8 +23,6 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener
|
||||
|
||||
# Check to see if there is a quickbooks user attached to the issue
|
||||
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
|
||||
|
||||
@@ -26,12 +30,6 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener
|
||||
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
|
||||
|
||||
# Generate the drop down list of quickbooks invoices
|
||||
select_invoice = f.select :qbo_invoice_id, QboInvoice.all.pluck(:doc_number, :id).sort! {|x, y| y <=> x}, :selected => selected_invoice, include_blank: true
|
||||
|
||||
# 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
|
||||
|
||||
@@ -43,6 +41,6 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener
|
||||
|
||||
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>"
|
||||
return "<p>#{select_customer}</p> <p>#{select_estimate}</p> <p>#{vehicle}</p>"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,7 +15,7 @@ class IssuesSaveHookListener < Redmine::Hook::ViewListener
|
||||
issue = context[:issue]
|
||||
|
||||
# Check to see if we have registered with QBO
|
||||
if Qbo.first && issue.customer && issue.qbo_item
|
||||
if Qbo.first && issue.customer && issue. qbo_item_id
|
||||
|
||||
# if this is a quote, lets create a new estimate based off estimated hours
|
||||
if issue.tracker.name = "Quote" && issue.status.name = "New" && issue.qbo_estimate
|
||||
@@ -44,10 +44,6 @@ class IssuesSaveHookListener < Redmine::Hook::ViewListener
|
||||
|
||||
# Add the line items to the estimate
|
||||
estimate.line_items << line_item
|
||||
|
||||
# Save the etimate to the issue
|
||||
#issue.qbo_estimate_id = estimate_base.service.create(estimate).id
|
||||
#issue.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -55,55 +51,6 @@ class IssuesSaveHookListener < Redmine::Hook::ViewListener
|
||||
# Called After Issue Saved
|
||||
def controller_issues_edit_after_save(context={})
|
||||
issue = context[:issue]
|
||||
|
||||
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?
|
||||
end
|
||||
end
|
||||
|
||||
# Create billable time entries
|
||||
def bill_time(issue, employee_id)
|
||||
|
||||
# Get unbilled time entries
|
||||
spent_time = issue.time_entries.where(qbo_billed: [false, nil])
|
||||
spent_hours ||= spent_time.sum(:hours) || 0
|
||||
|
||||
if spent_hours > 0 then
|
||||
|
||||
# Prepare to create a new Time Activity
|
||||
time_service = Qbo.get_base(:time_activity).service
|
||||
item_service = Qbo.get_base(:item).service
|
||||
time_entry = Quickbooks::Model::TimeActivity.new
|
||||
|
||||
# Convert float spent time to hours and minutes
|
||||
hours = spent_hours.to_i
|
||||
minutesDecimal = (( spent_hours - hours) * 60)
|
||||
minutes = minutesDecimal.to_i
|
||||
|
||||
# update time entries billed status
|
||||
spent_time.each do |entry|
|
||||
entry.qbo_billed = true
|
||||
entry.save
|
||||
end
|
||||
|
||||
item = item_service.fetch_by_id issue.qbo_item_id
|
||||
time_entry.description = "#{issue.tracker} ##{issue.id}: #{issue.subject}"
|
||||
time_entry.employee_id = employee_id
|
||||
time_entry.customer_id = issue.customer_id
|
||||
time_entry.billable_status = "Billable"
|
||||
time_entry.hours = hours
|
||||
time_entry.minutes = minutes
|
||||
time_entry.name_of = "Employee"
|
||||
time_entry.txn_date = Date.today
|
||||
time_entry.hourly_rate = item.unit_price
|
||||
time_entry.item_id = issue.qbo_item_id
|
||||
time_entry.start_time = issue.start_date
|
||||
time_entry.end_time = Time.now
|
||||
time_service.create(time_entry)
|
||||
end
|
||||
issue.bill_time if Qbo.first && issue.customer && issue.status.is_closed?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,9 +25,6 @@ class IssuesShowHookListener < Redmine::Hook::ViewListener
|
||||
customer = link_to issue.customer.name, "#{Redmine::Utils::relative_url_root}/customers/#{issue.customer.id}"
|
||||
end
|
||||
|
||||
# Check to see if there is a quickbooks item attached to the issue
|
||||
item = issue.qbo_item ? issue.qbo_item.name : nil
|
||||
|
||||
# Estimate Number
|
||||
if issue.qbo_estimate
|
||||
estimate = issue.qbo_estimate.doc_number
|
||||
@@ -35,12 +32,14 @@ class IssuesShowHookListener < Redmine::Hook::ViewListener
|
||||
end
|
||||
|
||||
# Invoice Number
|
||||
if issue.qbo_invoice
|
||||
invoice = issue.qbo_invoice.doc_number
|
||||
invoice_link = link_to invoice, "#{Redmine::Utils::relative_url_root}/qbo/invoice/#{issue.qbo_invoice.id}", :target => "_blank"
|
||||
invoice_link = ""
|
||||
if issue.qbo_invoice_ids
|
||||
issue.qbo_invoice_ids.each do |i|
|
||||
invoice = QboInvoice.find i
|
||||
invoice_link = invoice_link + link_to( invoice.doc_number, "#{Redmine::Utils::relative_url_root}/qbo/invoice/#{i}", :target => "_blank").to_s + " "
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
begin
|
||||
v = Vehicle.find(issue.vehicles_id)
|
||||
vehicle = link_to v.to_s, "#{Redmine::Utils::relative_url_root}/vehicles/#{v.id}"
|
||||
@@ -50,6 +49,8 @@ class IssuesShowHookListener < Redmine::Hook::ViewListener
|
||||
#do nothing
|
||||
end
|
||||
|
||||
split_vin = vin.scan(/.{1,9}/) if vin
|
||||
|
||||
return "
|
||||
<div class=\"attributes\">
|
||||
|
||||
@@ -57,11 +58,6 @@ class IssuesShowHookListener < Redmine::Hook::ViewListener
|
||||
<div class=\"label\"><span>Customer</span>:</div>
|
||||
<div class=\"value\">#{customer}</div>
|
||||
</div>
|
||||
|
||||
<div class=\"qbo_item_id attribute\">
|
||||
<div class=\"label\"><span>Item</span>:</div>
|
||||
<div class=\"value\">#{item}</div>
|
||||
</div>
|
||||
|
||||
<div class=\"qbo_estimate_id attribute\">
|
||||
<div class=\"label\"><span>Estimate</span>:</div>
|
||||
@@ -82,14 +78,18 @@ class IssuesShowHookListener < Redmine::Hook::ViewListener
|
||||
|
||||
<div class=\"vehicle_vin attribute\">
|
||||
<div class=\"label\"><span>VIN</span>:</div>
|
||||
<div class=\"value\">#{vin.gsub(/(.{9})/, '\1 ') if vin}</div>
|
||||
<div class=\"value\">#{split_vin[0] if split_vin}<b>#{split_vin[1] if split_vin}</b></div>
|
||||
</div>
|
||||
|
||||
<div class=\"vehicle_notes attribute\">
|
||||
<div class=\"label\"><span>Notes</span>:</div>
|
||||
<div class=\"value\">#{notes}</div>
|
||||
</div>
|
||||
</div>"
|
||||
</div> "
|
||||
end
|
||||
|
||||
def view_issues_show_description_bottom(context={})
|
||||
return "<br/> #{button_to "Bill Time", "#{Redmine::Utils::relative_url_root}/qbo/bill/#{context[:issue].id}", method: :get}"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user